diff --git a/.classpath b/.classpath index 708ddd7..b29a569 100644 --- a/.classpath +++ b/.classpath @@ -25,7 +25,6 @@ - @@ -33,9 +32,9 @@ - + - + diff --git a/list.txt b/list.txt index e8dfa15..7397f9d 100644 --- a/list.txt +++ b/list.txt @@ -12,4 +12,6 @@ src\main\webapp\WEB-INF\jsp\tiles\attribute\app.submenu.jsp src\main\webapp\WEB-INF\jsp\sgis\map\mapInformation\sichudanNew.jsp src\main\webapp\com\css\cross-section.css src\main\webapp\com\js\cross-section.js -src\main\resources\egovframework\mapper\sgis\map\MapMainMapper.xml \ No newline at end of file +src\main\resources\egovframework\mapper\sgis\map\MapMainMapper.xml +src\main\webapp\WEB-INF\jsp\sgis\map\mapInformation\boreholeLog.jsp +src\main\webapp\WEB-INF\jsp\sgis\surveysystem\createSurvey.jsp \ No newline at end of file diff --git a/pom.xml b/pom.xml index 3cea8b9..588afd9 100644 --- a/pom.xml +++ b/pom.xml @@ -306,6 +306,11 @@ jackson-databind 2.12.5 + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.12.5 + com.fasterxml.jackson.core jackson-annotations diff --git a/src/main/java/sgis/board/typehandler/UUIDTypeHandler.java b/src/main/java/sgis/board/typehandler/UUIDTypeHandler.java new file mode 100644 index 0000000..d64e721 --- /dev/null +++ b/src/main/java/sgis/board/typehandler/UUIDTypeHandler.java @@ -0,0 +1,40 @@ +package sgis.board.typehandler; + +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; +import org.apache.ibatis.type.MappedTypes; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.UUID; + +@MappedTypes(UUID.class) +@MappedJdbcTypes(JdbcType.OTHER) // Or JdbcType.VARCHAR if storing as string in DB +public class UUIDTypeHandler extends BaseTypeHandler { + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, UUID parameter, JdbcType jdbcType) throws SQLException { + ps.setString(i, parameter.toString()); + } + + @Override + public UUID getNullableResult(ResultSet rs, String columnName) throws SQLException { + String uuidString = rs.getString(columnName); + return uuidString == null ? null : UUID.fromString(uuidString); + } + + @Override + public UUID getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + String uuidString = rs.getString(columnIndex); + return uuidString == null ? null : UUID.fromString(uuidString); + } + + @Override + public UUID getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + String uuidString = cs.getString(columnIndex); + return uuidString == null ? null : UUID.fromString(uuidString); + } +} \ No newline at end of file diff --git a/src/main/java/sgis/map/mapper/MapMainMapper.java b/src/main/java/sgis/map/mapper/MapMainMapper.java index 97740d5..9ea266b 100644 --- a/src/main/java/sgis/map/mapper/MapMainMapper.java +++ b/src/main/java/sgis/map/mapper/MapMainMapper.java @@ -49,4 +49,6 @@ public interface MapMainMapper { EgovMap selectWebDownloadLog(Map params) throws Exception; // dhlee 사용자별 다운로드 요청한 개수를 얻어온다. List selectProjectList(Map params) throws Exception; + + List mapBoreholeLogHeader(Map params) throws Exception; } diff --git a/src/main/java/sgis/map/service/MapMainService.java b/src/main/java/sgis/map/service/MapMainService.java index 4b1df9e..c8c6a30 100644 --- a/src/main/java/sgis/map/service/MapMainService.java +++ b/src/main/java/sgis/map/service/MapMainService.java @@ -50,4 +50,5 @@ public interface MapMainService { List selectProjectList(Map params) throws Exception; // dhlee + List mapBoreholeLogList(Map params) throws Exception; } diff --git a/src/main/java/sgis/map/service/impl/MapMainServiceImpl.java b/src/main/java/sgis/map/service/impl/MapMainServiceImpl.java index e7a7191..7b9d934 100644 --- a/src/main/java/sgis/map/service/impl/MapMainServiceImpl.java +++ b/src/main/java/sgis/map/service/impl/MapMainServiceImpl.java @@ -117,4 +117,15 @@ public class MapMainServiceImpl implements MapMainService { public List selectProjectList(Map params) throws Exception { return mapMainMapper.selectProjectList(params); } + + @Override + public List mapBoreholeLogList(Map params) throws Exception { + // TODO Auto-generated method stub + //String businessCode = String.valueOf(params.get("businessCode")); + //String holeCode = String.valueOf(params.get("holeCode")); + + List mapBoreholeLogHeader = mapMainMapper.mapBoreholeLogHeader(params); + + return mapBoreholeLogHeader; + } } diff --git a/src/main/java/sgis/map/web/MapMainController.java b/src/main/java/sgis/map/web/MapMainController.java index b9e8472..e1cf246 100644 --- a/src/main/java/sgis/map/web/MapMainController.java +++ b/src/main/java/sgis/map/web/MapMainController.java @@ -408,6 +408,31 @@ public class MapMainController extends BaseController { return responseJson; } + @RequestMapping(value = "/map/borehole-log.do") + public String mapBoreholeLog(ModelMap model, HttpServletRequest request, HttpServletResponse response, @RequestParam Map params) throws Exception { + return "/sgis/map/mapInformation/boreholeLog"; + } + + /** + * 지반 주상도 생성에 필요한 정보를 얻는다. + * @param model + * @param request + * @param response + * @param params + * @return + * @throws Exception + */ + @RequestMapping(value = "/map/borehole-log/list.do", method = RequestMethod.GET, produces = { MediaType.APPLICATION_JSON_VALUE }) + @ResponseBody + public JSONObject mapBoreholeLogList(ModelMap model, HttpServletRequest request, HttpServletResponse response, @RequestParam Map params) throws Exception { + StringBuffer sb = request.getRequestURL(); + String url = sb.substring(0, sb.lastIndexOf("/")); + + JSONObject responseJson = new JSONObject(); + responseJson.put("list", mapMainService.mapBoreholeLogList(params)); + return responseJson; + } + public JSONObject getSectionData(String code) throws Exception { code = code.trim(); @@ -422,6 +447,10 @@ public class MapMainController extends BaseController { EgovMap general = mapMainService.getGeneralData(params); + + if( general == null ) { + // 예외처리 추가하기 + } String[] values = String.valueOf(general.get("vlu")).split("\\|"); JSONObject resultJson = new JSONObject(); @@ -442,7 +471,11 @@ public class MapMainController extends BaseController { resultJson.put("LL", position); resultJson.put("PNAME", values[7]); - resultJson.put("WATER", values[8]); + if( 8 < values.length && values[8] != null ) { + resultJson.put("WATER", values[8]); + } else { + resultJson.put("WATER", ""); + } if (values.length == 10) { resultJson.put("PCOM", values[9]); diff --git a/src/main/java/sgis/surveysystem/config/LocalDateTimeTypeHandler.java b/src/main/java/sgis/surveysystem/config/LocalDateTimeTypeHandler.java new file mode 100644 index 0000000..b613b78 --- /dev/null +++ b/src/main/java/sgis/surveysystem/config/LocalDateTimeTypeHandler.java @@ -0,0 +1,48 @@ +package sgis.surveysystem.config; + +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; +import org.apache.ibatis.type.MappedTypes; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.LocalDateTime; + +/** + * java.time.LocalDateTime 객체와 JDBC Timestamp (PostgreSQL TIMESTAMP WITH TIME ZONE) 컬럼 간의 매핑을 처리하는 MyBatis TypeHandler. + */ +@MappedJdbcTypes(JdbcType.TIMESTAMP) // 이 TypeHandler가 처리할 JDBC 타입 +@MappedTypes(LocalDateTime.class) // 이 TypeHandler가 처리할 Java 타입 +public class LocalDateTimeTypeHandler extends BaseTypeHandler { + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, LocalDateTime parameter, JdbcType jdbcType) throws SQLException { + // LocalDateTime을 JDBC Timestamp로 변환하여 PreparedStatement에 설정 + ps.setTimestamp(i, Timestamp.valueOf(parameter)); + } + + @Override + public LocalDateTime getNullableResult(ResultSet rs, String columnName) throws SQLException { + // ResultSet에서 컬럼 이름으로 Timestamp 값을 가져와 LocalDateTime으로 변환 + Timestamp ts = rs.getTimestamp(columnName); + return (ts != null) ? ts.toLocalDateTime() : null; + } + + @Override + public LocalDateTime getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + // ResultSet에서 컬럼 인덱스로 Timestamp 값을 가져와 LocalDateTime으로 변환 + Timestamp ts = rs.getTimestamp(columnIndex); + return (ts != null) ? ts.toLocalDateTime() : null; + } + + @Override + public LocalDateTime getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + // CallableStatement에서 Timestamp 값을 가져와 LocalDateTime으로 변환 + Timestamp ts = cs.getTimestamp(columnIndex); + return (ts != null) ? ts.toLocalDateTime() : null; + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/config/UUIDTypeHandler.java b/src/main/java/sgis/surveysystem/config/UUIDTypeHandler.java new file mode 100644 index 0000000..9696141 --- /dev/null +++ b/src/main/java/sgis/surveysystem/config/UUIDTypeHandler.java @@ -0,0 +1,49 @@ +package sgis.surveysystem.config; + +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; +import org.apache.ibatis.type.MappedTypes; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.UUID; + +/** + * java.util.UUID 객체와 PostgreSQL의 UUID (JDBC Type.OTHER) 컬럼 간의 매핑을 처리하는 MyBatis TypeHandler. + */ +@MappedJdbcTypes(JdbcType.OTHER) // 이 TypeHandler가 처리할 JDBC 타입 (PostgreSQL의 UUID는 OTHER로 인식됨) +@MappedTypes(UUID.class) // 이 TypeHandler가 처리할 Java 타입 +public class UUIDTypeHandler extends BaseTypeHandler { + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, UUID parameter, JdbcType jdbcType) throws SQLException { + // Java UUID 객체를 JDBC PreparedStatement에 설정할 때 + // PostgreSQL의 UUID 타입은 String으로 변환하여 설정하거나, setObject(UUID)를 직접 사용할 수 있습니다. + // setString이 더 안전하고 호환성이 높습니다. + ps.setString(i, parameter.toString()); + } + + @Override + public UUID getNullableResult(ResultSet rs, String columnName) throws SQLException { + // ResultSet에서 컬럼 이름으로 값을 가져와 Java UUID 객체로 변환할 때 + String uuidString = rs.getString(columnName); + return (uuidString != null) ? UUID.fromString(uuidString) : null; + } + + @Override + public UUID getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + // ResultSet에서 컬럼 인덱스로 값을 가져와 Java UUID 객체로 변환할 때 + String uuidString = rs.getString(columnIndex); + return (uuidString != null) ? UUID.fromString(uuidString) : null; + } + + @Override + public UUID getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + // CallableStatement에서 값을 가져와 Java UUID 객체로 변환할 때 + String uuidString = cs.getString(columnIndex); + return (uuidString != null) ? UUID.fromString(uuidString) : null; + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/controller/AdminSurveyPageController.java b/src/main/java/sgis/surveysystem/controller/AdminSurveyPageController.java new file mode 100644 index 0000000..ddb90a5 --- /dev/null +++ b/src/main/java/sgis/surveysystem/controller/AdminSurveyPageController.java @@ -0,0 +1,28 @@ +package sgis.surveysystem.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * 관리자 설문 페이지를 위한 컨트롤러. + * JSP 뷰 파일을 반환합니다. + */ +@Controller // 뷰 이름을 반환하는 컨트롤러임을 명시 +@RequestMapping("/admin/survey") // 이 컨트롤러의 기본 URL 경로 +public class AdminSurveyPageController { + + /** + * 설문 생성 폼 페이지를 표시합니다. + * 웹 브라우저에서 http://localhost:8080/your-app-context/admin/survey/create 로 접근 시 이 메서드가 호출됩니다. + * (your-app-context는 웹 애플리케이션의 컨텍스트 경로입니다. 예: /sgis) + * + * @return JSP 파일의 논리적 경로 (ViewResolver에 의해 실제 경로로 변환됨) + */ + @GetMapping("/create.do") + public String showCreateSurveyForm() { + // ViewResolver 설정에 따라 다음 경로의 JSP 파일을 찾게 됩니다: + // /WEB-INF/jsp/ + sgis/surveysystem/createSurvey + .jsp + return "sgis/surveysystem/createSurvey"; + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/controller/QuestionController.java b/src/main/java/sgis/surveysystem/controller/QuestionController.java new file mode 100644 index 0000000..2591acb --- /dev/null +++ b/src/main/java/sgis/surveysystem/controller/QuestionController.java @@ -0,0 +1,143 @@ +package sgis.surveysystem.controller; + +import sgis.surveysystem.dto.QuestionCreateRequest; +import sgis.surveysystem.dto.QuestionResponse; +import sgis.surveysystem.dto.QuestionUpdateRequest; +import sgis.surveysystem.dto.QuestionOptionResponse; // QuestionResponse에서 옵션을 포함하기 위해 필요 +import sgis.surveysystem.service.QuestionService; +import sgis.surveysystem.service.QuestionOptionService; // 문항 옵션을 가져오기 위해 주입 +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * 설문 문항(Question) 관련 HTTP 요청을 처리하는 REST 컨트롤러. + * 문항 생성, 조회, 수정, 삭제 등의 API 엔드포인트를 제공합니다. + */ +@RestController +@RequestMapping("/api/questions") +@RequiredArgsConstructor +public class QuestionController { + + private final QuestionService questionService; + private final QuestionOptionService questionOptionService; // 객관식 문항의 옵션을 로드하기 위해 필요 + + /** + * 새로운 설문 문항을 생성합니다. (HTTP POST) + * + * @param request Question 생성 요청 DTO + * @return 생성된 문항 정보를 담은 응답 (HTTP 201 Created) + */ + @PostMapping + public ResponseEntity createQuestion(@RequestBody QuestionCreateRequest request) { + QuestionResponse createdQuestion = QuestionResponse.from( + questionService.createQuestion( + request.getSurveyId(), + request.getQuestionText(), + request.getQuestionType(), + request.getQuestionOrder(), + request.getIsRequired() + ) + ); + return new ResponseEntity<>(createdQuestion, HttpStatus.CREATED); + } + + /** + * 특정 설문에 속한 모든 문항을 조회합니다. (HTTP GET) + * 문항 유형이 객관식인 경우, 해당 문항의 옵션도 함께 포함하여 반환합니다. + * + * @param surveyId 문항 목록을 조회할 설문 ID + * @return 해당 설문의 모든 문항 정보를 담은 응답 리스트 (HTTP 200 OK) + */ + @GetMapping("/by-survey/{surveyId}") + public ResponseEntity> getQuestionsBySurveyId(@PathVariable UUID surveyId) { + List questions = questionService.getQuestionsBySurveyId(surveyId).stream() + .map(question -> { + QuestionResponse questionResponse = QuestionResponse.from(question); + // 문항 유형이 객관식인 경우, 해당 문항의 옵션을 조회하여 DTO에 설정합니다. + if (question.getQuestionType().startsWith("객관식")) { + List options = questionOptionService.getOptionsByQuestionId(question.getQuestionId()).stream() + .map(QuestionOptionResponse::from) + .collect(Collectors.toList()); + questionResponse.setOptions(options); // QuestionResponse DTO에 옵션 리스트 설정 + } + return questionResponse; + }) + .collect(Collectors.toList()); + return ResponseEntity.ok(questions); + } + + /** + * 특정 ID의 문항을 조회합니다. (HTTP GET) + * 문항 유형이 객관식인 경우, 해당 문항의 옵션도 함께 포함하여 반환합니다. + * + * @param questionId 조회할 문항의 고유 ID + * @return 조회된 문항 정보를 담은 응답 (HTTP 200 OK) + * 문항이 없을 경우 HTTP 404 Not Found 반환 + */ + @GetMapping("/{questionId}") + public ResponseEntity getQuestionById(@PathVariable UUID questionId) { + try { + QuestionResponse question = QuestionResponse.from(questionService.getQuestionById(questionId)); + // 문항 유형이 객관식인 경우, 해당 문항의 옵션을 조회하여 DTO에 설정합니다. + if (question.getQuestionType().startsWith("객관식")) { + List options = questionOptionService.getOptionsByQuestionId(question.getQuestionId()).stream() + .map(QuestionOptionResponse::from) + .collect(Collectors.toList()); + question.setOptions(options); // QuestionResponse DTO에 옵션 리스트 설정 + } + return ResponseEntity.ok(question); + } catch (NoSuchElementException e) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + + /** + * 문항 정보를 업데이트합니다. (HTTP PUT) + * + * @param questionId 업데이트할 문항의 고유 ID + * @param request Question 업데이트 요청 DTO + * @return 업데이트된 문항 정보를 담은 응답 (HTTP 200 OK) + * 문항이 없을 경우 HTTP 404 Not Found 반환 + */ + @PutMapping("/{questionId}") + public ResponseEntity updateQuestion(@PathVariable UUID questionId, @RequestBody QuestionUpdateRequest request) { + try { + QuestionResponse updatedQuestion = QuestionResponse.from( + questionService.updateQuestion( + questionId, + request.getQuestionText(), + request.getQuestionType(), + request.getQuestionOrder(), + request.getIsRequired() + ) + ); + return ResponseEntity.ok(updatedQuestion); + } catch (NoSuchElementException e) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + + /** + * 특정 ID의 문항을 삭제합니다. (HTTP DELETE) + * + * @param questionId 삭제할 문항의 고유 ID + * @return 응답 본문 없이 HTTP 204 No Content 반환 + * 문항이 없을 경우 HTTP 404 Not Found 반환 + */ + @DeleteMapping("/{questionId}") + public ResponseEntity deleteQuestion(@PathVariable UUID questionId) { + try { + questionService.deleteQuestion(questionId); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } catch (NoSuchElementException e) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/controller/QuestionOptionController.java b/src/main/java/sgis/surveysystem/controller/QuestionOptionController.java new file mode 100644 index 0000000..a439146 --- /dev/null +++ b/src/main/java/sgis/surveysystem/controller/QuestionOptionController.java @@ -0,0 +1,117 @@ +package sgis.surveysystem.controller; + +import sgis.surveysystem.dto.QuestionOptionCreateRequest; +import sgis.surveysystem.dto.QuestionOptionResponse; +import sgis.surveysystem.dto.QuestionOptionUpdateRequest; +import sgis.surveysystem.service.QuestionOptionService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * 객관식 문항 선택지(QuestionOption) 관련 HTTP 요청을 처리하는 REST 컨트롤러. + * 선택지 생성, 조회, 수정, 삭제 등의 API 엔드포인트를 제공합니다. + */ +@RestController +@RequestMapping("/api/question-options") +@RequiredArgsConstructor +public class QuestionOptionController { + + private final QuestionOptionService questionOptionService; + + /** + * 새로운 객관식 문항 선택지를 생성합니다. (HTTP POST) + * + * @param request QuestionOption 생성 요청 DTO + * @return 생성된 선택지 정보를 담은 응답 (HTTP 201 Created) + */ + @PostMapping + public ResponseEntity createQuestionOption(@RequestBody QuestionOptionCreateRequest request) { + QuestionOptionResponse createdOption = QuestionOptionResponse.from( + questionOptionService.createQuestionOption( + request.getQuestionId(), + request.getOptionText(), + request.getOptionOrder() + ) + ); + return new ResponseEntity<>(createdOption, HttpStatus.CREATED); + } + + /** + * 특정 문항에 속한 모든 선택지를 조회합니다. (HTTP GET) + * + * @param questionId 선택지 목록을 조회할 문항 ID + * @return 해당 문항의 모든 선택지 정보를 담은 응답 리스트 (HTTP 200 OK) + */ + @GetMapping("/by-question/{questionId}") + public ResponseEntity> getOptionsByQuestionId(@PathVariable UUID questionId) { + List options = questionOptionService.getOptionsByQuestionId(questionId).stream() + .map(QuestionOptionResponse::from) + .collect(Collectors.toList()); + return ResponseEntity.ok(options); + } + + /** + * 특정 ID의 선택지를 조회합니다. (HTTP GET) + * + * @param optionId 조회할 선택지의 고유 ID + * @return 조회된 선택지 정보를 담은 응답 (HTTP 200 OK) + * 선택지가 없을 경우 HTTP 404 Not Found 반환 + */ + @GetMapping("/{optionId}") + public ResponseEntity getQuestionOptionById(@PathVariable UUID optionId) { + try { + QuestionOptionResponse option = QuestionOptionResponse.from(questionOptionService.getQuestionOptionById(optionId)); + return ResponseEntity.ok(option); + } catch (NoSuchElementException e) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + + /** + * 선택지 정보를 업데이트합니다. (HTTP PUT) + * + * @param optionId 업데이트할 선택지의 고유 ID + * @param request QuestionOption 업데이트 요청 DTO + * @return 업데이트된 선택지 정보를 담은 응답 (HTTP 200 OK) + * 선택지가 없을 경우 HTTP 404 Not Found 반환 + */ + @PutMapping("/{optionId}") + public ResponseEntity updateQuestionOption(@PathVariable UUID optionId, @RequestBody QuestionOptionUpdateRequest request) { + try { + QuestionOptionResponse updatedOption = QuestionOptionResponse.from( + questionOptionService.updateQuestionOption( + optionId, + request.getOptionText(), + request.getOptionOrder() + ) + ); + return ResponseEntity.ok(updatedOption); + } catch (NoSuchElementException e) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + + /** + * 특정 ID의 선택지를 삭제합니다. (HTTP DELETE) + * + * @param optionId 삭제할 선택지의 고유 ID + * @return 응답 본문 없이 HTTP 204 No Content 반환 + * 선택지가 없을 경우 HTTP 404 Not Found 반환 + */ + @DeleteMapping("/{optionId}") + public ResponseEntity deleteQuestionOption(@PathVariable UUID optionId) { + try { + questionOptionService.deleteQuestionOption(optionId); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } catch (NoSuchElementException e) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/controller/SurveyController.java b/src/main/java/sgis/surveysystem/controller/SurveyController.java new file mode 100644 index 0000000..b8e13d9 --- /dev/null +++ b/src/main/java/sgis/surveysystem/controller/SurveyController.java @@ -0,0 +1,154 @@ +package sgis.surveysystem.controller; + +import sgis.surveysystem.dto.SurveyCreateRequest; +import sgis.surveysystem.dto.SurveyResponse; +import sgis.surveysystem.dto.SurveyUpdateRequest; +import sgis.surveysystem.service.SurveyService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * 설문(Survey) 관련 HTTP 요청을 처리하는 REST 컨트롤러. + * 설문 생성, 조회, 수정, 삭제 등의 API 엔드포인트를 제공합니다. + * 모든 요청은 .do 확장자로 끝나며, GET과 POST 메서드만 사용합니다. + */ +@RestController // RESTful 웹 서비스 컨트롤러임을 선언 +@RequestMapping("/api/surveys") // 기본 URL 경로 (이 부분은 .do를 포함하지 않음, 각 메서드에서 .do를 추가) +@RequiredArgsConstructor // Lombok을 사용하여 final 필드를 인자로 받는 생성자 자동 생성 (SurveyService 주입) +public class SurveyController { + + private final SurveyService surveyService; + + /** + * 새로운 설문을 생성합니다. (HTTP POST) + * 요청 본문에서 설문 정보를 받아 서비스를 통해 저장합니다. + * + * @param request Survey 생성 요청 DTO + * @return 생성된 설문 정보를 담은 응답 (HTTP 201 Created) + */ + @PostMapping("/createSurvey.do") // POST 요청 처리, .do 확장자 추가 + public ResponseEntity createSurvey(@RequestBody SurveyCreateRequest request) { + // 서비스 계층의 createSurvey 메서드를 호출하여 설문을 생성합니다. + // DTO의 정보를 서비스 메서드의 파라미터로 전달합니다. + SurveyResponse createdSurvey = SurveyResponse.from( + surveyService.createSurvey( + request.getSurveyTitle(), + request.getSurveyDescription(), // 수정: surveydescription() -> getSurveyDescription() + request.getStartDate(), + request.getEndDate() + ) + ); + // 생성 성공 시 201 Created 상태 코드와 함께 생성된 설문 정보를 반환합니다. + return new ResponseEntity<>(createdSurvey, HttpStatus.CREATED); + } + + /** + * 모든 설문 목록을 조회합니다. (HTTP GET) + * + * @return 모든 설문 정보를 담은 응답 리스트 (HTTP 200 OK) + */ + @GetMapping("/getAllSurveys.do") // GET 요청 처리, .do 확장자 추가 + public ResponseEntity> getAllSurveys() { + // 서비스 계층에서 모든 설문을 조회하고, 각 설문 Entity를 SurveyResponse DTO로 변환합니다. + List surveys = surveyService.getAllSurveys().stream() + .map(SurveyResponse::from) + .collect(Collectors.toList()); + // 조회 성공 시 200 OK 상태 코드와 함께 설문 목록을 반환합니다. + return ResponseEntity.ok(surveys); + } + + /** + * 특정 ID의 설문을 조회합니다. (HTTP GET) + * URL 경로 변수(PathVariable)를 통해 설문 ID를 받습니다. + * + * @param surveyId 조회할 설문의 고유 ID + * @return 조회된 설문 정보를 담은 응답 (HTTP 200 OK) + * 설문이 없을 경우 HTTP 404 Not Found 반환 + */ + @GetMapping("/{surveyId}/getSurvey.do") // {surveyId}는 URL 경로의 변수, .do 확장자 추가 + public ResponseEntity getSurveyById(@PathVariable UUID surveyId) { + try { + // 서비스 계층에서 ID로 설문을 조회하고, Entity를 SurveyResponse DTO로 변환합니다. + SurveyResponse survey = SurveyResponse.from(surveyService.getSurveyById(surveyId)); + return ResponseEntity.ok(survey); + } catch (NoSuchElementException e) { + // 해당 ID의 설문을 찾을 수 없을 경우 404 Not Found 상태 코드를 반환합니다. + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + + /** + * 설문 정보를 업데이트합니다. (HTTP POST) + * PUT 요청 대신 POST를 사용하며, URL 경로에 update 액션을 명시합니다. + * + * @param surveyId 업데이트할 설문의 고유 ID + * @param request Survey 업데이트 요청 DTO + * @return 업데이트된 설문 정보를 담은 응답 (HTTP 200 OK) + * 설문이 없을 경우 HTTP 404 Not Found 반환 + */ + @PostMapping("/{surveyId}/updateSurvey.do") // POST 요청으로 업데이트 처리, .do 확장자 추가 + public ResponseEntity updateSurvey(@PathVariable UUID surveyId, @RequestBody SurveyUpdateRequest request) { + try { + // 서비스 계층의 updateSurvey 메서드를 호출하여 설문을 업데이트합니다. + SurveyResponse updatedSurvey = SurveyResponse.from( + surveyService.updateSurvey( + surveyId, + request.getSurveyTitle(), + request.getSurveyDescription(), // 수정: surveydescription() -> getSurveyDescription() + request.getIsActive(), + request.getStartDate(), + request.getEndDate() + ) + ); + return ResponseEntity.ok(updatedSurvey); + } catch (NoSuchElementException e) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + + /** + * 특정 ID의 설문을 삭제합니다. (HTTP POST) + * DELETE 요청 대신 POST를 사용하며, URL 경로에 delete 액션을 명시합니다. + * + * @param surveyId 삭제할 설문의 고유 ID + * @return 응답 본문 없이 HTTP 204 No Content 반환 + * 설문이 없을 경우 HTTP 404 Not Found 반환 + */ + @PostMapping("/{surveyId}/deleteSurvey.do") // POST 요청으로 삭제 처리, .do 확장자 추가 + public ResponseEntity deleteSurvey(@PathVariable UUID surveyId) { + try { + // 서비스 계층의 deleteSurvey 메서드를 호출하여 설문을 삭제합니다. + surveyService.deleteSurvey(surveyId); + // 삭제 성공 시 204 No Content 상태 코드를 반환합니다. (응답 본문 없음) + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } catch (NoSuchElementException e) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + + /** + * 특정 ID의 설문을 비활성화합니다. (HTTP POST) + * PATCH 요청 대신 POST를 사용하며, URL 경로에 deactivate 액션을 명시합니다. + * + * @param surveyId 비활성화할 설문의 고유 ID + * @return 비활성화된 설문 정보를 담은 응답 (HTTP 200 OK) + * 설문이 없을 경우 HTTP 404 Not Found 반환 + */ + @PostMapping("/{surveyId}/deactivateSurvey.do") // POST 요청으로 비활성화 처리, .do 확장자 추가 + public ResponseEntity deactivateSurvey(@PathVariable UUID surveyId) { + try { + // 서비스 계층의 deactivateSurvey 메서드를 호출하여 설문을 비활성화합니다. + SurveyResponse deactivatedSurvey = SurveyResponse.from(surveyService.deactivateSurvey(surveyId)); + return ResponseEntity.ok(deactivatedSurvey); + } catch (NoSuchElementException e) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } +} diff --git a/src/main/java/sgis/surveysystem/domain/Answer.java b/src/main/java/sgis/surveysystem/domain/Answer.java new file mode 100644 index 0000000..3446145 --- /dev/null +++ b/src/main/java/sgis/surveysystem/domain/Answer.java @@ -0,0 +1,29 @@ +package sgis.surveysystem.domain; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.UUID; + +@Getter +@Setter +@NoArgsConstructor +@Builder +public class Answer { + private UUID answerId; + private UUID responseId; // FK: tb_survey_responses.response_id + private UUID questionId; // FK: tb_questions.question_id + private String answerText; + private UUID selectedOptionId; // FK: tb_question_options.option_id (단일 선택용) + + @Builder + public Answer(UUID answerId, UUID responseId, UUID questionId, String answerText, UUID selectedOptionId) { + this.answerId = answerId; + this.responseId = responseId; + this.questionId = questionId; + this.answerText = answerText; + this.selectedOptionId = selectedOptionId; + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/domain/AnswerSelectedOption.java b/src/main/java/sgis/surveysystem/domain/AnswerSelectedOption.java new file mode 100644 index 0000000..31c65ff --- /dev/null +++ b/src/main/java/sgis/surveysystem/domain/AnswerSelectedOption.java @@ -0,0 +1,22 @@ +package sgis.surveysystem.domain; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.UUID; + +@Getter +@Setter +@NoArgsConstructor +public class AnswerSelectedOption { + private UUID answerId; // FK: tb_answers.answer_id (복합 PK의 일부) + private UUID optionId; // FK: tb_question_options.option_id (복합 PK의 일부) + + @Builder + public AnswerSelectedOption(UUID answerId, UUID optionId) { + this.answerId = answerId; + this.optionId = optionId; + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/domain/Question.java b/src/main/java/sgis/surveysystem/domain/Question.java new file mode 100644 index 0000000..fe980a2 --- /dev/null +++ b/src/main/java/sgis/surveysystem/domain/Question.java @@ -0,0 +1,30 @@ +package sgis.surveysystem.domain; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.UUID; + +@Getter +@Setter +@NoArgsConstructor +public class Question { + private UUID questionId; + private UUID surveyId; // FK: tb_surveys.survey_id + private String questionText; + private String questionType; + private Integer questionOrder; + private Boolean isRequired; + + @Builder + public Question(UUID questionId, UUID surveyId, String questionText, String questionType, Integer questionOrder, Boolean isRequired) { + this.questionId = questionId; + this.surveyId = surveyId; + this.questionText = questionText; + this.questionType = questionType; + this.questionOrder = questionOrder; + this.isRequired = isRequired; + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/domain/QuestionOption.java b/src/main/java/sgis/surveysystem/domain/QuestionOption.java new file mode 100644 index 0000000..08087f7 --- /dev/null +++ b/src/main/java/sgis/surveysystem/domain/QuestionOption.java @@ -0,0 +1,26 @@ +package sgis.surveysystem.domain; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.UUID; + +@Getter +@Setter +@NoArgsConstructor +public class QuestionOption { + private UUID optionId; + private UUID questionId; // FK: tb_questions.question_id + private String optionText; + private Integer optionOrder; + + @Builder + public QuestionOption(UUID optionId, UUID questionId, String optionText, Integer optionOrder) { + this.optionId = optionId; + this.questionId = questionId; + this.optionText = optionText; + this.optionOrder = optionOrder; + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/domain/Respondent.java b/src/main/java/sgis/surveysystem/domain/Respondent.java new file mode 100644 index 0000000..44aa4d7 --- /dev/null +++ b/src/main/java/sgis/surveysystem/domain/Respondent.java @@ -0,0 +1,29 @@ +package sgis.surveysystem.domain; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Getter +@Setter +@NoArgsConstructor +public class Respondent { + private UUID respondentId; + private String userId; + private String ipAddress; // INET 타입은 String으로 매핑 (MyBatis에서 별도 타입 핸들러 필요할 수 있음) + private LocalDateTime responseStartAt; + private LocalDateTime responseEndAt; + + @Builder + public Respondent(UUID respondentId, String userId, String ipAddress, LocalDateTime responseStartAt, LocalDateTime responseEndAt) { + this.respondentId = respondentId; + this.userId = userId; + this.ipAddress = ipAddress; + this.responseStartAt = responseStartAt; + this.responseEndAt = responseEndAt; + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/domain/Survey.java b/src/main/java/sgis/surveysystem/domain/Survey.java new file mode 100644 index 0000000..7abc4b6 --- /dev/null +++ b/src/main/java/sgis/surveysystem/domain/Survey.java @@ -0,0 +1,36 @@ +package sgis.surveysystem.domain; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; // MyBatis는 매핑 시 Setter를 사용하기도 함 + +import java.time.LocalDateTime; +import java.util.UUID; + +@Getter +@Setter // MyBatis 매핑을 위해 필요할 수 있음 +@NoArgsConstructor // 기본 생성자 +@Builder +public class Survey { + private UUID surveyId; + private String surveyTitle; + private String surveyDescription; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private Boolean isActive; + private LocalDateTime startDate; + private LocalDateTime endDate; + + @Builder // 빌더 패턴을 위한 어노테이션 + public Survey(UUID surveyId, String surveyTitle, String surveyDescription, LocalDateTime createdAt, LocalDateTime updatedAt, Boolean isActive, LocalDateTime startDate, LocalDateTime endDate) { + this.surveyId = surveyId; + this.surveyTitle = surveyTitle; + this.surveyDescription = surveyDescription; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + this.isActive = isActive; + this.startDate = startDate; + this.endDate = endDate; + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/domain/SurveyResponse.java b/src/main/java/sgis/surveysystem/domain/SurveyResponse.java new file mode 100644 index 0000000..8a2fd79 --- /dev/null +++ b/src/main/java/sgis/surveysystem/domain/SurveyResponse.java @@ -0,0 +1,27 @@ +package sgis.surveysystem.domain; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Getter +@Setter +@NoArgsConstructor +public class SurveyResponse { + private UUID responseId; + private UUID surveyId; // FK: tb_surveys.survey_id + private UUID respondentId; // FK: tb_respondents.respondent_id + private LocalDateTime submittedAt; + + @Builder + public SurveyResponse(UUID responseId, UUID surveyId, UUID respondentId, LocalDateTime submittedAt) { + this.responseId = responseId; + this.surveyId = surveyId; + this.respondentId = respondentId; + this.submittedAt = submittedAt; + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/dto/AnswerCreateRequest.java b/src/main/java/sgis/surveysystem/dto/AnswerCreateRequest.java new file mode 100644 index 0000000..9a43f5b --- /dev/null +++ b/src/main/java/sgis/surveysystem/dto/AnswerCreateRequest.java @@ -0,0 +1,26 @@ +package sgis.surveysystem.dto; + +import sgis.surveysystem.domain.Answer; +import sgis.surveysystem.domain.AnswerSelectedOption; // 다중 선택 옵션 DTO 변환을 위해 필요 +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +// 답변 생성 요청 DTO (단일/주관식/다중 선택 모두 커버) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class AnswerCreateRequest { + private UUID responseId; + private UUID questionId; + private String answerText; // 단답형, 주관식용 + private UUID selectedOptionId; // 객관식 단일 선택용 + private List selectedOptionIds; // 객관식 다중 선택용 +} diff --git a/src/main/java/sgis/surveysystem/dto/AnswerResponse.java b/src/main/java/sgis/surveysystem/dto/AnswerResponse.java new file mode 100644 index 0000000..09f2811 --- /dev/null +++ b/src/main/java/sgis/surveysystem/dto/AnswerResponse.java @@ -0,0 +1,40 @@ +package sgis.surveysystem.dto; + +import java.util.List; +import java.util.UUID; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import sgis.surveysystem.domain.Answer; + +//답변 응답 DTO (다중 선택 옵션 포함) +@Getter +@Setter // AnswerSelectedOptionResponse 리스트를 설정하기 위해 Setter 필요 +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AnswerResponse { + private UUID answerId; + private UUID responseId; + private UUID questionId; + private String answerText; + private UUID selectedOptionId; + private List selectedOptions; // 다중 선택된 옵션 목록 + + public static AnswerResponse from(Answer answer) { + if (answer == null) { + return null; + } + return AnswerResponse.builder() + .answerId(answer.getAnswerId()) + .responseId(answer.getResponseId()) + .questionId(answer.getQuestionId()) + .answerText(answer.getAnswerText()) + .selectedOptionId(answer.getSelectedOptionId()) + // selectedOptions는 서비스/컨트롤러에서 AnswerSelectedOptionService를 통해 채웁니다. + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/dto/AnswerSelectedOptionCreateRequest.java b/src/main/java/sgis/surveysystem/dto/AnswerSelectedOptionCreateRequest.java new file mode 100644 index 0000000..5dbe199 --- /dev/null +++ b/src/main/java/sgis/surveysystem/dto/AnswerSelectedOptionCreateRequest.java @@ -0,0 +1,18 @@ +package sgis.surveysystem.dto; + +import java.util.UUID; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +//다중 선택 옵션 추가 요청 DTO +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class AnswerSelectedOptionCreateRequest { + private UUID answerId; // 어떤 Answer에 속하는지 + private UUID optionId; // 선택된 옵션 ID +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/dto/AnswerSelectedOptionResponse.java b/src/main/java/sgis/surveysystem/dto/AnswerSelectedOptionResponse.java new file mode 100644 index 0000000..2ca5ca1 --- /dev/null +++ b/src/main/java/sgis/surveysystem/dto/AnswerSelectedOptionResponse.java @@ -0,0 +1,32 @@ +package sgis.surveysystem.dto; + +import sgis.surveysystem.domain.AnswerSelectedOption; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.util.UUID; + + + +// 다중 선택 옵션 응답 DTO +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AnswerSelectedOptionResponse { + private UUID answerId; + private UUID optionId; + + public static AnswerSelectedOptionResponse from(AnswerSelectedOption answerSelectedOption) { + if (answerSelectedOption == null) { + return null; + } + return AnswerSelectedOptionResponse.builder() + .answerId(answerSelectedOption.getAnswerId()) + .optionId(answerSelectedOption.getOptionId()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/dto/AnswerUpdateRequest.java b/src/main/java/sgis/surveysystem/dto/AnswerUpdateRequest.java new file mode 100644 index 0000000..a9cd259 --- /dev/null +++ b/src/main/java/sgis/surveysystem/dto/AnswerUpdateRequest.java @@ -0,0 +1,19 @@ +package sgis.surveysystem.dto; + +import java.util.UUID; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +//답변 업데이트 요청 DTO (주로 단답/주관식/단일 선택용) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class AnswerUpdateRequest { + private String answerText; + private UUID selectedOptionId; + // 다중 선택 옵션 업데이트는 AnswerSelectedOption 관련 별도 요청 사용 권장 +} diff --git a/src/main/java/sgis/surveysystem/dto/QuestionCreateRequest.java b/src/main/java/sgis/surveysystem/dto/QuestionCreateRequest.java new file mode 100644 index 0000000..e3b95f1 --- /dev/null +++ b/src/main/java/sgis/surveysystem/dto/QuestionCreateRequest.java @@ -0,0 +1,28 @@ +package sgis.surveysystem.dto; + +import sgis.surveysystem.domain.Question; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +// 문항 생성 요청 DTO +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class QuestionCreateRequest { + private UUID surveyId; // 어떤 설문에 속하는지 + private String questionText; + private String questionType; + private Integer questionOrder; + private Boolean isRequired; +} + + + diff --git a/src/main/java/sgis/surveysystem/dto/QuestionOptionCreateRequest.java b/src/main/java/sgis/surveysystem/dto/QuestionOptionCreateRequest.java new file mode 100644 index 0000000..21d6c95 --- /dev/null +++ b/src/main/java/sgis/surveysystem/dto/QuestionOptionCreateRequest.java @@ -0,0 +1,21 @@ +package sgis.surveysystem.dto; + +import sgis.surveysystem.domain.QuestionOption; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.util.UUID; + +// 선택지 생성 요청 DTO +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class QuestionOptionCreateRequest { + private UUID questionId; // 어떤 문항에 속하는지 + private String optionText; + private Integer optionOrder; +} diff --git a/src/main/java/sgis/surveysystem/dto/QuestionOptionResponse.java b/src/main/java/sgis/surveysystem/dto/QuestionOptionResponse.java new file mode 100644 index 0000000..63134dc --- /dev/null +++ b/src/main/java/sgis/surveysystem/dto/QuestionOptionResponse.java @@ -0,0 +1,35 @@ +package sgis.surveysystem.dto; + +import sgis.surveysystem.domain.QuestionOption; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.util.UUID; + + +// 선택지 응답 DTO +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class QuestionOptionResponse { + private UUID optionId; + private UUID questionId; // 어떤 문항에 속하는지 + private String optionText; + private Integer optionOrder; + + public static QuestionOptionResponse from(QuestionOption option) { + if (option == null) { + return null; + } + return QuestionOptionResponse.builder() + .optionId(option.getOptionId()) + .questionId(option.getQuestionId()) + .optionText(option.getOptionText()) + .optionOrder(option.getOptionOrder()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/dto/QuestionOptionUpdateRequest.java b/src/main/java/sgis/surveysystem/dto/QuestionOptionUpdateRequest.java new file mode 100644 index 0000000..af41498 --- /dev/null +++ b/src/main/java/sgis/surveysystem/dto/QuestionOptionUpdateRequest.java @@ -0,0 +1,21 @@ +package sgis.surveysystem.dto; + +import sgis.surveysystem.domain.QuestionOption; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.util.UUID; + + +// 선택지 업데이트 요청 DTO +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class QuestionOptionUpdateRequest { + private String optionText; + private Integer optionOrder; +} diff --git a/src/main/java/sgis/surveysystem/dto/QuestionResponse.java b/src/main/java/sgis/surveysystem/dto/QuestionResponse.java new file mode 100644 index 0000000..02af97e --- /dev/null +++ b/src/main/java/sgis/surveysystem/dto/QuestionResponse.java @@ -0,0 +1,42 @@ +package sgis.surveysystem.dto; + +import java.util.List; +import java.util.UUID; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import sgis.surveysystem.domain.Question; + +//문항 응답 DTO (선택지 포함 가능) +@Getter +@Setter // Setter가 필요한 경우 (예: Controller에서 옵션 리스트를 동적으로 설정) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class QuestionResponse { + private UUID questionId; + private UUID surveyId; // 어떤 설문에 속하는지 + private String questionText; + private String questionType; + private Integer questionOrder; + private Boolean isRequired; + private List options; // 객관식 문항의 선택지 리스트 (서비스/컨트롤러에서 채움) + + public static QuestionResponse from(Question question) { + if (question == null) { + return null; + } + return QuestionResponse.builder() + .questionId(question.getQuestionId()) + .surveyId(question.getSurveyId()) + .questionText(question.getQuestionText()) + .questionType(question.getQuestionType()) + .questionOrder(question.getQuestionOrder()) + .isRequired(question.getIsRequired()) + // options 필드는 기본적으로 null로 두고, Controller/Service에서 필요한 경우 채웁니다. + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/dto/QuestionUpdateRequest.java b/src/main/java/sgis/surveysystem/dto/QuestionUpdateRequest.java new file mode 100644 index 0000000..cf9f67a --- /dev/null +++ b/src/main/java/sgis/surveysystem/dto/QuestionUpdateRequest.java @@ -0,0 +1,18 @@ +package sgis.surveysystem.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +//문항 업데이트 요청 DTO +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class QuestionUpdateRequest { + private String questionText; + private String questionType; + private Integer questionOrder; + private Boolean isRequired; +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/dto/RespondentCreateRequest.java b/src/main/java/sgis/surveysystem/dto/RespondentCreateRequest.java new file mode 100644 index 0000000..13e4f43 --- /dev/null +++ b/src/main/java/sgis/surveysystem/dto/RespondentCreateRequest.java @@ -0,0 +1,17 @@ +package sgis.surveysystem.dto; + +import lombok.Getter; +import lombok.Setter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + + +// 응답자 생성 요청 DTO (회원용/익명용) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class RespondentCreateRequest { + private String userId; // 시스템 내 사용자 ID (회원인 경우) + private String ipAddress; // 응답자의 IP 주소 +} diff --git a/src/main/java/sgis/surveysystem/dto/RespondentResponse.java b/src/main/java/sgis/surveysystem/dto/RespondentResponse.java new file mode 100644 index 0000000..b664df1 --- /dev/null +++ b/src/main/java/sgis/surveysystem/dto/RespondentResponse.java @@ -0,0 +1,38 @@ +package sgis.surveysystem.dto; + +import sgis.surveysystem.domain.Respondent; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.time.LocalDateTime; +import java.util.UUID; + + +// 응답자 응답 DTO +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RespondentResponse { + private UUID respondentId; + private String userId; + private String ipAddress; + private LocalDateTime responseStartAt; + private LocalDateTime responseEndAt; + + public static RespondentResponse from(Respondent respondent) { + if (respondent == null) { + return null; + } + return RespondentResponse.builder() + .respondentId(respondent.getRespondentId()) + .userId(respondent.getUserId()) + .ipAddress(respondent.getIpAddress()) + .responseStartAt(respondent.getResponseStartAt()) + .responseEndAt(respondent.getResponseEndAt()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/dto/RespondentUpdateRequest.java b/src/main/java/sgis/surveysystem/dto/RespondentUpdateRequest.java new file mode 100644 index 0000000..d746218 --- /dev/null +++ b/src/main/java/sgis/surveysystem/dto/RespondentUpdateRequest.java @@ -0,0 +1,23 @@ +package sgis.surveysystem.dto; + +import sgis.surveysystem.domain.Respondent; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.time.LocalDateTime; +import java.util.UUID; + + +// 응답자 업데이트 요청 DTO (주로 응답 완료 시간 업데이트 등에 사용) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class RespondentUpdateRequest { + private String userId; + private String ipAddress; + private LocalDateTime responseEndAt; +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/dto/SurveyCreateRequest.java b/src/main/java/sgis/surveysystem/dto/SurveyCreateRequest.java new file mode 100644 index 0000000..ab7ca9b --- /dev/null +++ b/src/main/java/sgis/surveysystem/dto/SurveyCreateRequest.java @@ -0,0 +1,23 @@ +package sgis.surveysystem.dto; + +import sgis.surveysystem.domain.Survey; // 도메인 객체를 DTO로 변환하기 위해 필요 +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; // 모든 필드를 인자로 받는 생성자 추가 (선택 사항이지만 유용) + +import java.time.LocalDateTime; +import java.util.UUID; + +// 설문 생성 요청 DTO: 클라이언트에서 설문 생성 시 보내는 데이터 +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor // 요청 DTO는 모든 필드 생성자도 유용 +public class SurveyCreateRequest { + private String surveyTitle; + private String surveyDescription; + private LocalDateTime startDate; + private LocalDateTime endDate; +} diff --git a/src/main/java/sgis/surveysystem/dto/SurveyResponse.java b/src/main/java/sgis/surveysystem/dto/SurveyResponse.java new file mode 100644 index 0000000..8652e35 --- /dev/null +++ b/src/main/java/sgis/surveysystem/dto/SurveyResponse.java @@ -0,0 +1,46 @@ +package sgis.surveysystem.dto; + +import sgis.surveysystem.domain.Survey; // Import Survey domain object +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.time.LocalDateTime; +import java.util.UUID; + + +// Survey 응답 DTO: 서버에서 클라이언트로 설문 정보를 보낼 때 사용하는 데이터 +@Getter +@Setter // DTO가 데이터를 받을 때 Setter가 필요할 수 있음 +@NoArgsConstructor +@AllArgsConstructor // Lombok이 모든 필드를 인자로 받는 생성자를 생성 +@Builder // <--- 클래스 레벨에 @Builder 어노테이션 +public class SurveyResponse { + private UUID surveyId; + private String surveyTitle; + private String surveyDescription; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private Boolean isActive; + private LocalDateTime startDate; + private LocalDateTime endDate; + + // Entity (도메인 객체)를 DTO로 변환하는 정적 팩토리 메서드 + public static SurveyResponse from(Survey survey) { + if (survey == null) { + return null; + } + return SurveyResponse.builder() // <--- 이 부분이 이제 정상적으로 호출될 것임 + .surveyId(survey.getSurveyId()) + .surveyTitle(survey.getSurveyTitle()) + .surveyDescription(survey.getSurveyDescription()) + .createdAt(survey.getCreatedAt()) + .updatedAt(survey.getUpdatedAt()) + .isActive(survey.getIsActive()) + .startDate(survey.getStartDate()) + .endDate(survey.getEndDate()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/dto/SurveyResponseCreateRequest.java b/src/main/java/sgis/surveysystem/dto/SurveyResponseCreateRequest.java new file mode 100644 index 0000000..712a506 --- /dev/null +++ b/src/main/java/sgis/surveysystem/dto/SurveyResponseCreateRequest.java @@ -0,0 +1,22 @@ +package sgis.surveysystem.dto; + +import sgis.surveysystem.domain.SurveyResponse; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.time.LocalDateTime; +import java.util.UUID; + +// 설문 응답 세션 생성 요청 DTO +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class SurveyResponseCreateRequest { + private UUID surveyId; + private UUID respondentId; // 익명 설문인 경우 null +} + diff --git a/src/main/java/sgis/surveysystem/dto/SurveyResponseResponse.java b/src/main/java/sgis/surveysystem/dto/SurveyResponseResponse.java new file mode 100644 index 0000000..e0a7edb --- /dev/null +++ b/src/main/java/sgis/surveysystem/dto/SurveyResponseResponse.java @@ -0,0 +1,34 @@ +package sgis.surveysystem.dto; + +import java.time.LocalDateTime; +import java.util.UUID; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import sgis.surveysystem.domain.SurveyResponse; + +//설문 응답 세션 응답 DTO +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SurveyResponseResponse { + private UUID responseId; + private UUID surveyId; + private UUID respondentId; + private LocalDateTime submittedAt; + + public static SurveyResponseResponse from(SurveyResponse surveyResponse) { + if (surveyResponse == null) { + return null; + } + return SurveyResponseResponse.builder() + .responseId(surveyResponse.getResponseId()) + .surveyId(surveyResponse.getSurveyId()) + .respondentId(surveyResponse.getRespondentId()) + .submittedAt(surveyResponse.getSubmittedAt()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/dto/SurveyUpdateRequest.java b/src/main/java/sgis/surveysystem/dto/SurveyUpdateRequest.java new file mode 100644 index 0000000..874fd42 --- /dev/null +++ b/src/main/java/sgis/surveysystem/dto/SurveyUpdateRequest.java @@ -0,0 +1,26 @@ +package sgis.surveysystem.dto; + +import sgis.surveysystem.domain.Survey; // 도메인 객체를 DTO로 변환하기 위해 필요 +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; // 모든 필드를 인자로 받는 생성자 추가 (선택 사항이지만 유용) + +import java.time.LocalDateTime; +import java.util.UUID; + + + +// 설문 업데이트 요청 DTO: 클라이언트에서 설문 업데이트 시 보내는 데이터 +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor // 요청 DTO는 모든 필드 생성자도 유용 +public class SurveyUpdateRequest { + private String surveyTitle; + private String surveyDescription; + private Boolean isActive; + private LocalDateTime startDate; + private LocalDateTime endDate; +} diff --git a/src/main/java/sgis/surveysystem/mapper/AnswerMapper.java b/src/main/java/sgis/surveysystem/mapper/AnswerMapper.java new file mode 100644 index 0000000..cc504c4 --- /dev/null +++ b/src/main/java/sgis/surveysystem/mapper/AnswerMapper.java @@ -0,0 +1,17 @@ +package sgis.surveysystem.mapper; + +import sgis.surveysystem.domain.Answer; +import org.apache.ibatis.annotations.Mapper; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Mapper +public interface AnswerMapper { + void insertAnswer(Answer answer); + Optional findAnswerById(UUID answerId); + List findAnswersByResponseId(UUID responseId); + List findAnswersByQuestionId(UUID questionId); + void updateAnswer(Answer answer); + void deleteAnswer(UUID answerId); +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/mapper/AnswerSelectedOptionMapper.java b/src/main/java/sgis/surveysystem/mapper/AnswerSelectedOptionMapper.java new file mode 100644 index 0000000..8d0a67d --- /dev/null +++ b/src/main/java/sgis/surveysystem/mapper/AnswerSelectedOptionMapper.java @@ -0,0 +1,16 @@ +package sgis.surveysystem.mapper; + +import sgis.surveysystem.domain.AnswerSelectedOption; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; +import java.util.UUID; + +@Mapper +public interface AnswerSelectedOptionMapper { + void insertAnswerSelectedOption(AnswerSelectedOption answerSelectedOption); + void insertMultipleAnswerSelectedOptions(List options); // 다중 선택 옵션 일괄 삽입 + List findOptionsByAnswerId(UUID answerId); + void deleteByAnswerId(UUID answerId); // 특정 답변에 연결된 모든 선택 옵션 삭제 + void deleteAnswerSelectedOption(@Param("answerId") UUID answerId, @Param("optionId") UUID optionId); // 특정 선택 옵션 삭제 +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/mapper/QuestionMapper.java b/src/main/java/sgis/surveysystem/mapper/QuestionMapper.java new file mode 100644 index 0000000..3d32e6a --- /dev/null +++ b/src/main/java/sgis/surveysystem/mapper/QuestionMapper.java @@ -0,0 +1,17 @@ +package sgis.surveysystem.mapper; + +import sgis.surveysystem.domain.Question; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Mapper +public interface QuestionMapper { + void insertQuestion(Question question); + List findQuestionsBySurveyId(UUID surveyId); + Optional findQuestionById(UUID questionId); + void updateQuestion(Question question); + void deleteQuestion(UUID questionId); +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/mapper/QuestionOptionMapper.java b/src/main/java/sgis/surveysystem/mapper/QuestionOptionMapper.java new file mode 100644 index 0000000..8f418d2 --- /dev/null +++ b/src/main/java/sgis/surveysystem/mapper/QuestionOptionMapper.java @@ -0,0 +1,17 @@ +package sgis.surveysystem.mapper; + +import sgis.surveysystem.domain.QuestionOption; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Mapper +public interface QuestionOptionMapper { + void insertQuestionOption(QuestionOption option); + List findOptionsByQuestionId(UUID questionId); + Optional findQuestionOptionById(UUID optionId); + void updateQuestionOption(QuestionOption option); + void deleteQuestionOption(UUID optionId); +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/mapper/RespondentMapper.java b/src/main/java/sgis/surveysystem/mapper/RespondentMapper.java new file mode 100644 index 0000000..8a360e9 --- /dev/null +++ b/src/main/java/sgis/surveysystem/mapper/RespondentMapper.java @@ -0,0 +1,16 @@ +package sgis.surveysystem.mapper; + +import sgis.surveysystem.domain.Respondent; +import org.apache.ibatis.annotations.Mapper; +import java.util.Optional; +import java.util.UUID; +import java.util.List; + +@Mapper +public interface RespondentMapper { + void insertRespondent(Respondent respondent); + Optional findRespondentById(UUID respondentId); + Optional findRespondentByUserId(String userId); // user_id로 응답자 찾기 + void updateRespondent(Respondent respondent); + void deleteRespondent(UUID respondentId); +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/mapper/SurveyMapper.java b/src/main/java/sgis/surveysystem/mapper/SurveyMapper.java new file mode 100644 index 0000000..5a530ed --- /dev/null +++ b/src/main/java/sgis/surveysystem/mapper/SurveyMapper.java @@ -0,0 +1,19 @@ +package sgis.surveysystem.mapper; + +import sgis.surveysystem.domain.Survey; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Mapper +public interface SurveyMapper { + void insertSurvey(Survey survey); + List findAllSurveys(); + Optional findSurveyById(UUID surveyId); + void updateSurvey(Survey survey); + void deleteSurvey(UUID surveyId); + void updateSurveyStatus(@Param("surveyId") UUID surveyId, @Param("isActive") Boolean isActive); +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/mapper/SurveyResponseMapper.java b/src/main/java/sgis/surveysystem/mapper/SurveyResponseMapper.java new file mode 100644 index 0000000..e6cdaeb --- /dev/null +++ b/src/main/java/sgis/surveysystem/mapper/SurveyResponseMapper.java @@ -0,0 +1,16 @@ +package sgis.surveysystem.mapper; + +import sgis.surveysystem.domain.SurveyResponse; +import org.apache.ibatis.annotations.Mapper; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Mapper +public interface SurveyResponseMapper { + void insertSurveyResponse(SurveyResponse surveyResponse); + Optional findSurveyResponseById(UUID responseId); + List findResponsesBySurveyId(UUID surveyId); + List findResponsesByRespondentId(UUID respondentId); + void deleteSurveyResponse(UUID responseId); +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/service/AnswerSelectedOptionService.java b/src/main/java/sgis/surveysystem/service/AnswerSelectedOptionService.java new file mode 100644 index 0000000..0639b13 --- /dev/null +++ b/src/main/java/sgis/surveysystem/service/AnswerSelectedOptionService.java @@ -0,0 +1,113 @@ +package sgis.surveysystem.service; + +import sgis.surveysystem.domain.AnswerSelectedOption; +import sgis.surveysystem.mapper.AnswerSelectedOptionMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * 다중 선택 객관식 답변의 선택된 옵션(AnswerSelectedOption) 관련 비즈니스 로직을 처리하는 서비스 클래스. + * Answer 테이블이 아닌 별도의 테이블에 저장되는 다중 선택 옵션을 관리합니다. + */ +@Service +@RequiredArgsConstructor +@Transactional +public class AnswerSelectedOptionService { + + private final AnswerSelectedOptionMapper answerSelectedOptionMapper; + private final AnswerService answerService; // 외래 키 연결 및 답변 유형 확인을 위해 주입 + private final QuestionOptionService questionOptionService; // 선택지 유효성 검사를 위해 주입 + + /** + * 다중 선택 객관식 답변에 단일 선택지를 추가합니다. + * + * @param answerId 답변 고유 ID (해당 답변은 '객관식_다중' 유형이어야 함) + * @param optionId 추가할 선택지 ID + * @return 생성된 AnswerSelectedOption 객체 + * @throws IllegalArgumentException 답변 유형이 '객관식_다중'이 아닐 경우 + * @throws NoSuchElementException 관련 엔티티를 찾을 수 없을 경우 + */ + public AnswerSelectedOption addSelectedOption(UUID answerId, UUID optionId) { + // 답변 존재 여부 확인 및 해당 답변이 '객관식_다중' 유형인지 검증 + // (AnswerService의 getAnswerById를 통해 QuestionService를 거쳐 questionType 확인 필요) + answerService.getAnswerById(answerId); // Answer 존재 확인 + // TODO: 여기에서 answerId에 해당하는 Question의 questionType이 '객관식_다중'인지 확인하는 로직 추가 권장 + + questionOptionService.getQuestionOptionById(optionId); // 선택지 존재 여부 확인 + + AnswerSelectedOption selectedOption = AnswerSelectedOption.builder() + .answerId(answerId) + .optionId(optionId) + .build(); + answerSelectedOptionMapper.insertAnswerSelectedOption(selectedOption); // Mapper를 통해 선택 옵션 삽입 + return selectedOption; + } + + /** + * 다중 선택 객관식 답변에 여러 선택지를 일괄 추가합니다. + * + * @param answerId 답변 고유 ID + * @param optionIds 추가할 선택지 ID 목록 + * @throws IllegalArgumentException 답변 유형이 '객관식_다중'이 아닐 경우 + * @throws NoSuchElementException 관련 엔티티를 찾을 수 없을 경우 + */ + public void addMultipleSelectedOptions(UUID answerId, List optionIds) { + answerService.getAnswerById(answerId); // Answer 존재 확인 + // TODO: 여기에서 answerId에 해당하는 Question의 questionType이 '객관식_다중'인지 확인하는 로직 추가 권장 + + if (optionIds == null || optionIds.isEmpty()) { + return; // 추가할 옵션이 없으면 아무것도 하지 않습니다. + } + + List optionsToInsert = new ArrayList<>(); + for (UUID optionId : optionIds) { + questionOptionService.getQuestionOptionById(optionId); // 각 선택지 유효성 검사 + optionsToInsert.add(AnswerSelectedOption.builder() + .answerId(answerId) + .optionId(optionId) + .build()); + } + answerSelectedOptionMapper.insertMultipleAnswerSelectedOptions(optionsToInsert); // Mapper를 통해 선택 옵션 일괄 삽입 + } + + /** + * 특정 답변에 선택된 모든 옵션을 조회합니다. + * 읽기 전용 트랜잭션으로 설정합니다. + * + * @param answerId 답변 고유 ID + * @return 선택된 AnswerSelectedOption 객체 리스트 + */ + @Transactional(readOnly = true) + public List getSelectedOptionsByAnswerId(UUID answerId) { + answerService.getAnswerById(answerId); // 답변 존재 여부 확인 + return answerSelectedOptionMapper.findOptionsByAnswerId(answerId); // Mapper를 통해 선택된 옵션 목록 조회 + } + + /** + * 다중 선택 객관식 답변에서 특정 선택지를 제거합니다. + * + * @param answerId 답변 고유 ID + * @param optionId 제거할 선택지 ID + */ + public void removeSelectedOption(UUID answerId, UUID optionId) { + // 삭제 전에 해당 연결이 존재하는지 확인하는 로직을 추가할 수 있습니다. + answerSelectedOptionMapper.deleteAnswerSelectedOption(answerId, optionId); // Mapper를 통해 특정 선택 옵션 삭제 + } + + /** + * 특정 답변에 연결된 모든 선택지를 제거합니다. + * + * @param answerId 답변 고유 ID + */ + public void removeAllSelectedOptionsByAnswerId(UUID answerId) { + answerService.getAnswerById(answerId); // 답변 존재 여부 확인 + answerSelectedOptionMapper.deleteByAnswerId(answerId); // Mapper를 통해 답변에 연결된 모든 선택 옵션 삭제 + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/service/AnswerService.java b/src/main/java/sgis/surveysystem/service/AnswerService.java new file mode 100644 index 0000000..81124ca --- /dev/null +++ b/src/main/java/sgis/surveysystem/service/AnswerService.java @@ -0,0 +1,195 @@ +package sgis.surveysystem.service; + +import sgis.surveysystem.domain.Answer; +import sgis.surveysystem.domain.AnswerSelectedOption; +import sgis.surveysystem.mapper.AnswerMapper; +import sgis.surveysystem.mapper.AnswerSelectedOptionMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * 개별 문항 응답(Answer) 관련 비즈니스 로직을 처리하는 서비스 클래스. + * 단답형, 주관식, 객관식 단일/다중 선택 응답을 관리합니다. + */ +@Service +@RequiredArgsConstructor +@Transactional +public class AnswerService { + + private final AnswerMapper answerMapper; + private final SurveyResponseService surveyResponseService; // 외래 키 연결을 위해 주입 + private final QuestionService questionService; // 외래 키 연결 및 문항 유형 확인을 위해 주입 + private final QuestionOptionService questionOptionService; // 선택지 유효성 검사 및 다중 선택 옵션 처리를 위해 주입 + private final AnswerSelectedOptionMapper answerSelectedOptionMapper; // 다중 선택 옵션 매퍼 주입 + + /** + * 새로운 문항 응답을 생성합니다. + * 문항 유형에 따라 answerText 또는 selectedOptionId(s)를 저장합니다. + * + * @param responseId 응답 세션 ID + * @param questionId 문항 ID + * @param answerText 단답형/주관식 응답 내용 (객관식은 null) + * @param selectedOptionId 객관식 단일 선택 시 선택된 옵션 ID (다중 선택/단답/주관식은 null) + * @param selectedOptionIds 객관식 다중 선택 시 선택된 옵션 ID 목록 (다른 유형은 null) + * @return 생성된 Answer 객체 + * @throws IllegalArgumentException 유효하지 않은 응답 유형 또는 데이터가 제공될 경우 + * @throws NoSuchElementException 관련 엔티티(응답 세션, 문항, 선택지)를 찾을 수 없을 경우 + */ + public Answer createAnswer(UUID responseId, UUID questionId, String answerText, UUID selectedOptionId, List selectedOptionIds) { + surveyResponseService.getSurveyResponseById(responseId); // 응답 세션 존재 여부 확인 + // 문항 존재 여부 확인 및 문항 유형 가져오기 + String questionType = questionService.getQuestionById(questionId).getQuestionType(); + + Answer answer = Answer.builder() + .answerId(UUID.randomUUID()) // 애플리케이션에서 새로운 UUID 생성 + .responseId(responseId) + .questionId(questionId) + .build(); + + // 문항 유형에 따라 응답 데이터 유효성 검사 및 설정 + switch (questionType) { + case "단답형": + case "주관식": + if (answerText == null || answerText.trim().isEmpty()) { + throw new IllegalArgumentException("Short answer or essay questions require text answer."); + } + answer.setAnswerText(answerText); + break; + case "객관식_단일": + if (selectedOptionId == null) { + throw new IllegalArgumentException("Single choice question requires one selected option."); + } + questionOptionService.getQuestionOptionById(selectedOptionId); // 선택지 존재 여부 확인 + answer.setSelectedOptionId(selectedOptionId); + break; + case "객관식_다중": + if (selectedOptionIds == null || selectedOptionIds.isEmpty()) { + throw new IllegalArgumentException("Multiple choice question requires at least one selected option."); + } + // Answer 테이블에는 다중 선택 옵션이 직접 저장되지 않으므로, answerText와 selectedOptionId는 null로 유지 + break; + default: + throw new IllegalArgumentException("Invalid question type: " + questionType); + } + + answerMapper.insertAnswer(answer); // Answer 테이블에 기본 응답 정보 삽입 + + // 다중 선택 옵션이 있는 경우 AnswerSelectedOption 테이블에 추가 정보 삽입 + if ("객관식_다중".equals(questionType) && selectedOptionIds != null && !selectedOptionIds.isEmpty()) { + List multiOptions = selectedOptionIds.stream() + .map(optionId -> { + questionOptionService.getQuestionOptionById(optionId); // 각 선택지 존재 여부 확인 + return AnswerSelectedOption.builder() + .answerId(answer.getAnswerId()) + .optionId(optionId) + .build(); + }) + .collect(Collectors.toList()); + answerSelectedOptionMapper.insertMultipleAnswerSelectedOptions(multiOptions); // 일괄 삽입 + } + + return getAnswerById(answer.getAnswerId()); // 최종적으로 저장된 Answer 객체 반환 + } + + /** + * 특정 ID의 답변을 조회합니다. (객관식 다중 선택 옵션이 있다면 함께 로드) + * 읽기 전용 트랜잭션으로 설정합니다. + * + * @param answerId 답변 고유 ID + * @return 조회된 Answer 객체 + * @throws NoSuchElementException 해당 ID의 답변이 없을 경우 + */ + @Transactional(readOnly = true) + public Answer getAnswerById(UUID answerId) { + Answer answer = answerMapper.findAnswerById(answerId) + .orElseThrow(() -> new NoSuchElementException("Answer not found with ID: " + answerId)); + + // 문항 유형이 '객관식_다중'인 경우, AnswerSelectedOption에서 추가 선택지를 로드하여 Answer 객체에 설정합니다. + // (Answer 도메인 객체에 List options 필드를 추가하고 Setter를 통해 설정 가능) + String questionType = questionService.getQuestionById(answer.getQuestionId()).getQuestionType(); + if ("객관식_다중".equals(questionType)) { + List selectedOptions = answerSelectedOptionMapper.findOptionsByAnswerId(answerId); + // 예시: answer.setAnswerSelectedOptions(selectedOptions); // Answer 도메인에 필드 추가 필요 + } + return answer; + } + + /** + * 특정 응답 세션에 대한 모든 답변을 조회합니다. (각 답변의 다중 선택 옵션도 함께 로드) + * 읽기 전용 트랜잭션으로 설정합니다. + * + * @param responseId 응답 세션 고유 ID + * @return 해당 응답 세션의 모든 Answer 객체 리스트 + * @throws NoSuchElementException 해당 응답 세션 ID가 없을 경우 + */ + @Transactional(readOnly = true) + public List getAnswersByResponseId(UUID responseId) { + surveyResponseService.getSurveyResponseById(responseId); // 응답 세션 존재 여부 확인 + List answers = answerMapper.findAnswersByResponseId(responseId); + + // 각 답변에 대해 다중 선택 옵션을 로드합니다. + for (Answer answer : answers) { + String questionType = questionService.getQuestionById(answer.getQuestionId()).getQuestionType(); + if ("객관식_다중".equals(questionType)) { + List selectedOptions = answerSelectedOptionMapper.findOptionsByAnswerId(answer.getAnswerId()); + // 예시: answer.setAnswerSelectedOptions(selectedOptions); // Answer 도메인에 필드 추가 필요 + } + } + return answers; + } + + /** + * 답변을 업데이트합니다. (단답형, 주관식, 객관식 단일 선택만 해당) + * 객관식 다중 선택 문항의 옵션 업데이트는 AnswerSelectedOptionService를 통해 별도로 처리해야 합니다. + * + * @param answerId 업데이트할 답변 고유 ID + * @param answerText 새로운 단답형/주관식 내용 + * @param selectedOptionId 새로운 객관식 단일 선택 옵션 ID + * @return 업데이트된 Answer 객체 + * @throws IllegalArgumentException 유효하지 않은 응답 데이터 또는 다중 선택 문항에 대한 시도 + * @throws NoSuchElementException 해당 ID의 답변이 없을 경우 + */ + public Answer updateAnswer(UUID answerId, String answerText, UUID selectedOptionId) { + Answer existingAnswer = getAnswerById(answerId); // 기존 답변 조회 + String questionType = questionService.getQuestionById(existingAnswer.getQuestionId()).getQuestionType(); + + // 다중 선택 문항은 이 메서드로 업데이트하지 않도록 제약 + if ("객관식_다중".equals(questionType)) { + throw new IllegalArgumentException("Multi-select answers should be updated via specific methods (e.g., in AnswerSelectedOptionService)."); + } + + Answer answer = Answer.builder() + .answerId(answerId) + .responseId(existingAnswer.getResponseId()) // 기존 응답 세션 ID 유지 + .questionId(existingAnswer.getQuestionId()) // 기존 문항 ID 유지 + .answerText(answerText) + .selectedOptionId(selectedOptionId) + .build(); + + answerMapper.updateAnswer(answer); // Mapper를 통해 답변 정보 업데이트 + return getAnswerById(answerId); // 업데이트된 최신 정보 반환 + } + + /** + * 특정 ID의 답변을 삭제합니다. + * 객관식 다중 선택 문항의 경우, 관련 AnswerSelectedOption 레코드도 함께 삭제됩니다. + * + * @param answerId 삭제할 답변 고유 ID + * @throws NoSuchElementException 해당 ID의 답변이 없을 경우 + */ + public void deleteAnswer(UUID answerId) { + getAnswerById(answerId); // 삭제할 답변이 존재하는지 먼저 확인합니다. + + // 다중 선택 옵션이 있다면 먼저 삭제하여 외래 키 제약조건 위배를 방지합니다. + answerSelectedOptionMapper.deleteByAnswerId(answerId); + + answerMapper.deleteAnswer(answerId); // Mapper를 통해 답변 삭제 + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/service/QuestionOptionService.java b/src/main/java/sgis/surveysystem/service/QuestionOptionService.java new file mode 100644 index 0000000..903d7dd --- /dev/null +++ b/src/main/java/sgis/surveysystem/service/QuestionOptionService.java @@ -0,0 +1,112 @@ +package sgis.surveysystem.service; + +import sgis.surveysystem.domain.QuestionOption; +import sgis.surveysystem.mapper.QuestionOptionMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; + +/** + * 객관식 문항 선택지(QuestionOption) 관련 비즈니스 로직을 처리하는 서비스 클래스. + * MyBatis Mapper를 통해 데이터베이스와 상호작용합니다. + */ +@Service +@RequiredArgsConstructor +@Transactional +public class QuestionOptionService { + + private final QuestionOptionMapper questionOptionMapper; + private final QuestionService questionService; // 선택지가 속할 문항의 유효성 검사를 위해 QuestionService 주입 + + /** + * 새로운 객관식 문항 선택지를 생성합니다. + * 선택지 ID는 애플리케이션에서 UUID를 생성하여 할당합니다. + * + * @param questionId 선택지가 속할 문항 ID + * @param optionText 선택지 내용 + * @param optionOrder 문항 내 선택지 순서 + * @return 생성된 QuestionOption 객체 + * @throws NoSuchElementException 해당 문항 ID가 유효하지 않을 경우 + */ + public QuestionOption createQuestionOption(UUID questionId, String optionText, Integer optionOrder) { + // 선택지가 속할 문항이 존재하는지 먼저 확인합니다. + questionService.getQuestionById(questionId); + // TODO: 해당 Question의 questionType이 '객관식_단일' 또는 '객관식_다중'인지 검증하는 로직 추가를 권장합니다. + + QuestionOption option = QuestionOption.builder() + .optionId(UUID.randomUUID()) // 애플리케이션에서 새로운 UUID 생성 + .questionId(questionId) + .optionText(optionText) + .optionOrder(optionOrder) + .build(); + questionOptionMapper.insertQuestionOption(option); // Mapper를 통해 데이터베이스에 선택지 정보 삽입 + return option; + } + + /** + * 특정 문항에 속한 모든 선택지를 순서대로 조회합니다. + * 읽기 전용 트랜잭션으로 설정합니다. + * + * @param questionId 문항 고유 ID + * @return 해당 문항의 모든 QuestionOption 객체 리스트 + * @throws NoSuchElementException 해당 문항 ID가 유효하지 않을 경우 + */ + @Transactional(readOnly = true) + public List getOptionsByQuestionId(UUID questionId) { + questionService.getQuestionById(questionId); // 문항 존재 여부 확인 + return questionOptionMapper.findOptionsByQuestionId(questionId); // Mapper를 통해 문항 ID로 선택지 목록 조회 + } + + /** + * 특정 ID의 선택지를 조회합니다. + * 읽기 전용 트랜잭션으로 설정합니다. + * + * @param optionId 선택지 고유 ID + * @return 조회된 QuestionOption 객체 + * @throws NoSuchElementException 해당 ID의 선택지가 데이터베이스에 없을 경우 발생 + */ + @Transactional(readOnly = true) + public QuestionOption getQuestionOptionById(UUID optionId) { + return questionOptionMapper.findQuestionOptionById(optionId) // Mapper를 통해 ID로 선택지 조회 + .orElseThrow(() -> new NoSuchElementException("Question Option not found with ID: " + optionId)); // 없을 경우 예외 발생 + } + + /** + * 선택지 정보를 업데이트합니다. + * 업데이트 전에 해당 선택지가 존재하는지 확인합니다. + * + * @param optionId 업데이트할 선택지의 고유 ID + * @param optionText 새로운 선택지 내용 + * @param optionOrder 새로운 선택지 순서 + * @return 업데이트된 QuestionOption 객체 (업데이트 후 다시 조회하여 최신 상태 반환) + * @throws NoSuchElementException 해당 ID의 선택지가 없을 경우 + */ + public QuestionOption updateQuestionOption(UUID optionId, String optionText, Integer optionOrder) { + getQuestionOptionById(optionId); // 업데이트할 선택지가 존재하는지 먼저 확인합니다. + + QuestionOption option = QuestionOption.builder() + .optionId(optionId) // 업데이트 대상 선택지 ID + .optionText(optionText) + .optionOrder(optionOrder) + .build(); + questionOptionMapper.updateQuestionOption(option); // Mapper를 통해 선택지 정보 업데이트 + + return getQuestionOptionById(optionId); // 업데이트된 최신 정보를 다시 조회하여 반환합니다. + } + + /** + * 특정 ID의 선택지를 삭제합니다. + * 삭제 전에 해당 선택지가 존재하는지 확인합니다. + * + * @param optionId 삭제할 선택지의 고유 ID + * @throws NoSuchElementException 해당 ID의 선택지가 없을 경우 + */ + public void deleteQuestionOption(UUID optionId) { + getQuestionOptionById(optionId); // 삭제할 선택지가 존재하는지 먼저 확인합니다. + questionOptionMapper.deleteQuestionOption(optionId); // Mapper를 통해 선택지 삭제 + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/service/QuestionService.java b/src/main/java/sgis/surveysystem/service/QuestionService.java new file mode 100644 index 0000000..9890d2d --- /dev/null +++ b/src/main/java/sgis/surveysystem/service/QuestionService.java @@ -0,0 +1,119 @@ +package sgis.surveysystem.service; + +import sgis.surveysystem.domain.Question; +import sgis.surveysystem.mapper.QuestionMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; + +/** + * 설문 문항(Question) 관련 비즈니스 로직을 처리하는 서비스 클래스. + * MyBatis Mapper를 통해 데이터베이스와 상호작용합니다. + */ +@Service +@RequiredArgsConstructor +@Transactional +public class QuestionService { + + private final QuestionMapper questionMapper; + private final SurveyService surveyService; // 문항이 속할 설문의 유효성 검사를 위해 SurveyService 주입 + + /** + * 새로운 설문 문항을 생성합니다. + * 문항 ID는 애플리케이션에서 UUID를 생성하여 할당합니다. + * + * @param surveyId 문항이 속할 설문 ID + * @param questionText 문항 내용 + * @param questionType 문항 유형 ('단답형', '객관식_단일', '객관식_다중', '주관식') + * @param questionOrder 설문 내 문항 순서 + * @param isRequired 필수 응답 여부 + * @return 생성된 Question 객체 + * @throws NoSuchElementException 해당 설문 ID가 유효하지 않을 경우 + */ + public Question createQuestion(UUID surveyId, String questionText, String questionType, Integer questionOrder, Boolean isRequired) { + // 문항이 속할 설문이 존재하는지 먼저 확인하여 외래 키 제약조건 위배를 방지합니다. + surveyService.getSurveyById(surveyId); + + Question question = Question.builder() + .questionId(UUID.randomUUID()) // 애플리케이션에서 새로운 UUID 생성 + .surveyId(surveyId) + .questionText(questionText) + .questionType(questionType) + .questionOrder(questionOrder) + .isRequired(isRequired) + .build(); + questionMapper.insertQuestion(question); // Mapper를 통해 데이터베이스에 문항 정보 삽입 + return question; + } + + /** + * 특정 설문에 속한 모든 문항을 순서대로 조회합니다. + * 읽기 전용 트랜잭션으로 설정합니다. + * + * @param surveyId 설문 고유 ID + * @return 해당 설문의 모든 Question 객체 리스트 + * @throws NoSuchElementException 해당 설문 ID가 유효하지 않을 경우 + */ + @Transactional(readOnly = true) + public List getQuestionsBySurveyId(UUID surveyId) { + surveyService.getSurveyById(surveyId); // 설문 존재 여부 확인 + return questionMapper.findQuestionsBySurveyId(surveyId); // Mapper를 통해 설문 ID로 문항 목록 조회 + } + + /** + * 특정 ID의 문항을 조회합니다. + * 읽기 전용 트랜잭션으로 설정합니다. + * + * @param questionId 문항 고유 ID + * @return 조회된 Question 객체 + * @throws NoSuchElementException 해당 ID의 문항이 데이터베이스에 없을 경우 발생 + */ + @Transactional(readOnly = true) + public Question getQuestionById(UUID questionId) { + return questionMapper.findQuestionById(questionId) // Mapper를 통해 ID로 문항 조회 + .orElseThrow(() -> new NoSuchElementException("Question not found with ID: " + questionId)); // 없을 경우 예외 발생 + } + + /** + * 문항 정보를 업데이트합니다. + * 업데이트 전에 해당 문항이 존재하는지 확인합니다. + * + * @param questionId 업데이트할 문항의 고유 ID + * @param questionText 새로운 문항 내용 + * @param questionType 새로운 문항 유형 + * @param questionOrder 새로운 문항 순서 + * @param isRequired 새로운 필수 응답 여부 + * @return 업데이트된 Question 객체 (업데이트 후 다시 조회하여 최신 상태 반환) + * @throws NoSuchElementException 해당 ID의 문항이 없을 경우 + */ + public Question updateQuestion(UUID questionId, String questionText, String questionType, Integer questionOrder, Boolean isRequired) { + getQuestionById(questionId); // 업데이트할 문항이 존재하는지 먼저 확인합니다. + + Question question = Question.builder() + .questionId(questionId) // 업데이트 대상 문항 ID + .questionText(questionText) + .questionType(questionType) + .questionOrder(questionOrder) + .isRequired(isRequired) + .build(); + questionMapper.updateQuestion(question); // Mapper를 통해 문항 정보 업데이트 + + return getQuestionById(questionId); // 업데이트된 최신 정보를 다시 조회하여 반환합니다. + } + + /** + * 특정 ID의 문항을 삭제합니다. + * 삭제 전에 해당 문항이 존재하는지 확인합니다. + * + * @param questionId 삭제할 문항의 고유 ID + * @throws NoSuchElementException 해당 ID의 문항이 없을 경우 + */ + public void deleteQuestion(UUID questionId) { + getQuestionById(questionId); // 삭제할 문항이 존재하는지 먼저 확인합니다. + questionMapper.deleteQuestion(questionId); // Mapper를 통해 문항 삭제 + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/service/RespondentService.java b/src/main/java/sgis/surveysystem/service/RespondentService.java new file mode 100644 index 0000000..f1c4caa --- /dev/null +++ b/src/main/java/sgis/surveysystem/service/RespondentService.java @@ -0,0 +1,101 @@ +package sgis.surveysystem.service; + +import sgis.surveysystem.domain.Respondent; +import sgis.surveysystem.mapper.RespondentMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.UUID; + +/** + * 응답자(Respondent) 관련 비즈니스 로직을 처리하는 서비스 클래스. + * MyBatis Mapper를 통해 데이터베이스와 상호작용합니다. + */ +@Service +@RequiredArgsConstructor +@Transactional +public class RespondentService { + + private final RespondentMapper respondentMapper; + + /** + * 새로운 응답자를 생성하거나, user_id가 주어지고 해당 user_id로 이미 존재하는 응답자를 조회합니다. + * + * @param userId 시스템 내 사용자 ID (선택 사항, 익명 응답 시 null) + * @param ipAddress 응답자의 IP 주소 (선택 사항) + * @return 생성되거나 조회된 Respondent 객체 + */ + public Respondent createOrGetRespondent(String userId, String ipAddress) { + // user_id가 제공된 경우, 기존 응답자가 있는지 먼저 확인합니다. + if (userId != null && !userId.isEmpty()) { + Optional existingRespondent = respondentMapper.findRespondentByUserId(userId); + if (existingRespondent.isPresent()) { + return existingRespondent.get(); // 이미 존재하는 응답자 반환 + } + } + + // 기존 응답자가 없거나 user_id가 제공되지 않은 경우, 새로운 응답자를 생성합니다. + Respondent respondent = Respondent.builder() + .respondentId(UUID.randomUUID()) // 애플리케이션에서 새로운 UUID 생성 + .userId(userId) + .ipAddress(ipAddress) + .responseStartAt(LocalDateTime.now()) // 응답 시작 시점 기록 + .build(); + respondentMapper.insertRespondent(respondent); // Mapper를 통해 응답자 정보 삽입 + return respondent; + } + + /** + * 특정 ID의 응답자를 조회합니다. + * 읽기 전용 트랜잭션으로 설정합니다. + * + * @param respondentId 응답자 고유 ID + * @return 조회된 Respondent 객체 + * @throws NoSuchElementException 해당 ID의 응답자가 데이터베이스에 없을 경우 발생 + */ + @Transactional(readOnly = true) + public Respondent getRespondentById(UUID respondentId) { + return respondentMapper.findRespondentById(respondentId) // Mapper를 통해 ID로 응답자 조회 + .orElseThrow(() -> new NoSuchElementException("Respondent not found with ID: " + respondentId)); // 없을 경우 예외 발생 + } + + /** + * 응답자 정보를 업데이트합니다. + * 업데이트 전에 해당 응답자가 존재하는지 확인합니다. + * + * @param respondentId 업데이트할 응답자 고유 ID + * @param userId 새로운 사용자 ID + * @param ipAddress 새로운 IP 주소 + * @param responseEndAt 응답 완료 일시 (null이면 현재 시각으로 설정) + * @return 업데이트된 Respondent 객체 (업데이트 후 다시 조회하여 최신 상태 반환) + * @throws NoSuchElementException 해당 ID의 응답자가 없을 경우 + */ + public Respondent updateRespondent(UUID respondentId, String userId, String ipAddress, LocalDateTime responseEndAt) { + getRespondentById(respondentId); // 업데이트할 응답자가 존재하는지 먼저 확인합니다. + + Respondent respondent = Respondent.builder() + .respondentId(respondentId) // 업데이트 대상 응답자 ID + .userId(userId) + .ipAddress(ipAddress) + .responseEndAt(responseEndAt != null ? responseEndAt : LocalDateTime.now()) // 완료 시점 갱신 또는 현재 시각 + .build(); + respondentMapper.updateRespondent(respondent); // Mapper를 통해 응답자 정보 업데이트 + return getRespondentById(respondentId); // 업데이트된 최신 정보를 다시 조회하여 반환합니다. + } + + /** + * 특정 ID의 응답자를 삭제합니다. + * 삭제 전에 해당 응답자가 존재하는지 확인합니다. + * + * @param respondentId 삭제할 응답자 고유 ID + * @throws NoSuchElementException 해당 ID의 응답자가 없을 경우 + */ + public void deleteRespondent(UUID respondentId) { + getRespondentById(respondentId); // 삭제할 응답자가 존재하는지 먼저 확인합니다. + respondentMapper.deleteRespondent(respondentId); // Mapper를 통해 응답자 삭제 + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/service/SurveyResponseService.java b/src/main/java/sgis/surveysystem/service/SurveyResponseService.java new file mode 100644 index 0000000..47a7822 --- /dev/null +++ b/src/main/java/sgis/surveysystem/service/SurveyResponseService.java @@ -0,0 +1,107 @@ +package sgis.surveysystem.service; + +import sgis.surveysystem.domain.SurveyResponse; +import sgis.surveysystem.mapper.SurveyResponseMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; + +/** + * 설문 응답 세션(SurveyResponse) 관련 비즈니스 로직을 처리하는 서비스 클래스. + * 사용자가 설문을 제출할 때마다 생성되는 응답 세션을 관리합니다. + */ +@Service +@RequiredArgsConstructor +@Transactional +public class SurveyResponseService { + + private final SurveyResponseMapper surveyResponseMapper; + private final SurveyService surveyService; // 외래 키 연결을 위해 SurveyService 주입 + private final RespondentService respondentService; // 외래 키 연결을 위해 RespondentService 주입 + + /** + * 새로운 설문 응답 세션을 생성합니다. + * 응답 세션 ID는 애플리케이션에서 UUID를 생성하여 할당합니다. + * + * @param surveyId 응답이 속한 설문 ID + * @param respondentId 응답자 ID (선택 사항, 익명 설문인 경우 null) + * @return 생성된 SurveyResponse 객체 + * @throws NoSuchElementException 설문 ID 또는 응답자 ID가 유효하지 않을 경우 + */ + public SurveyResponse createSurveyResponse(UUID surveyId, UUID respondentId) { + surveyService.getSurveyById(surveyId); // 설문 존재 여부 확인 + + // 응답자 ID가 제공된 경우, 해당 응답자가 존재하는지 확인합니다. + if (respondentId != null) { + respondentService.getRespondentById(respondentId); + } + + SurveyResponse surveyResponse = SurveyResponse.builder() + .responseId(UUID.randomUUID()) // 애플리케이션에서 새로운 UUID 생성 + .surveyId(surveyId) + .respondentId(respondentId) + .submittedAt(LocalDateTime.now()) // 제출 시점 기록 + .build(); + surveyResponseMapper.insertSurveyResponse(surveyResponse); // Mapper를 통해 응답 세션 정보 삽입 + return surveyResponse; + } + + /** + * 특정 ID의 설문 응답 세션을 조회합니다. + * 읽기 전용 트랜잭션으로 설정합니다. + * + * @param responseId 응답 세션 고유 ID + * @return 조회된 SurveyResponse 객체 + * @throws NoSuchElementException 해당 ID의 응답 세션이 데이터베이스에 없을 경우 발생 + */ + @Transactional(readOnly = true) + public SurveyResponse getSurveyResponseById(UUID responseId) { + return surveyResponseMapper.findSurveyResponseById(responseId) // Mapper를 통해 ID로 응답 세션 조회 + .orElseThrow(() -> new NoSuchElementException("Survey Response not found with ID: " + responseId)); // 없을 경우 예외 발생 + } + + /** + * 특정 설문에 대한 모든 응답 세션을 조회합니다. + * 읽기 전용 트랜잭션으로 설정합니다. + * + * @param surveyId 설문 고유 ID + * @return 해당 설문의 모든 SurveyResponse 객체 리스트 + * @throws NoSuchElementException 해당 설문 ID가 유효하지 않을 경우 + */ + @Transactional(readOnly = true) + public List getSurveyResponsesBySurveyId(UUID surveyId) { + surveyService.getSurveyById(surveyId); // 설문 존재 여부 확인 + return surveyResponseMapper.findResponsesBySurveyId(surveyId); // Mapper를 통해 설문 ID로 응답 세션 목록 조회 + } + + /** + * 특정 응답자가 제출한 모든 응답 세션을 조회합니다. + * 읽기 전용 트랜잭션으로 설정합니다. + * + * @param respondentId 응답자 고유 ID + * @return 해당 응답자의 모든 SurveyResponse 객체 리스트 + * @throws NoSuchElementException 해당 응답자 ID가 유효하지 않을 경우 + */ + @Transactional(readOnly = true) + public List getSurveyResponsesByRespondentId(UUID respondentId) { + respondentService.getRespondentById(respondentId); // 응답자 존재 여부 확인 + return surveyResponseMapper.findResponsesByRespondentId(respondentId); // Mapper를 통해 응답자 ID로 응답 세션 목록 조회 + } + + /** + * 특정 ID의 설문 응답 세션을 삭제합니다. + * 삭제 전에 해당 응답 세션이 존재하는지 확인합니다. + * + * @param responseId 삭제할 응답 세션 고유 ID + * @throws NoSuchElementException 해당 ID의 응답 세션이 없을 경우 + */ + public void deleteSurveyResponse(UUID responseId) { + getSurveyResponseById(responseId); // 삭제할 응답 세션이 존재하는지 먼저 확인합니다. + surveyResponseMapper.deleteSurveyResponse(responseId); // Mapper를 통해 응답 세션 삭제 + } +} \ No newline at end of file diff --git a/src/main/java/sgis/surveysystem/service/SurveyService.java b/src/main/java/sgis/surveysystem/service/SurveyService.java new file mode 100644 index 0000000..972f8c1 --- /dev/null +++ b/src/main/java/sgis/surveysystem/service/SurveyService.java @@ -0,0 +1,137 @@ +package sgis.surveysystem.service; + +import sgis.surveysystem.domain.Survey; +import sgis.surveysystem.mapper.SurveyMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; + +/** + * 설문(Survey) 관련 비즈니스 로직을 처리하는 서비스 클래스. + * MyBatis Mapper를 통해 데이터베이스와 상호작용합니다. + */ +@Service +@RequiredArgsConstructor // Lombok을 사용하여 final 필드를 인자로 받는 생성자를 자동 생성합니다. (의존성 주입) +@Transactional // 클래스 레벨에 트랜잭션 설정을 적용합니다. +public class SurveyService { + + private final SurveyMapper surveyMapper; + + /** + * 새로운 설문을 생성합니다. + * 설문 ID는 애플리케이션에서 UUID를 생성하여 할당합니다. + * + * @param surveyTitle 설문 제목 + * @param surveyDescription 설문 상세 설명 + * @param startDate 설문 시작 일시 + * @param endDate 설문 종료 일시 + * @return 생성된 Survey 객체 + */ + public Survey createSurvey(String surveyTitle, String surveyDescription, LocalDateTime startDate, LocalDateTime endDate) { + // 애플리케이션에서 새로운 UUID를 생성하여 설문 ID로 사용합니다. + // 데이터베이스의 DEFAULT gen_random_uuid() 설정과 무관하게 동작합니다. + Survey survey = Survey.builder() + .surveyId(UUID.randomUUID()) + .surveyTitle(surveyTitle) + .surveyDescription(surveyDescription) + .createdAt(LocalDateTime.now()) // 현재 시각으로 생성일 설정 + .updatedAt(LocalDateTime.now()) // 현재 시각으로 수정일 설정 + .isActive(true) // 기본적으로 활성화 상태로 설정 + .startDate(startDate) + .endDate(endDate) + .build(); + surveyMapper.insertSurvey(survey); // Mapper를 통해 데이터베이스에 설문 정보 삽입 + return survey; + } + + /** + * 모든 설문 목록을 조회합니다. + * 읽기 전용 트랜잭션으로 설정하여 성능을 최적화합니다. + * + * @return 모든 Survey 객체 리스트 + */ + @Transactional(readOnly = true) + public List getAllSurveys() { + return surveyMapper.findAllSurveys(); // Mapper를 통해 모든 설문 조회 + } + + /** + * 특정 ID의 설문을 조회합니다. + * 읽기 전용 트랜잭션으로 설정합니다. + * + * @param surveyId 설문 고유 ID + * @return 조회된 Survey 객체 + * @throws NoSuchElementException 해당 ID의 설문이 데이터베이스에 없을 경우 발생 + */ + @Transactional(readOnly = true) + public Survey getSurveyById(UUID surveyId) { + return surveyMapper.findSurveyById(surveyId) // Mapper를 통해 ID로 설문 조회 + .orElseThrow(() -> new NoSuchElementException("Survey not found with ID: " + surveyId)); // 없을 경우 예외 발생 + } + + /** + * 설문 정보를 업데이트합니다. + * 업데이트 전에 해당 설문이 존재하는지 확인합니다. + * + * @param surveyId 업데이트할 설문의 고유 ID + * @param surveyTitle 새로운 설문 제목 + * @param surveyDescription 새로운 설문 설명 + * @param isActive 설문 활성화 여부 + * @param startDate 새로운 설문 시작 일시 + * @param endDate 새로운 설문 종료 일시 + * @return 업데이트된 Survey 객체 (업데이트 후 다시 조회하여 최신 상태 반환) + * @throws NoSuchElementException 해당 ID의 설문이 없을 경우 + */ + public Survey updateSurvey(UUID surveyId, String surveyTitle, String surveyDescription, Boolean isActive, LocalDateTime startDate, LocalDateTime endDate) { + // 업데이트할 설문이 존재하는지 먼저 확인합니다. + getSurveyById(surveyId); + + // 업데이트할 필드들을 포함하는 Survey 객체를 생성합니다. + // MyBatis는 이 객체의 필드 값을 사용하여 SQL 쿼리의 파라미터를 채웁니다. + Survey survey = Survey.builder() + .surveyId(surveyId) // 업데이트 대상 설문 ID + .surveyTitle(surveyTitle) + .surveyDescription(surveyDescription) + .isActive(isActive) + .startDate(startDate) + .endDate(endDate) + .updatedAt(LocalDateTime.now()) // 업데이트 시각을 현재로 갱신 + .build(); + surveyMapper.updateSurvey(survey); // Mapper를 통해 설문 정보 업데이트 + + // 업데이트된 최신 정보를 데이터베이스에서 다시 조회하여 반환합니다. + return getSurveyById(surveyId); + } + + /** + * 특정 ID의 설문을 삭제합니다. + * 삭제 전에 해당 설문이 존재하는지 확인합니다. + * + * @param surveyId 삭제할 설문의 고유 ID + * @throws NoSuchElementException 해당 ID의 설문이 없을 경우 + */ + public void deleteSurvey(UUID surveyId) { + // 삭제할 설문이 존재하는지 먼저 확인합니다. + getSurveyById(surveyId); + surveyMapper.deleteSurvey(surveyId); // Mapper를 통해 설문 삭제 + } + + /** + * 특정 ID의 설문을 비활성화합니다. + * 비활성화 전에 해당 설문이 존재하는지 확인합니다. + * + * @param surveyId 비활성화할 설문의 고유 ID + * @return 비활성화된 Survey 객체 (업데이트 후 다시 조회하여 최신 상태 반환) + * @throws NoSuchElementException 해당 ID의 설문이 없을 경우 + */ + public Survey deactivateSurvey(UUID surveyId) { + getSurveyById(surveyId); // 존재 여부 확인 + surveyMapper.updateSurveyStatus(surveyId, false); // Mapper를 통해 설문 상태 업데이트 + return getSurveyById(surveyId); // 업데이트된 상태로 반환 + } +} \ No newline at end of file diff --git a/src/main/resources/egovframework/mapper/sgis/map/MapMainMapper.xml b/src/main/resources/egovframework/mapper/sgis/map/MapMainMapper.xml index e496b63..ce13c3e 100644 --- a/src/main/resources/egovframework/mapper/sgis/map/MapMainMapper.xml +++ b/src/main/resources/egovframework/mapper/sgis/map/MapMainMapper.xml @@ -514,4 +514,54 @@ FROM (SELECT ROWNUM RNK, + + + + diff --git a/src/main/resources/egovframework/mapper/sgis/surveysystem/AnswerMapper.xml b/src/main/resources/egovframework/mapper/sgis/surveysystem/AnswerMapper.xml new file mode 100644 index 0000000..a84276f --- /dev/null +++ b/src/main/resources/egovframework/mapper/sgis/surveysystem/AnswerMapper.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + INSERT INTO tb_answers ( + answer_id, + response_id, + question_id, + answer_text, + selected_option_id + ) VALUES ( + #{answerId, jdbcType=OTHER}, + #{responseId, jdbcType=OTHER}, + #{questionId, jdbcType=OTHER}, + #{answerText}, + #{selectedOptionId, jdbcType=OTHER} + ) + + + + + + + + + + UPDATE tb_answers + SET + answer_text = #{answerText}, + selected_option_id = #{selectedOptionId, jdbcType=OTHER} + WHERE answer_id = #{answerId, jdbcType=OTHER} + + + + DELETE FROM tb_answers + WHERE answer_id = #{answerId, jdbcType=OTHER} + + \ No newline at end of file diff --git a/src/main/resources/egovframework/mapper/sgis/surveysystem/AnswerSelectedOptionMapper.xml b/src/main/resources/egovframework/mapper/sgis/surveysystem/AnswerSelectedOptionMapper.xml new file mode 100644 index 0000000..d44eb5e --- /dev/null +++ b/src/main/resources/egovframework/mapper/sgis/surveysystem/AnswerSelectedOptionMapper.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + INSERT INTO tb_answer_selected_options ( + answer_id, + option_id + ) VALUES ( + #{answerId, jdbcType=OTHER}, + #{optionId, jdbcType=OTHER} + ) + + + + INSERT INTO tb_answer_selected_options ( + answer_id, + option_id + ) VALUES + + ( + #{item.answerId, jdbcType=OTHER}, + #{item.optionId, jdbcType=OTHER} + ) + + + + + + + DELETE FROM tb_answer_selected_options + WHERE answer_id = #{answerId, jdbcType=OTHER} + + + + DELETE FROM tb_answer_selected_options + WHERE answer_id = #{param1, jdbcType=OTHER} AND option_id = #{param2, jdbcType=OTHER} + + \ No newline at end of file diff --git a/src/main/resources/egovframework/mapper/sgis/surveysystem/QuestionMapper.xml b/src/main/resources/egovframework/mapper/sgis/surveysystem/QuestionMapper.xml new file mode 100644 index 0000000..f5ce90d --- /dev/null +++ b/src/main/resources/egovframework/mapper/sgis/surveysystem/QuestionMapper.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + INSERT INTO tb_questions ( + question_id, + survey_id, + question_text, + question_type, + question_order, + is_required + ) VALUES ( + #{questionId, jdbcType=OTHER}, + #{surveyId, jdbcType=OTHER}, + #{questionText}, + #{questionType}, + #{questionOrder}, + #{isRequired} + ) + + + + + + + + + + + UPDATE tb_questions + SET + question_text = #{questionText}, + question_type = #{questionType}, + question_order = #{questionOrder}, + is_required = #{isRequired} + WHERE question_id = #{questionId, jdbcType=OTHER} + + + + + DELETE FROM tb_questions + WHERE question_id = #{questionId, jdbcType=OTHER} + + \ No newline at end of file diff --git a/src/main/resources/egovframework/mapper/sgis/surveysystem/QuestionOptionMapper.xml b/src/main/resources/egovframework/mapper/sgis/surveysystem/QuestionOptionMapper.xml new file mode 100644 index 0000000..bd134c1 --- /dev/null +++ b/src/main/resources/egovframework/mapper/sgis/surveysystem/QuestionOptionMapper.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + INSERT INTO tb_question_options ( + option_id, + question_id, + option_text, + option_order + ) VALUES ( + #{optionId, jdbcType=OTHER}, + #{questionId, jdbcType=OTHER}, + #{optionText}, + #{optionOrder} + ) + + + + + + + + UPDATE tb_question_options + SET + option_text = #{optionText}, + option_order = #{optionOrder} + WHERE option_id = #{optionId, jdbcType=OTHER} + + + + DELETE FROM tb_question_options + WHERE option_id = #{optionId, jdbcType=OTHER} + + \ No newline at end of file diff --git a/src/main/resources/egovframework/mapper/sgis/surveysystem/RespondentMapper.xml b/src/main/resources/egovframework/mapper/sgis/surveysystem/RespondentMapper.xml new file mode 100644 index 0000000..4820568 --- /dev/null +++ b/src/main/resources/egovframework/mapper/sgis/surveysystem/RespondentMapper.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + INSERT INTO tb_respondents ( + respondent_id, + user_id, + ip_address, + response_start_at, + response_end_at + ) VALUES ( + #{respondentId, jdbcType=OTHER}, + #{userId}, + #{ipAddress, jdbcType=OTHER}, + #{responseStartAt}, + #{responseEndAt} + ) + + + + + + + + UPDATE tb_respondents + SET + user_id = #{userId}, + ip_address = #{ipAddress, jdbcType=OTHER}, + response_start_at = #{responseStartAt}, + response_end_at = #{responseEndAt} + WHERE respondent_id = #{respondentId, jdbcType=OTHER} + + + + DELETE FROM tb_respondents + WHERE respondent_id = #{respondentId, jdbcType=OTHER} + + \ No newline at end of file diff --git a/src/main/resources/egovframework/mapper/sgis/surveysystem/SurveyMapper.xml b/src/main/resources/egovframework/mapper/sgis/surveysystem/SurveyMapper.xml new file mode 100644 index 0000000..f6e962a --- /dev/null +++ b/src/main/resources/egovframework/mapper/sgis/surveysystem/SurveyMapper.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + INSERT INTO tb_surveys ( + survey_id, + survey_title, + survey_description, + created_at, + updated_at, + is_active, + start_date, + end_date + ) VALUES ( + #{surveyId, jdbcType=OTHER}, #{surveyTitle}, + #{surveyDescription}, + #{createdAt}, + #{updatedAt}, + #{isActive}, + #{startDate}, + #{endDate} + ) + + + + + + + + UPDATE tb_surveys + SET + survey_title = #{surveyTitle}, + survey_description = #{surveyDescription}, + updated_at = #{updatedAt}, + is_active = #{isActive}, + start_date = #{startDate}, + end_date = #{endDate} + WHERE survey_id = #{surveyId, jdbcType=OTHER} + + + + UPDATE tb_surveys + SET + is_active = #{isActive}, + updated_at = NOW() + WHERE survey_id = #{surveyId, jdbcType=OTHER} + + + + DELETE FROM tb_surveys + WHERE survey_id = #{surveyId, jdbcType=OTHER} + + \ No newline at end of file diff --git a/src/main/resources/egovframework/mapper/sgis/surveysystem/SurveyResponseMapper.xml b/src/main/resources/egovframework/mapper/sgis/surveysystem/SurveyResponseMapper.xml new file mode 100644 index 0000000..5941da3 --- /dev/null +++ b/src/main/resources/egovframework/mapper/sgis/surveysystem/SurveyResponseMapper.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + INSERT INTO tb_survey_responses ( + response_id, + survey_id, + respondent_id, + submitted_at + ) VALUES ( + #{responseId, jdbcType=OTHER}, + #{surveyId, jdbcType=OTHER}, + #{respondentId, jdbcType=OTHER}, + #{submittedAt} + ) + + + + + + + + + + DELETE FROM tb_survey_responses + WHERE response_id = #{responseId, jdbcType=OTHER} + + \ No newline at end of file diff --git a/src/main/resources/egovframework/spring/com/context-mapper.xml b/src/main/resources/egovframework/spring/com/context-mapper.xml index cf1626c..3d77438 100644 --- a/src/main/resources/egovframework/spring/com/context-mapper.xml +++ b/src/main/resources/egovframework/spring/com/context-mapper.xml @@ -19,7 +19,9 @@ - + + + diff --git a/src/main/webapp/WEB-INF/jsp/sgis/map/mapInformation/boreholeLog.jsp b/src/main/webapp/WEB-INF/jsp/sgis/map/mapInformation/boreholeLog.jsp new file mode 100644 index 0000000..a8752ea --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/sgis/map/mapInformation/boreholeLog.jsp @@ -0,0 +1,1360 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%> + + + + + + + 지반 주상도 + + + + +
+
+
+ + + 1 / 4 + + +
+
+ ? + +
+
+ +
+ 지반주상도 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
조사명발주처시추번호
위치좌표(m)X(N): Y(E): 표고(m)
시추완료일굴진심도(GL,-m)케이싱심도(-m)지하수위(-m)
사용장비시추방법시추자조사자
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
심도
(-m)
표고
(-m)
층후

지반명현장관찰기록코아회수율 (%)시료번호 및 채취방법심도 및 시료형태cm/sec특수계수
색조BLOWS/cm표준관입시험
1.93.69-5.10 +

+
+

매립토층

+

회색

+
+

논에 매립한 토층, 자갈섞인 모래, 느슨한 상대밀도, 습한상태

+
+
+
7-1.417.00 +

+
+

퇴적토층

+

암회색

+
+

실트섞인 점토, 매우 연약~연약한 연경도, 젖은상태, *자연시료 채취구간 -> 심도 3.0~3.8m, -> 심도 6.0~6.8m

+
10-4.41-5.00 +

+
+

퇴적토층

+

갈색

+
+

실트섞인 점토, 보통 단단~단단한 연경도, 습한상태

+
14.2-8.614.20 +

+
+

퇴적토층

+

회색-회갈색

+
+

자갈섞인 모래, 보통 조밀~조밀한 상대밀도, 습한상태

+
+ +
+
범례
+ +
+
[시료형태]
+
+
+ + 자연시료
UNDISTURBED +
+
+ + 흩트러진 시료
DISTURBED SAMPLE +
+
+ + 코아시료
CORE SAMPLE +
+
+
+ +
+
[채취방법]
+
+
DS : 데니슨 샘플러
+
PS : 피스톤 샘플러
+
SC : 싱글코어 배럴
+
DC : 더블코어 배럴
+
TC : 트리플코어 배럴
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/sgis/map/mapMain.jsp b/src/main/webapp/WEB-INF/jsp/sgis/map/mapMain.jsp index 2b84b23..80f6008 100644 --- a/src/main/webapp/WEB-INF/jsp/sgis/map/mapMain.jsp +++ b/src/main/webapp/WEB-INF/jsp/sgis/map/mapMain.jsp @@ -381,7 +381,7 @@
  • 지반단면도 - 지반주상도 + 지반주상도
  • @@ -1001,7 +1018,7 @@ - +