feat: 관리자 설문 생성 추가

master
thkim 2025-07-11 19:03:23 +09:00
parent f0f09432c3
commit 83a0fbdad9
49 changed files with 1263 additions and 217 deletions

85
.gitignore vendored Normal file
View File

@ -0,0 +1,85 @@
/target
.DS_Store
._.DS_Store
**/.DS_Store
**/._.DS_Store
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.apt_generated
.classpath
.factorypath
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
# Scala IDE specific (Scala & Java development for Eclipse)
.cache-main
.scala_dependencies
.worksheet
# Uncomment this line if you wish to ignore the project description file.
# Typically, this file would be tracked if it contains build/dependency configurations:
#.project
.svn
# ---> Java
# Compiled class file
#*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
### VS Code ###
.vscode/
.vs/
# Added by thkim

View File

@ -1,17 +1,7 @@
src\main\webapp\WEB-INF\jsp\sgis\com\common\header.jsp
src\main\webapp\WEB-INF\jsp\tiles\attribute\portal.top.jsp
src\main\webapp\WEB-INF\jsp\sgis\com\common\head.jsp
src\main\webapp\WEB-INF\jsp\sgis\com\board\boardList.jsp
src\main\resources\egovframework\mapper\sgis\board\PostMapper.xml
src\main\webapp\WEB-INF\jsp\sgis\com\board\boardView.jsp
src\main\webapp\WEB-INF\jsp\sgis\com\board\boardWrite.jsp
src\main\resources\egovframework\mapper\sgis\board\MemberMapper.xml
src\main\webapp\com\js\main.map.js
src\main\webapp\WEB-INF\jsp\sgis\map\mapMain.jsp
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
src\main\webapp\WEB-INF\jsp\sgis\map\mapInformation\boreholeLog.jsp
src\main\webapp\WEB-INF\jsp\sgis\surveysystem\createSurvey.jsp src\main\webapp\WEB-INF\jsp\sgis\surveysystem\createSurvey.jsp
src\main\webapp\WEB-INF\jsp\tiles\attribute\adm.top.jsp
src\main\webapp\WEB-INF\jsp\sgis\surveysystem\createQuestion.jsp
src\main\resources\egovframework\mapper\sgis\surveysystem\SurveyMapper.xml
src\main\resources\egovframework\mapper\sgis\surveysystem\QuestionMapper.xml
src\main\webapp\WEB-INF\jsp\sgis\surveysystem\createQuestionOption.jsp
src\main\webapp\WEB-INF\jsp\sgis\map\mapMain.jsp

68
pom.xml
View File

@ -25,36 +25,26 @@
</properties> </properties>
<repositories> <repositories>
<repository> <repository>
<id>mvn2</id> <id>mvn2s</id>
<url>http://repo1.maven.org/maven2/</url> <url>https://repo1.maven.org/maven2/</url>
<releases> <releases>
<enabled>true</enabled> <enabled>true</enabled>
</releases> </releases>
<snapshots> <snapshots>
<enabled>true</enabled> <enabled>true</enabled>
</snapshots> </snapshots>
</repository> </repository>
<repository> <repository>
<id>egovframe</id> <id>egovframe</id>
<url>http://www.egovframe.go.kr/maven/</url> <url>https://maven.egovframe.go.kr/maven/</url>
<releases> <releases>
<enabled>true</enabled> <enabled>true</enabled>
</releases> </releases>
<snapshots> <snapshots>
<enabled>false</enabled> <enabled>false</enabled>
</snapshots> </snapshots>
</repository> </repository>
<repository>
<id>egovframe2</id>
<url>http://maven.egovframe.kr:8080/maven/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository> <repository>
<id>osgeo</id> <id>osgeo</id>
<name>OSGeo Release Repository</name> <name>OSGeo Release Repository</name>
@ -299,22 +289,22 @@
<dependency> <dependency>
<groupId>com.fasterxml.jackson.core</groupId> <groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId> <artifactId>jackson-core</artifactId>
<version>2.12.5</version> <version>2.12.4</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.fasterxml.jackson.core</groupId> <groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId> <artifactId>jackson-databind</artifactId>
<version>2.12.5</version> <version>2.12.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.12.5</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.fasterxml.jackson.core</groupId> <groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId> <artifactId>jackson-annotations</artifactId>
<version>2.12.5</version> <version>2.12.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.12.4</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.googlecode.json-simple</groupId> <groupId>com.googlecode.json-simple</groupId>
@ -480,7 +470,7 @@
<build> <build>
<defaultGoal>install</defaultGoal> <defaultGoal>install</defaultGoal>
<directory>${basedir}/target</directory> <directory>${basedir}/target</directory>
<finalName>sht_webapp</finalName> <finalName>smart_ground</finalName>
<pluginManagement> <pluginManagement>
<plugins> <plugins>
<plugin> <plugin>

View File

@ -1,7 +1,5 @@
package sgis.com.util; package sgis.com.util;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.security.AlgorithmParameters; import java.security.AlgorithmParameters;
import java.security.MessageDigest; import java.security.MessageDigest;
@ -15,7 +13,6 @@ import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64;
import org.junit.Test;
/** /**
* @FileName : CryptoUtil.java * @FileName : CryptoUtil.java

View File

@ -4,7 +4,6 @@ import java.text.DecimalFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import org.junit.Test;
/** /**
* @FileName : StringUtil.java * @FileName : StringUtil.java

View File

@ -3,10 +3,14 @@ package sgis.surveysystem.controller;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.ui.Model; // Model 객체 사용을 위해 추가
import java.util.UUID;
/** /**
* . * .
* JSP . * JSP .
* .do .
*/ */
@Controller // 뷰 이름을 반환하는 컨트롤러임을 명시 @Controller // 뷰 이름을 반환하는 컨트롤러임을 명시
@RequestMapping("/admin/survey") // 이 컨트롤러의 기본 URL 경로 @RequestMapping("/admin/survey") // 이 컨트롤러의 기본 URL 경로
@ -14,15 +18,50 @@ public class AdminSurveyPageController {
/** /**
* . * .
* http://localhost:8080/your-app-context/admin/survey/create 로 접근 시 이 메서드가 호출됩니다. * `/admin/survey/createSurveyForm.do` .
* (your-app-context . : /sgis)
* *
* @return JSP (ViewResolver ) * @return JSP (ViewResolver )
*/ */
@GetMapping("/create.do") @GetMapping("/createSurveyForm.do") // GET 요청 처리, .do 확장자 추가
public String showCreateSurveyForm() { public String showCreateSurveyForm() {
// ViewResolver 설정에 따라 다음 경로의 JSP 파일을 찾게 됩니다: // ViewResolver 설정에 따라 다음 경로의 JSP 파일을 찾게 됩니다:
// /WEB-INF/jsp/ + sgis/surveysystem/createSurvey + .jsp // /WEB-INF/jsp/sgis/surveysystem/createSurvey.jsp
return "sgis/surveysystem/createSurvey"; return "sgis/surveysystem/createSurvey";
} }
/**
* .
* `/admin/survey/{surveyId}/createQuestionForm.do` .
* .
*
* @param surveyId ID
* @param model Spring Model ( )
* @return JSP (ViewResolver )
*/
@GetMapping("/{surveyId}/createQuestionForm.do") // GET 요청 처리, .do 확장자 추가
public String showCreateQuestionForm(@PathVariable UUID surveyId, Model model) {
// surveyId를 Model에 추가하여 JSP에서 ${surveyId}로 접근할 수 있도록 합니다.
model.addAttribute("surveyId", surveyId);
// ViewResolver 설정에 따라 다음 경로의 JSP 파일을 찾게 됩니다:
// /WEB-INF/jsp/sgis/surveysystem/createQuestion.jsp
return "sgis/surveysystem/createQuestion";
}
/**
* .
* `/admin/survey/question/{questionId}/createOptionForm.do` .
* .
*
* @param questionId ID
* @param model Spring Model ( )
* @return JSP (ViewResolver )
*/
@GetMapping("/question/{questionId}/createOptionForm.do") // GET 요청 처리, .do 확장자 추가
public String showCreateQuestionOptionForm(@PathVariable UUID questionId, Model model) {
// questionId를 Model에 추가하여 JSP에서 ${questionId}로 접근할 수 있도록 합니다.
model.addAttribute("questionId", questionId);
// ViewResolver 설정에 따라 다음 경로의 JSP 파일을 찾게 됩니다:
// /WEB-INF/jsp/sgis/surveysystem/createQuestionOption.jsp
return "sgis/surveysystem/createQuestionOption";
}
} }

View File

@ -19,6 +19,7 @@ import java.util.stream.Collectors;
/** /**
* (Question) HTTP REST . * (Question) HTTP REST .
* , , , API . * , , , API .
* .do , GET POST .
*/ */
@RestController @RestController
@RequestMapping("/api/questions") @RequestMapping("/api/questions")
@ -34,7 +35,7 @@ public class QuestionController {
* @param request Question DTO * @param request Question DTO
* @return (HTTP 201 Created) * @return (HTTP 201 Created)
*/ */
@PostMapping @PostMapping("/createQuestion.do") // POST 요청 처리, .do 확장자 추가
public ResponseEntity<QuestionResponse> createQuestion(@RequestBody QuestionCreateRequest request) { public ResponseEntity<QuestionResponse> createQuestion(@RequestBody QuestionCreateRequest request) {
QuestionResponse createdQuestion = QuestionResponse.from( QuestionResponse createdQuestion = QuestionResponse.from(
questionService.createQuestion( questionService.createQuestion(
@ -55,7 +56,7 @@ public class QuestionController {
* @param surveyId ID * @param surveyId ID
* @return (HTTP 200 OK) * @return (HTTP 200 OK)
*/ */
@GetMapping("/by-survey/{surveyId}") @GetMapping("/by-survey/{surveyId}/getQuestions.do") // GET 요청 처리, .do 확장자 추가
public ResponseEntity<List<QuestionResponse>> getQuestionsBySurveyId(@PathVariable UUID surveyId) { public ResponseEntity<List<QuestionResponse>> getQuestionsBySurveyId(@PathVariable UUID surveyId) {
List<QuestionResponse> questions = questionService.getQuestionsBySurveyId(surveyId).stream() List<QuestionResponse> questions = questionService.getQuestionsBySurveyId(surveyId).stream()
.map(question -> { .map(question -> {
@ -81,7 +82,7 @@ public class QuestionController {
* @return (HTTP 200 OK) * @return (HTTP 200 OK)
* HTTP 404 Not Found * HTTP 404 Not Found
*/ */
@GetMapping("/{questionId}") @GetMapping("/{questionId}/getQuestion.do") // GET 요청 처리, .do 확장자 추가
public ResponseEntity<QuestionResponse> getQuestionById(@PathVariable UUID questionId) { public ResponseEntity<QuestionResponse> getQuestionById(@PathVariable UUID questionId) {
try { try {
QuestionResponse question = QuestionResponse.from(questionService.getQuestionById(questionId)); QuestionResponse question = QuestionResponse.from(questionService.getQuestionById(questionId));
@ -99,14 +100,15 @@ public class QuestionController {
} }
/** /**
* . (HTTP PUT) * . (HTTP POST)
* PUT POST , URL update .
* *
* @param questionId ID * @param questionId ID
* @param request Question DTO * @param request Question DTO
* @return (HTTP 200 OK) * @return (HTTP 200 OK)
* HTTP 404 Not Found * HTTP 404 Not Found
*/ */
@PutMapping("/{questionId}") @PostMapping("/{questionId}/updateQuestion.do") // POST 요청으로 업데이트 처리, .do 확장자 추가
public ResponseEntity<QuestionResponse> updateQuestion(@PathVariable UUID questionId, @RequestBody QuestionUpdateRequest request) { public ResponseEntity<QuestionResponse> updateQuestion(@PathVariable UUID questionId, @RequestBody QuestionUpdateRequest request) {
try { try {
QuestionResponse updatedQuestion = QuestionResponse.from( QuestionResponse updatedQuestion = QuestionResponse.from(
@ -125,13 +127,14 @@ public class QuestionController {
} }
/** /**
* ID . (HTTP DELETE) * ID . (HTTP POST)
* DELETE POST , URL delete .
* *
* @param questionId ID * @param questionId ID
* @return HTTP 204 No Content * @return HTTP 204 No Content
* HTTP 404 Not Found * HTTP 404 Not Found
*/ */
@DeleteMapping("/{questionId}") @PostMapping("/{questionId}/deleteQuestion.do") // POST 요청으로 삭제 처리, .do 확장자 추가
public ResponseEntity<Void> deleteQuestion(@PathVariable UUID questionId) { public ResponseEntity<Void> deleteQuestion(@PathVariable UUID questionId) {
try { try {
questionService.deleteQuestion(questionId); questionService.deleteQuestion(questionId);

View File

@ -17,6 +17,7 @@ import java.util.stream.Collectors;
/** /**
* (QuestionOption) HTTP REST . * (QuestionOption) HTTP REST .
* , , , API . * , , , API .
* .do , GET POST .
*/ */
@RestController @RestController
@RequestMapping("/api/question-options") @RequestMapping("/api/question-options")
@ -31,7 +32,7 @@ public class QuestionOptionController {
* @param request QuestionOption DTO * @param request QuestionOption DTO
* @return (HTTP 201 Created) * @return (HTTP 201 Created)
*/ */
@PostMapping @PostMapping("/createQuestionOption.do") // POST 요청 처리, .do 확장자 추가
public ResponseEntity<QuestionOptionResponse> createQuestionOption(@RequestBody QuestionOptionCreateRequest request) { public ResponseEntity<QuestionOptionResponse> createQuestionOption(@RequestBody QuestionOptionCreateRequest request) {
QuestionOptionResponse createdOption = QuestionOptionResponse.from( QuestionOptionResponse createdOption = QuestionOptionResponse.from(
questionOptionService.createQuestionOption( questionOptionService.createQuestionOption(
@ -49,7 +50,7 @@ public class QuestionOptionController {
* @param questionId ID * @param questionId ID
* @return (HTTP 200 OK) * @return (HTTP 200 OK)
*/ */
@GetMapping("/by-question/{questionId}") @GetMapping("/by-question/{questionId}/getOptions.do") // GET 요청 처리, .do 확장자 추가
public ResponseEntity<List<QuestionOptionResponse>> getOptionsByQuestionId(@PathVariable UUID questionId) { public ResponseEntity<List<QuestionOptionResponse>> getOptionsByQuestionId(@PathVariable UUID questionId) {
List<QuestionOptionResponse> options = questionOptionService.getOptionsByQuestionId(questionId).stream() List<QuestionOptionResponse> options = questionOptionService.getOptionsByQuestionId(questionId).stream()
.map(QuestionOptionResponse::from) .map(QuestionOptionResponse::from)
@ -64,7 +65,7 @@ public class QuestionOptionController {
* @return (HTTP 200 OK) * @return (HTTP 200 OK)
* HTTP 404 Not Found * HTTP 404 Not Found
*/ */
@GetMapping("/{optionId}") @GetMapping("/{optionId}/getOption.do") // GET 요청 처리, .do 확장자 추가
public ResponseEntity<QuestionOptionResponse> getQuestionOptionById(@PathVariable UUID optionId) { public ResponseEntity<QuestionOptionResponse> getQuestionOptionById(@PathVariable UUID optionId) {
try { try {
QuestionOptionResponse option = QuestionOptionResponse.from(questionOptionService.getQuestionOptionById(optionId)); QuestionOptionResponse option = QuestionOptionResponse.from(questionOptionService.getQuestionOptionById(optionId));
@ -75,14 +76,15 @@ public class QuestionOptionController {
} }
/** /**
* . (HTTP PUT) * . (HTTP POST)
* PUT POST , URL update .
* *
* @param optionId ID * @param optionId ID
* @param request QuestionOption DTO * @param request QuestionOption DTO
* @return (HTTP 200 OK) * @return (HTTP 200 OK)
* HTTP 404 Not Found * HTTP 404 Not Found
*/ */
@PutMapping("/{optionId}") @PostMapping("/{optionId}/updateOption.do") // POST 요청으로 업데이트 처리, .do 확장자 추가
public ResponseEntity<QuestionOptionResponse> updateQuestionOption(@PathVariable UUID optionId, @RequestBody QuestionOptionUpdateRequest request) { public ResponseEntity<QuestionOptionResponse> updateQuestionOption(@PathVariable UUID optionId, @RequestBody QuestionOptionUpdateRequest request) {
try { try {
QuestionOptionResponse updatedOption = QuestionOptionResponse.from( QuestionOptionResponse updatedOption = QuestionOptionResponse.from(
@ -99,13 +101,14 @@ public class QuestionOptionController {
} }
/** /**
* ID . (HTTP DELETE) * ID . (HTTP POST)
* DELETE POST , URL delete .
* *
* @param optionId ID * @param optionId ID
* @return HTTP 204 No Content * @return HTTP 204 No Content
* HTTP 404 Not Found * HTTP 404 Not Found
*/ */
@DeleteMapping("/{optionId}") @PostMapping("/{optionId}/deleteOption.do") // POST 요청으로 삭제 처리, .do 확장자 추가
public ResponseEntity<Void> deleteQuestionOption(@PathVariable UUID optionId) { public ResponseEntity<Void> deleteQuestionOption(@PathVariable UUID optionId) {
try { try {
questionOptionService.deleteQuestionOption(optionId); questionOptionService.deleteQuestionOption(optionId);

View File

@ -9,6 +9,8 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.UUID; import java.util.UUID;
@ -33,19 +35,26 @@ public class SurveyController {
* @param request Survey DTO * @param request Survey DTO
* @return (HTTP 201 Created) * @return (HTTP 201 Created)
*/ */
@PostMapping("/createSurvey.do") // POST 요청 처리, .do 확장자 추가 @PostMapping("/createSurvey.do")
public ResponseEntity<SurveyResponse> createSurvey(@RequestBody SurveyCreateRequest request) { public ResponseEntity<SurveyResponse> createSurvey(@RequestBody SurveyCreateRequest request) {
// 서비스 계층의 createSurvey 메서드를 호출하여 설문을 생성합니다.
// DTO의 정보를 서비스 메서드의 파라미터로 전달합니다. LocalDateTime startDate = null;
if (request.getStartDate() != null && !request.getStartDate().isEmpty()) {
startDate = LocalDateTime.parse(request.getStartDate(), DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"));
}
LocalDateTime endDate = null;
if (request.getEndDate() != null && !request.getEndDate().isEmpty()) {
endDate = LocalDateTime.parse(request.getEndDate(), DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"));
}
SurveyResponse createdSurvey = SurveyResponse.from( SurveyResponse createdSurvey = SurveyResponse.from(
surveyService.createSurvey( surveyService.createSurvey(
request.getSurveyTitle(), request.getSurveyTitle(),
request.getSurveyDescription(), // 수정: surveydescription() -> getSurveyDescription() request.getSurveyDescription(),
request.getStartDate(), startDate, // 파싱된 LocalDateTime 객체 전달
request.getEndDate() endDate // 파싱된 LocalDateTime 객체 전달
) )
); );
// 생성 성공 시 201 Created 상태 코드와 함께 생성된 설문 정보를 반환합니다.
return new ResponseEntity<>(createdSurvey, HttpStatus.CREATED); return new ResponseEntity<>(createdSurvey, HttpStatus.CREATED);
} }

View File

@ -18,6 +18,8 @@ import java.util.UUID;
public class SurveyCreateRequest { public class SurveyCreateRequest {
private String surveyTitle; private String surveyTitle;
private String surveyDescription; private String surveyDescription;
private LocalDateTime startDate; //private LocalDateTime startDate;
private LocalDateTime endDate; //private LocalDateTime endDate;
private String startDate;
private String endDate;
} }

View File

@ -2,16 +2,21 @@ package sgis.surveysystem.mapper;
import sgis.surveysystem.domain.Question; import sgis.surveysystem.domain.Question;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional; // Optional은 더 이상 findQuestionById의 반환 타입으로 사용하지 않음
import java.util.UUID; import java.util.UUID;
@Mapper @Mapper
public interface QuestionMapper { public interface QuestionMapper {
void insertQuestion(Question question); void insertQuestion(Question question);
List<Question> findQuestionsBySurveyId(UUID surveyId); List<Question> findQuestionsBySurveyId(UUID surveyId);
Optional<Question> findQuestionById(UUID questionId);
Question findQuestionById(UUID questionId);
void updateQuestion(Question question); void updateQuestion(Question question);
void deleteQuestion(UUID questionId); void deleteQuestion(UUID questionId);
} }

View File

@ -2,16 +2,20 @@ package sgis.surveysystem.mapper;
import sgis.surveysystem.domain.QuestionOption; import sgis.surveysystem.domain.QuestionOption;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; // @Param 사용 시 필요
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional; // Optional은 더 이상 findQuestionOptionById의 반환 타입으로 사용하지 않음
import java.util.UUID; import java.util.UUID;
@Mapper @Mapper
public interface QuestionOptionMapper { public interface QuestionOptionMapper {
void insertQuestionOption(QuestionOption option); void insertQuestionOption(QuestionOption option);
List<QuestionOption> findOptionsByQuestionId(UUID questionId); List<QuestionOption> findOptionsByQuestionId(UUID questionId);
Optional<QuestionOption> findQuestionOptionById(UUID optionId); QuestionOption findQuestionOptionById(UUID optionId);
void updateQuestionOption(QuestionOption option); void updateQuestionOption(QuestionOption option);
void deleteQuestionOption(UUID optionId); void deleteQuestionOption(UUID optionId);
} }

View File

@ -5,15 +5,21 @@ import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional; // Optional은 더 이상 findSurveyById의 반환 타입으로 사용하지 않음
import java.util.UUID; import java.util.UUID;
@Mapper @Mapper // MyBatis Mapper 임을 선언
public interface SurveyMapper { public interface SurveyMapper {
void insertSurvey(Survey survey); void insertSurvey(Survey survey); // insert
List<Survey> findAllSurveys();
Optional<Survey> findSurveyById(UUID surveyId); List<Survey> findAllSurveys(); // select All
void updateSurvey(Survey survey);
void deleteSurvey(UUID surveyId); // findSurveyById 메서드의 반환 타입을 Optional<Survey>에서 Survey로 변경
void updateSurveyStatus(@Param("surveyId") UUID surveyId, @Param("isActive") Boolean isActive); Survey findSurveyById(UUID surveyId); // select by ID (Optional 대신 직접 Survey 반환)
void updateSurvey(Survey survey); // update
void deleteSurvey(UUID surveyId); // delete by ID
void updateSurveyStatus(@Param("surveyId") UUID surveyId, @Param("isActive") Boolean isActive); // 특정 필드 업데이트
} }

View File

@ -8,6 +8,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.Optional; // Optional 클래스 임포트
import java.util.UUID; import java.util.UUID;
/** /**
@ -71,7 +72,8 @@ public class QuestionOptionService {
*/ */
@Transactional(readOnly = true) @Transactional(readOnly = true)
public QuestionOption getQuestionOptionById(UUID optionId) { public QuestionOption getQuestionOptionById(UUID optionId) {
return questionOptionMapper.findQuestionOptionById(optionId) // Mapper를 통해 ID로 선택지 조회 // Mapper에서 직접 QuestionOption 객체를 반환하도록 변경했으므로, Optional.ofNullable()을 사용합니다.
return Optional.ofNullable(questionOptionMapper.findQuestionOptionById(optionId))
.orElseThrow(() -> new NoSuchElementException("Question Option not found with ID: " + optionId)); // 없을 경우 예외 발생 .orElseThrow(() -> new NoSuchElementException("Question Option not found with ID: " + optionId)); // 없을 경우 예외 발생
} }

View File

@ -8,6 +8,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.Optional; // Optional 클래스 임포트
import java.util.UUID; import java.util.UUID;
/** /**
@ -74,7 +75,8 @@ public class QuestionService {
*/ */
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Question getQuestionById(UUID questionId) { public Question getQuestionById(UUID questionId) {
return questionMapper.findQuestionById(questionId) // Mapper를 통해 ID로 문항 조회 // Mapper에서 직접 Question 객체를 반환하도록 변경했으므로, Optional.ofNullable()을 사용합니다.
return Optional.ofNullable(questionMapper.findQuestionById(questionId))
.orElseThrow(() -> new NoSuchElementException("Question not found with ID: " + questionId)); // 없을 경우 예외 발생 .orElseThrow(() -> new NoSuchElementException("Question not found with ID: " + questionId)); // 없을 경우 예외 발생
} }

View File

@ -9,6 +9,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.Optional; // Optional 클래스 임포트
import java.util.UUID; import java.util.UUID;
/** /**
@ -70,7 +71,8 @@ public class SurveyService {
*/ */
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Survey getSurveyById(UUID surveyId) { public Survey getSurveyById(UUID surveyId) {
return surveyMapper.findSurveyById(surveyId) // Mapper를 통해 ID로 설문 조회 // Mapper에서 직접 Survey 객체를 반환하도록 변경했으므로, Optional.ofNullable()을 사용합니다.
return Optional.ofNullable(surveyMapper.findSurveyById(surveyId))
.orElseThrow(() -> new NoSuchElementException("Survey not found with ID: " + surveyId)); // 없을 경우 예외 발생 .orElseThrow(() -> new NoSuchElementException("Survey not found with ID: " + surveyId)); // 없을 경우 예외 발생
} }

View File

@ -141,6 +141,7 @@
</if> </if>
<![CDATA[ <![CDATA[
ORDER BY A.PROJECT_CODE desc ORDER BY A.PROJECT_CODE desc
LIMIT 1
]]> ]]>
</select> </select>

View File

@ -21,8 +21,8 @@
question_order, question_order,
is_required is_required
) VALUES ( ) VALUES (
#{questionId, jdbcType=OTHER}, CAST(#{questionId, jdbcType=OTHER} AS uuid), <!-- 이 부분을 수정했습니다. -->
#{surveyId, jdbcType=OTHER}, CAST(#{surveyId, jdbcType=OTHER} AS uuid), <!-- 이 부분을 수정했습니다. -->
#{questionText}, #{questionText},
#{questionType}, #{questionType},
#{questionOrder}, #{questionOrder},
@ -30,7 +30,6 @@
) )
</insert> </insert>
<!-- 설문 ID로 문항 목록 조회 -->
<select id="findQuestionsBySurveyId" resultMap="questionResultMap"> <select id="findQuestionsBySurveyId" resultMap="questionResultMap">
SELECT SELECT
question_id, question_id,
@ -40,11 +39,10 @@
question_order, question_order,
is_required is_required
FROM tb_questions FROM tb_questions
WHERE survey_id = #{surveyId, jdbcType=OTHER} WHERE survey_id = CAST(#{surveyId, jdbcType=OTHER} AS uuid)
ORDER BY question_order ASC ORDER BY question_order ASC
</select> </select>
<!-- ID로 특정 문항 조회 -->
<select id="findQuestionById" resultMap="questionResultMap"> <select id="findQuestionById" resultMap="questionResultMap">
SELECT SELECT
question_id, question_id,
@ -54,10 +52,9 @@
question_order, question_order,
is_required is_required
FROM tb_questions FROM tb_questions
WHERE question_id = #{questionId, jdbcType=OTHER} WHERE question_id = CAST(#{questionId, jdbcType=OTHER} AS uuid)
</select> </select>
<!-- 문항 업데이트 -->
<update id="updateQuestion" parameterType="sgis.surveysystem.domain.Question"> <update id="updateQuestion" parameterType="sgis.surveysystem.domain.Question">
UPDATE tb_questions UPDATE tb_questions
SET SET
@ -65,12 +62,11 @@
question_type = #{questionType}, question_type = #{questionType},
question_order = #{questionOrder}, question_order = #{questionOrder},
is_required = #{isRequired} is_required = #{isRequired}
WHERE question_id = #{questionId, jdbcType=OTHER} WHERE question_id = CAST(#{questionId, jdbcType=OTHER} AS uuid)
</update> </update>
<!-- 문항 삭제 -->
<delete id="deleteQuestion"> <delete id="deleteQuestion">
DELETE FROM tb_questions DELETE FROM tb_questions
WHERE question_id = #{questionId, jdbcType=OTHER} WHERE question_id = CAST(#{questionId, jdbcType=OTHER} AS uuid)
</delete> </delete>
</mapper> </mapper>

View File

@ -9,6 +9,7 @@
<result property="optionOrder" column="option_order"/> <result property="optionOrder" column="option_order"/>
</resultMap> </resultMap>
<!-- 선택지 삽입 -->
<insert id="insertQuestionOption" parameterType="sgis.surveysystem.domain.QuestionOption"> <insert id="insertQuestionOption" parameterType="sgis.surveysystem.domain.QuestionOption">
INSERT INTO tb_question_options ( INSERT INTO tb_question_options (
option_id, option_id,
@ -16,8 +17,8 @@
option_text, option_text,
option_order option_order
) VALUES ( ) VALUES (
#{optionId, jdbcType=OTHER}, CAST(#{optionId, jdbcType=OTHER} AS uuid), <!-- 이 부분을 수정했습니다. -->
#{questionId, jdbcType=OTHER}, CAST(#{questionId, jdbcType=OTHER} AS uuid), <!-- 이 부분을 수정했습니다. -->
#{optionText}, #{optionText},
#{optionOrder} #{optionOrder}
) )
@ -30,7 +31,7 @@
option_text, option_text,
option_order option_order
FROM tb_question_options FROM tb_question_options
WHERE question_id = #{questionId, jdbcType=OTHER} WHERE question_id = CAST(#{questionId, jdbcType=OTHER} AS uuid)
ORDER BY option_order ASC ORDER BY option_order ASC
</select> </select>
@ -41,7 +42,7 @@
option_text, option_text,
option_order option_order
FROM tb_question_options FROM tb_question_options
WHERE option_id = #{optionId, jdbcType=OTHER} WHERE option_id = CAST(#{optionId, jdbcType=OTHER} AS uuid)
</select> </select>
<update id="updateQuestionOption" parameterType="sgis.surveysystem.domain.QuestionOption"> <update id="updateQuestionOption" parameterType="sgis.surveysystem.domain.QuestionOption">
@ -49,11 +50,11 @@
SET SET
option_text = #{optionText}, option_text = #{optionText},
option_order = #{optionOrder} option_order = #{optionOrder}
WHERE option_id = #{optionId, jdbcType=OTHER} WHERE option_id = CAST(#{optionId, jdbcType=OTHER} AS uuid)
</update> </update>
<delete id="deleteQuestionOption"> <delete id="deleteQuestionOption">
DELETE FROM tb_question_options DELETE FROM tb_question_options
WHERE option_id = #{optionId, jdbcType=OTHER} WHERE option_id = CAST(#{optionId, jdbcType=OTHER} AS uuid)
</delete> </delete>
</mapper> </mapper>

View File

@ -24,7 +24,8 @@
start_date, start_date,
end_date end_date
) VALUES ( ) VALUES (
#{surveyId, jdbcType=OTHER}, #{surveyTitle}, CAST(#{surveyId, jdbcType=OTHER} AS uuid),
#{surveyTitle},
#{surveyDescription}, #{surveyDescription},
#{createdAt}, #{createdAt},
#{updatedAt}, #{updatedAt},
@ -58,7 +59,7 @@
start_date, start_date,
end_date end_date
FROM tb_surveys FROM tb_surveys
WHERE survey_id = #{surveyId, jdbcType=OTHER} WHERE survey_id = CAST(#{surveyId, jdbcType=OTHER} AS uuid) <!-- 이 부분을 수정했습니다. -->
</select> </select>
<update id="updateSurvey" parameterType="sgis.surveysystem.domain.Survey"> <update id="updateSurvey" parameterType="sgis.surveysystem.domain.Survey">
@ -70,7 +71,7 @@
is_active = #{isActive}, is_active = #{isActive},
start_date = #{startDate}, start_date = #{startDate},
end_date = #{endDate} end_date = #{endDate}
WHERE survey_id = #{surveyId, jdbcType=OTHER} WHERE survey_id = CAST(#{surveyId, jdbcType=OTHER} AS uuid) <!-- 이 부분도 일관성을 위해 수정합니다. -->
</update> </update>
<update id="updateSurveyStatus"> <update id="updateSurveyStatus">
@ -78,11 +79,11 @@
SET SET
is_active = #{isActive}, is_active = #{isActive},
updated_at = NOW() updated_at = NOW()
WHERE survey_id = #{surveyId, jdbcType=OTHER} WHERE survey_id = CAST(#{surveyId, jdbcType=OTHER} AS uuid) <!-- 이 부분도 일관성을 위해 수정합니다. -->
</update> </update>
<delete id="deleteSurvey"> <delete id="deleteSurvey">
DELETE FROM tb_surveys DELETE FROM tb_surveys
WHERE survey_id = #{surveyId, jdbcType=OTHER} WHERE survey_id = CAST(#{surveyId, jdbcType=OTHER} AS uuid) <!-- 이 부분도 일관성을 위해 수정합니다. -->
</delete> </delete>
</mapper> </mapper>

View File

@ -4,19 +4,21 @@
xmlns:context="http://www.springframework.org/schema/context" xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:task="http://www.springframework.org/schema/task" xmlns:task="http://www.springframework.org/schema/task"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.0.xsd"> http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
<!-- 패키지 내 Controller, Service, Repository 클래스의 auto detect를 위한 mvc 설정 --> <!-- 패키지 내 Controller, Service, Repository 클래스의 auto detect를 위한 mvc 설정 -->
<context:component-scan base-package="egovframework,sgis"> <context:component-scan base-package="egovframework,sgis">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/> <context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/>
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/> <context:include-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
</context:component-scan> </context:component-scan>
<!-- 서블릿컨이너상의 exception에 대한 오류 페이지를 연결하는 mvc 설정--> <!-- 서블릿컨이너상의 exception에 대한 오류 페이지를 연결하는 mvc 설정-->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"> <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="defaultErrorView" value="sgis/com/error/error"/> <property name="defaultErrorView" value="sgis/com/error/error"/>
<property name="exceptionMappings"> <property name="exceptionMappings">
@ -39,18 +41,17 @@
<!-- 로그인 체크가 필요한 URL과 로그인 여부를 체크해준다 --> <!-- 로그인 체크가 필요한 URL과 로그인 여부를 체크해준다 -->
<mvc:interceptors> <mvc:interceptors>
<mvc:interceptor> <mvc:interceptor>
<mvc:mapping path="/com/loginAction.do"/><!-- 로그인 --> <mvc:mapping path="/com/loginAction.do"/>
<mvc:mapping path="/com/logoutAction.do"/><!-- 로그아웃 --> <mvc:mapping path="/com/logoutAction.do"/>
<mvc:exclude-mapping path="/com/loginForm.do"/> <mvc:exclude-mapping path="/com/loginForm.do"/>
<bean class="egovframework.com.cmm.interceptor.LoginInterceptor" /> <bean class="egovframework.com.cmm.interceptor.LoginInterceptor" />
</mvc:interceptor> </mvc:interceptor>
<mvc:interceptor> <mvc:interceptor>
<mvc:mapping path="/sgis/portal/portalMain.do"/> <mvc:mapping path="/sgis/portal/portalMain.do"/>
<mvc:mapping path="/com/*.do"/> <!-- 공통 --> <mvc:mapping path="/com/*.do"/>
<mvc:mapping path="/app/*/*.do"/> <!-- 입력시스템 --> <mvc:mapping path="/app/*/*.do"/>
<mvc:mapping path="/map/*.do"/> <!-- 지도시스템 --> <mvc:mapping path="/map/*.do"/>
<!-- <bean class="egovframework.com.cmm.interceptor.noLoginInterceptor" /> -->
<bean class="egovframework.com.cmm.interceptor.AuthenticInterceptor" /> <bean class="egovframework.com.cmm.interceptor.AuthenticInterceptor" />
</mvc:interceptor> </mvc:interceptor>
</mvc:interceptors> </mvc:interceptors>
@ -58,6 +59,41 @@
<!-- Annotation 을 사용하지 않는 경우에 대한 MVC 처리 설정 --> <!-- Annotation 을 사용하지 않는 경우에 대한 MVC 처리 설정 -->
<mvc:view-controller path="/cmmn/validator.do" view-name="cmmn/validator"/> <mvc:view-controller path="/cmmn/validator.do" view-name="cmmn/validator"/>
<mvc:annotation-driven/> <!-- ObjectMapper 빈을 전역적으로 정의 -->
<bean id="objectMapper" class="com.fasterxml.jackson.databind.ObjectMapper">
<!-- 기본 생성자 사용 -->
<!-- 날짜/시간 포맷팅 설정 (선택 사항이지만 권장) -->
<property name="serializationInclusion" value="NON_NULL"/>
<property name="dateFormat">
<bean class="com.fasterxml.jackson.databind.util.StdDateFormat"/>
</property>
</bean>
<!-- ObjectMapper에 JavaTimeModule 등록을 위한 MethodInvokingFactoryBean -->
<!-- objectMapper 빈이 생성된 후 registerModule 메서드를 호출하여 모듈을 등록합니다. -->
<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetObject" ref="objectMapper"/>
<property name="targetMethod" value="registerModule"/>
<property name="arguments">
<list>
<bean class="com.fasterxml.jackson.datatype.jsr310.JavaTimeModule"/>
</list>
</property>
</bean>
<!-- mvc:annotation-driven을 명시적으로 재정의하여 메시지 컨버터 설정 -->
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>application/json;charset=UTF-8</value>
</list>
</property>
<!-- 전역으로 정의된 objectMapper 빈을 참조 -->
<property name="objectMapper" ref="objectMapper"/>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
</beans> </beans>

View File

@ -216,6 +216,7 @@ function selectComboList(class_name, data, selected_data){
} }
function go_save(){ function go_save(){
debugger;
<c:if test="${empty fileInfo}"> <c:if test="${empty fileInfo}">
var upload = $("#fileUpload1").data("kendoUpload"); var upload = $("#fileUpload1").data("kendoUpload");
var upCnt = upload.getFiles().length var upCnt = upload.getFiles().length

View File

@ -888,7 +888,7 @@
</div> </div>
<div class="title"> <div class="title">
지반주상도<span class="borehole-coordinate-system-info" id="borehole-coordinate-system-info" ></span> 시추주상도<span class="borehole-coordinate-system-info" id="borehole-coordinate-system-info" ></span>
</div> </div>
<table class="header" id="meta-data"> <table class="header" id="meta-data">
<thead> <thead>

View File

@ -186,7 +186,7 @@
<ul> <ul>
<li class="option-mouse"><span class="option-title">이동</span>마우스 왼쪽 드래그, <span class="option-title">확대</span>L-Shift + 마우스 왼쪽 드래그</li> <li class="option-mouse"><span class="option-title">이동</span>마우스 왼쪽 드래그, <span class="option-title">확대</span>L-Shift + 마우스 왼쪽 드래그</li>
<li class="option-ex ex-sample">시료를 채취한 부분으로 실내시험 결과를 확인 할 수 있습니다.</li> <li class="option-ex ex-sample">시료를 채취한 부분으로 실내시험 결과를 확인 할 수 있습니다.</li>
<li class="option-ex ex-jusang">지반주상도 정보를 확인 하실 수 있습니다.</li> <li class="option-ex ex-jusang">시추주상도 정보를 확인 하실 수 있습니다.</li>
<li class="option-ex ex-info">지반단면도에 대한 XML 데이터를 보실 수 있습니다.</li> <li class="option-ex ex-info">지반단면도에 대한 XML 데이터를 보실 수 있습니다.</li>
</ul> </ul>
</div> </div>

View File

@ -381,7 +381,7 @@
</li> </li>
<li style="width: 400px;"> <li style="width: 400px;">
<a href="javascript:showSichudan();" id="borehole-profile" class="btn btn-ske-blue btn-icon-left btn-icon-search" autocomplete="off" ><span>지반단면도</span></a> <a href="javascript:showSichudan();" id="borehole-profile" class="btn btn-ske-blue btn-icon-left btn-icon-search" autocomplete="off" ><span>지반단면도</span></a>
<a href="javascript:showBoreholeLog();" id="borehole-log" class="btn btn-ske-blue btn-icon-left btn-icon-search" autocomplete="off" ><span>지반주상도</span></a> <a href="javascript:showBoreholeLog();" id="borehole-log" class="btn btn-ske-blue btn-icon-left btn-icon-search" autocomplete="off" ><span>시추주상도</span></a>
<script> <script>
function showSichudan() { function showSichudan() {

View File

@ -0,0 +1,441 @@
<%@ 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"%>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>설문 문항 생성 (관리자)</title>
<style>
body {
font-family: 'Malgun Gothic', '맑은 고딕', Arial, sans-serif;
margin: 20px;
background-color: #f4f4f4;
color: #333;
display: flex;
flex-direction: column; /* 세로 정렬 */
justify-content: center;
align-items: center;
min-height: 100vh;
}
.header-section {
background-color: #fff;
padding: 20px 25px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
max-width: 600px;
width: 100%;
box-sizing: border-box;
margin-bottom: 20px; /* 폼과의 간격 */
}
.container {
background-color: #fff;
padding: 25px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
max-width: 600px;
width: 100%;
box-sizing: border-box;
}
h2 {
color: #0056b3;
text-align: center;
margin-bottom: 25px;
font-size: 1.8em;
border-bottom: 2px solid #eee;
padding-bottom: 10px;
}
.form-group {
margin-bottom: 18px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: bold;
color: #555;
}
.form-group input[type="text"],
.form-group input[type="number"],
.form-group select,
.form-group textarea {
width: 100%;
padding: 12px;
border: 1px solid #ccc;
border-radius: 5px;
box-sizing: border-box;
font-size: 1em;
transition: border-color 0.3s ease;
}
.form-group input[type="text"]:focus,
.form-group input[type="number"]:focus,
.form-group select:focus,
.form-group input[type="datetime-local"]:focus,
.form-group textarea:focus {
border-color: #007bff;
outline: none;
}
.form-group textarea {
resize: vertical;
min-height: 80px;
}
.form-group input[type="checkbox"] {
margin-right: 5px;
}
button {
background-color: #28a745; /* Green for create */
color: white;
padding: 12px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 1.1em;
width: 100%;
transition: background-color 0.3s ease, transform 0.2s ease;
margin-top: 10px;
}
button:hover {
background-color: #218838;
transform: translateY(-2px);
}
button:active {
transform: translateY(0);
}
.message {
margin-top: 25px;
padding: 12px;
border-radius: 5px;
text-align: center;
font-weight: bold;
display: none;
word-break: break-all;
}
.message.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.back-link {
display: block;
text-align: center;
margin-top: 20px;
color: #007bff;
text-decoration: none;
font-weight: bold;
}
.back-link:hover {
text-decoration: underline;
}
/* 기존 문항 목록 스타일 */
.question-list-section {
margin-top: 30px;
background-color: #fff;
padding: 25px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
max-width: 600px;
width: 100%;
box-sizing: border-box;
margin-bottom: 20px;
}
.question-list-section h3 {
color: #0056b3;
margin-bottom: 15px;
border-bottom: 1px dashed #eee;
padding-bottom: 8px;
}
.question-item {
border: 1px solid #e0e0e0;
border-radius: 5px;
padding: 15px;
margin-bottom: 10px;
background-color: #f9f9f9;
}
.question-item p {
margin: 0 0 5px 0;
font-weight: bold;
color: #444;
}
.question-item small {
color: #777;
display: block;
margin-bottom: 8px;
}
.question-options {
margin-top: 10px;
padding-left: 20px;
border-left: 2px solid #ddd;
}
.question-options li {
margin-bottom: 5px;
color: #666;
}
.question-options li:last-child {
margin-bottom: 0;
}
.no-questions {
text-align: center;
color: #888;
padding: 20px;
}
</style>
</head>
<body>
<div class="header-section">
<h2>설문 ID: <span id="displaySurveyId">${surveyId}</span></h2>
<p>이 설문에 대한 문항들을 생성합니다.</p>
<div id="initialMessage" class="message"></div>
</div>
<div class="question-list-section">
<h3>기존 문항 목록</h3>
<div id="existingQuestionsList">
<p class="no-questions">로딩 중...</p>
</div>
</div>
<div class="container">
<h2>새 문항 생성</h2>
<form id="createQuestionForm">
<!-- Hidden input to pass surveyId from URL to form data -->
<input type="hidden" id="surveyId" name="surveyId" value="${surveyId}">
<div class="form-group">
<label for="questionText">문항 내용:</label>
<textarea id="questionText" name="questionText" placeholder="예: 시스템 사용에 만족하십니까?" required></textarea>
</div>
<div class="form-group">
<label for="questionType">문항 유형:</label>
<select id="questionType" name="questionType" required>
<option value="">-- 선택 --</option>
<option value="단답형">단답형</option>
<option value="객관식_단일">객관식 (단일 선택)</option>
<option value="객관식_다중">객관식 (다중 선택)</option>
<option value="주관식">주관식</option>
</select>
</div>
<div class="form-group">
<label for="questionOrder">문항 순서:</label>
<input type="number" id="questionOrder" name="questionOrder" value="1" min="1" required>
</div>
<div class="form-group">
<input type="checkbox" id="isRequired" name="isRequired" checked>
<label for="isRequired">필수 응답</label>
</div>
<button type="submit">문항 생성</button>
</form>
<div id="message" class="message"></div>
<a href="#" class="back-link" onclick="history.back(); return false;">&larr; 이전 페이지로 돌아가기</a>
</div>
<script>
const contextPath = "<%= request.getContextPath() %>";
const surveyId = document.getElementById('surveyId').value;
const initialMessageDiv = document.getElementById('initialMessage');
const existingQuestionsListDiv = document.getElementById('existingQuestionsList');
// Function to fetch and display existing questions
async function fetchAndDisplayQuestions() {
console.log('fetchAndDisplayQuestions called for surveyId:', surveyId); // Debugging
if (!surveyId || surveyId === 'null' || surveyId === '') {
existingQuestionsListDiv.innerHTML = '<p class="no-questions">설문 ID가 유효하지 않아 기존 문항을 불러올 수 없습니다.</p>';
console.log('Invalid surveyId, not fetching questions.'); // Debugging
return;
}
const apiUrl = (typeof contextPath !== 'undefined' && contextPath !== null && contextPath !== '') ?
contextPath + '/api/questions/by-survey/' + surveyId + '/getQuestions.do' :
'/api/questions/by-survey/' + surveyId + '/getQuestions.do';
console.log('Fetching questions from:', apiUrl); // Debugging
try {
const response = await fetch(apiUrl);
console.log('Questions API Response Status:', response.status, response.statusText); // Debugging
if (response.ok) {
const questions = await response.json();
console.log('Fetched Questions (RAW):', JSON.stringify(questions, null, 2)); // Debugging: Raw JSON data
let maxOrder = 0; // 최대 문항 순서를 추적하기 위한 변수
if (questions.length === 0) {
existingQuestionsListDiv.innerHTML = '<p class="no-questions">아직 생성된 문항이 없습니다.</p>';
maxOrder = 0; // 문항이 없으면 최대 순서는 0
} else {
existingQuestionsListDiv.innerHTML = ''; // Clear loading message
for (const question of questions) {
// 현재 문항의 순서가 maxOrder보다 크면 업데이트
if (question.questionOrder > maxOrder) {
maxOrder = question.questionOrder;
}
console.log('Processing question:', JSON.stringify(question, null, 2)); // Debugging: Each question object
const questionItem = document.createElement('div');
questionItem.classList.add('question-item');
// 문항 클릭 시 선택지 생성 페이지로 이동하는 이벤트 리스너 추가
questionItem.style.cursor = 'pointer'; // 클릭 가능한 UI임을 나타내기 위해 커서 스타일 변경
questionItem.addEventListener('click', function() {
const redirectUrl = (typeof contextPath !== 'undefined' && contextPath !== null && contextPath !== '') ?
contextPath + '/admin/survey/question/' + question.questionId + '/createOptionForm.do' :
'/admin/survey/question/' + question.questionId + '/createOptionForm.do';
window.location.href = redirectUrl;
});
let optionsHtml = '';
// questionType이 객관식이고, question.options 배열이 존재하는 경우에만 옵션 처리
if (question.questionType && question.questionType.startsWith('객관식') && question.options && question.options.length > 0) {
optionsHtml = '<ul class="question-options">';
question.options.forEach(option => {
// 옵션 데이터도 안전하게 접근
const optionOrder = option.optionOrder !== null && option.optionOrder !== undefined ? option.optionOrder : '';
const optionText = option.optionText !== null && option.optionText !== undefined ? option.optionText : '';
// 백틱(`) 대신 문자열 연결(+) 사용
optionsHtml += '<li>' + optionOrder + '. ' + optionText + '</li>';
});
optionsHtml += '</ul>';
} else if (question.questionType && question.questionType.startsWith('객관식')) {
// 객관식 타입이지만 옵션이 없는 경우
optionsHtml = '<p class="question-options"><em>(선택지 없음)</em></p>';
}
// 주관식 등 옵션이 필요 없는 경우 optionsHtml은 빈 문자열로 유지됩니다.
// 안전하게 값 접근 및 플레이스홀더 설정
const order = String(question.questionOrder || '[순서 없음]');
const text = String(question.questionText || '[내용 없음]');
const type = String(question.questionType || '[유형 없음]');
// isRequired는 boolean 타입이므로, 명시적으로 '예'/'아니오'로 변환
const required = (question.isRequired === true) ? '예' : '아니오';
console.log('Final values for HTML:', { order, text, type, required }); // Add this line
// 백틱(`) 대신 문자열 연결(+) 사용
questionItem.innerHTML =
'<p>' + order + '. ' + text + '</p>' +
'<small>유형: ' + type + ' | 필수: ' + required + '</small>' +
optionsHtml;
existingQuestionsListDiv.appendChild(questionItem);
console.log('Appended question item:', questionItem.outerHTML); // Debugging: Check generated HTML
}
}
// 모든 문항 처리 후, 다음 문항 순서 설정
// 기존 문항이 없으면 1, 있으면 최대 순서 + 1
document.getElementById('questionOrder').value = maxOrder + 1;
} else {
existingQuestionsListDiv.innerHTML = '<p class="no-questions error">기존 문항을 불러오는데 실패했습니다.</p>';
console.error('Failed to fetch questions. Status:', response.status, response.statusText); // Debugging
// 오류 발생 시에도 기본값은 1로 설정 (혹은 기존 HTML 기본값 유지)
document.getElementById('questionOrder').value = 1;
}
} catch (error) {
console.error('Error fetching questions:', error);
existingQuestionsListDiv.innerHTML = '<p class="no-questions error">네트워크 오류로 문항을 불러올 수 없습니다.</p>';
// 네트워크 오류 발생 시에도 기본값은 1로 설정
document.getElementById('questionOrder').value = 1;
}
}
// --- Event Listener for Form Submission ---
document.getElementById('createQuestionForm').addEventListener('submit', async function(event) {
event.preventDefault(); // 폼의 기본 제출 동작 방지
const messageDiv = document.getElementById('message');
messageDiv.style.display = 'none'; // 이전 메시지 숨기기
messageDiv.classList.remove('success', 'error'); // 이전 스타일 클래스 제거
// 폼 필드에서 데이터 수집
const formData = {
surveyId: surveyId, // Hidden input에서 surveyId 가져오기
questionText: document.getElementById('questionText').value,
questionType: document.getElementById('questionType').value,
questionOrder: parseInt(document.getElementById('questionOrder').value, 10), // 숫자로 파싱
isRequired: document.getElementById('isRequired').checked // 체크박스 값
};
// 유효성 검사 (간단한 예시)
if (!formData.surveyId || formData.surveyId === 'null' || formData.surveyId === '') { // 빈 문자열도 확인
messageDiv.textContent = '설문 ID가 유효하지 않습니다. 이전 페이지에서 설문을 선택해주세요.';
messageDiv.classList.add('error');
messageDiv.style.display = 'block';
return;
}
if (!formData.questionType) {
messageDiv.textContent = '문항 유형을 선택해주세요.';
messageDiv.classList.add('error');
messageDiv.style.display = 'block';
return;
}
// QuestionController의 @RequestMapping("/api/questions")와 @PostMapping("/createQuestion.do")를 결합
const apiUrl = (typeof contextPath !== 'undefined' && contextPath !== null && contextPath !== '') ? contextPath + '/api/questions/createQuestion.do' : '/api/questions/createQuestion.do';
try {
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData) // JSON 문자열로 변환하여 전송
});
if (response.ok) { // HTTP 상태 코드가 200-299 범위인 경우 (성공)
const result = await response.json(); // 응답 본문을 JSON으로 파싱
messageDiv.textContent = '문항이 성공적으로 생성되었습니다! (ID: ' + result.questionId + ')';
messageDiv.classList.add('success');
messageDiv.style.display = 'block';
// 폼 초기화 (surveyId는 유지)
document.getElementById('questionText').value = '';
document.getElementById('questionType').value = '';
document.getElementById('questionOrder').value = parseInt(formData.questionOrder, 10) + 1; // 다음 문항 순서 자동 증가
document.getElementById('isRequired').checked = true;
// 문항 생성 성공 후 기존 문항 목록 새로고침
await fetchAndDisplayQuestions(); // 목록 새로고침
// 문항 유형이 객관식인 경우, 선택지 생성 페이지로 이동
if (formData.questionType.startsWith('객관식')) {
const redirectUrl = (typeof contextPath !== 'undefined' && contextPath !== null && contextPath !== '') ?
contextPath + '/admin/survey/question/' + result.questionId + '/createOptionForm.do' :
'/admin/survey/question/' + result.questionId + '/createOptionForm.do';
window.location.href = redirectUrl; // 페이지 리다이렉트
}
} else { // HTTP 상태 코드가 200-299 범위를 벗어난 경우 (실패)
const errorText = await response.text(); // 오류 메시지를 텍스트로 가져옴
messageDiv.textContent = '문항 생성 실패: ' + (errorText || '알 수 없는 오류');
messageDiv.classList.add('error');
messageDiv.style.display = 'block';
}
} catch (error) {
// 네트워크 오류 또는 fetch 요청 자체의 문제 발생 시
console.error('네트워크 오류:', error);
messageDiv.textContent = '네트워크 오류가 발생했습니다. 다시 시도해주세요.';
messageDiv.classList.add('error');
messageDiv.style.display = 'block';
}
});
// --- Initial Load Logic ---
// 초기 로드 시 surveyId가 없으면 경고 메시지 표시
if (!surveyId || surveyId === 'null' || surveyId === '') {
initialMessageDiv.textContent = '경고: 설문 ID가 없습니다. 문항을 생성할 설문을 먼저 선택해주세요.';
initialMessageDiv.classList.add('error');
initialMessageDiv.style.display = 'block';
existingQuestionsListDiv.innerHTML = '<p class="no-questions">설문 ID가 없어 문항 목록을 불러올 수 없습니다.</p>';
} else {
// surveyId가 유효하면 기존 문항 목록을 불러옵니다.
fetchAndDisplayQuestions();
}
</script>
</body>
</html>

View File

@ -0,0 +1,428 @@
<%@ 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"%>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>문항 선택지 생성 (관리자)</title>
<style>
body {
font-family: 'Malgun Gothic', '맑은 고딕', Arial, sans-serif;
margin: 20px;
background-color: #f4f4f4;
color: #333;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.header-section {
background-color: #fff;
padding: 20px 25px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
max-width: 600px;
width: 100%;
box-sizing: border-box;
margin-bottom: 20px;
}
.container {
background-color: #fff;
padding: 25px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
max-width: 600px;
width: 100%;
box-sizing: border-box;
}
h2 {
color: #0056b3;
text-align: center;
margin-bottom: 25px;
font-size: 1.8em;
border-bottom: 2px solid #eee;
padding-bottom: 10px;
}
.form-group {
margin-bottom: 18px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: bold;
color: #555;
}
.form-group input[type="text"],
.form-group input[type="number"],
.form-group select,
.form-group textarea {
width: 100%;
padding: 12px;
border: 1px solid #ccc;
border-radius: 5px;
box-sizing: border-box;
font-size: 1em;
transition: border-color 0.3s ease;
}
.form-group input[type="text"]:focus,
.form-group input[type="number"]:focus,
.form-group select:focus,
.form-group input[type="datetime-local"]:focus,
.form-group textarea:focus {
border-color: #007bff;
outline: none;
}
.form-group textarea {
resize: vertical;
min-height: 80px;
}
.form-group input[type="checkbox"] {
margin-right: 5px;
}
button {
background-color: #28a745;
color: white;
padding: 12px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 1.1em;
width: 100%;
transition: background-color 0.3s ease, transform 0.2s ease;
margin-top: 10px;
}
button:hover {
background-color: #218838;
transform: translateY(-2px);
}
button:active {
transform: translateY(0);
}
.message {
margin-top: 25px;
padding: 12px;
border-radius: 5px;
text-align: center;
font-weight: bold;
display: none;
word-break: break-all;
}
.message.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.back-link {
display: block;
text-align: center;
margin-top: 20px;
color: #007bff;
text-decoration: none;
font-weight: bold;
}
.back-link:hover {
text-decoration: underline;
}
/* Existing options list styles */
.option-list-section {
margin-top: 30px;
background-color: #fff;
padding: 25px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
max-width: 600px;
width: 100%;
box-sizing: border-box;
margin-bottom: 20px;
}
.option-list-section h3 {
color: #0056b3;
margin-bottom: 15px;
border-bottom: 1px dashed #eee;
padding-bottom: 8px;
}
.option-item {
border: 1px solid #e0e0e0;
border-radius: 5px;
padding: 15px;
margin-bottom: 10px;
background-color: #f9f9f9;
display: flex;
justify-content: space-between;
align-items: center;
}
.option-item p {
margin: 0;
font-weight: bold;
color: #444;
flex-grow: 1;
}
.option-item small {
color: #777;
display: block;
margin-bottom: 8px;
}
.no-options {
text-align: center;
color: #888;
padding: 20px;
}
.delete-option-btn {
background-color: #dc3545;
color: white;
padding: 5px 10px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.8em;
margin-left: 10px;
transition: background-color 0.3s ease;
width: auto;
}
.delete-option-btn:hover {
background-color: #c82333;
}
</style>
</head>
<body>
<div class="header-section">
<h2>문항 ID: <span id="displayQuestionId"><%= request.getAttribute("questionId") %></span></h2>
<p>이 문항에 대한 선택지들을 생성합니다.</p>
<div id="initialMessage" class="message"></div>
</div>
<div class="option-list-section">
<h3>기존 선택지 목록</h3>
<div id="existingOptionsList">
<p class="no-options">로딩 중...</p>
</div>
</div>
<div class="container">
<h2>새 선택지 생성</h2>
<form id="createOptionForm">
<!-- Hidden input to pass questionId from URL to form data -->
<input type="hidden" id="questionId" name="questionId" value="<%= request.getAttribute("questionId") %>">
<div class="form-group">
<label for="optionText">선택지 내용:</label>
<input type="text" id="optionText" name="optionText" placeholder="예: 매우 만족" required>
</div>
<div class="form-group">
<label for="optionOrder">선택지 순서:</label>
<input type="number" id="optionOrder" name="optionOrder" value="1" min="1" required>
</div>
<button type="submit">선택지 생성</button>
</form>
<div id="message" class="message"></div>
<a href="#" class="back-link" onclick="history.back(); return false;">&larr; 이전 페이지로 돌아가기</a>
</div>
<script>
const contextPath = "<%= request.getContextPath() %>";
const questionId = document.getElementById('questionId').value;
const initialMessageDiv = document.getElementById('initialMessage');
const existingOptionsListDiv = document.getElementById('existingOptionsList');
const messageDiv = document.getElementById('message'); // 메시지 div 전역 변수로 선언
// Function to fetch and display existing options
async function fetchAndDisplayOptions() {
console.log('fetchAndDisplayOptions called for questionId:', questionId);
if (!questionId || questionId === 'null' || questionId === '') {
existingOptionsListDiv.innerHTML = '<p class="no-options">문항 ID가 유효하지 않아 기존 선택지를 불러올 수 없습니다.</p>';
console.log('Invalid questionId, not fetching options.');
return;
}
const apiUrl = (typeof contextPath !== 'undefined' && contextPath !== null && contextPath !== '') ?
contextPath + '/api/question-options/by-question/' + questionId + '/getOptions.do' :
'/api/question-options/by-question/' + questionId + '/getOptions.do';
console.log('Fetching options from:', apiUrl);
try {
const response = await fetch(apiUrl);
console.log('Options API Response Status:', response.status, response.statusText);
if (response.ok) {
const options = await response.json();
console.log('Fetched Options (RAW):', JSON.stringify(options, null, 2));
let maxOrder = 0;
if (options.length === 0) {
existingOptionsListDiv.innerHTML = '<p class="no-options">아직 생성된 선택지가 없습니다.</p>';
maxOrder = 0;
} else {
existingOptionsListDiv.innerHTML = '';
for (const option of options) {
const currentOrder = parseInt(option.optionOrder, 10);
if (!isNaN(currentOrder) && currentOrder > maxOrder) {
maxOrder = currentOrder;
}
console.log('Processing option:', JSON.stringify(option, null, 2));
const optionItem = document.createElement('div');
optionItem.classList.add('option-item');
const order = String(option.optionOrder || '[순서 없음]');
const text = String(option.optionText || '[내용 없음]');
const optionIdValue = String(option.optionId || '');
console.log('Final values for HTML:', { order, text, optionIdValue });
optionItem.innerHTML =
'<p>' + order + '. ' + text + '</p>' +
'<button type="button" class="delete-option-btn" data-option-id="' + optionIdValue + '">삭제</button>';
existingOptionsListDiv.appendChild(optionItem);
console.log('Appended option item:', optionItem.outerHTML);
}
// Add event listeners for delete buttons after all options are appended
document.querySelectorAll('.delete-option-btn').forEach(button => {
button.addEventListener('click', async function() {
const optionIdToDelete = this.dataset.optionId;
console.log('Delete button clicked for optionId:', optionIdToDelete);
if (!confirm('정말로 이 선택지를 삭제하시겠습니까?')) {
return;
}
// DELETE 요청 대신 POST를 사용하며, URL 경로에 delete 액션을 명시
const deleteApiUrl = (typeof contextPath !== 'undefined' && contextPath !== null && contextPath !== '') ?
contextPath + '/api/question-options/' + optionIdToDelete + '/deleteOption.do' :
'/api/question-options/' + optionIdToDelete + '/deleteOption.do';
console.log('Attempting to delete option from:', deleteApiUrl); // Debugging
try {
const deleteResponse = await fetch(deleteApiUrl, {
method: 'POST', // POST로 변경
headers: {
'Content-Type': 'application/json'
},
// POST로 삭제 시, PathVariable로 ID를 보내므로 body는 필요 없음
// body: JSON.stringify({ optionId: optionIdToDelete }) // 서버가 @RequestBody로 ID를 받는다면 이 줄 활성화
});
console.log('Delete API Response Status:', deleteResponse.status, deleteResponse.statusText); // Debugging
if (deleteResponse.ok) { // HTTP 상태 코드가 200-299 범위인 경우 (성공)
console.log('Option deleted successfully:', optionIdToDelete);
messageDiv.textContent = '선택지가 성공적으로 삭제되었습니다.';
messageDiv.classList.add('success');
messageDiv.style.display = 'block';
await fetchAndDisplayOptions(); // 목록 새로고침
} else { // HTTP 상태 코드가 200-299 범위를 벗어난 경우 (실패)
const errorText = await deleteResponse.text();
console.error('Failed to delete option. Status:', deleteResponse.status, 'Error:', errorText);
messageDiv.textContent = '선택지 삭제 실패: ' + (errorText || '알 수 없는 오류');
messageDiv.classList.add('error');
messageDiv.style.display = 'block';
}
} catch (error) {
console.error('Error deleting option:', error);
messageDiv.textContent = '네트워크 오류로 선택지를 삭제할 수 없습니다.';
messageDiv.classList.add('error');
messageDiv.style.display = 'block';
}
});
});
}
document.getElementById('optionOrder').value = maxOrder + 1;
} else {
existingOptionsListDiv.innerHTML = '<p class="no-options error">기존 선택지를 불러오는데 실패했습니다.</p>';
console.error('Failed to fetch options. Status:', response.status, response.statusText);
document.getElementById('optionOrder').value = 1;
}
} catch (error) {
console.error('Error fetching options:', error);
existingOptionsListDiv.innerHTML = '<p class="no-options error">네트워크 오류로 선택지를 불러올 수 없습니다.</p>';
document.getElementById('optionOrder').value = 1;
}
}
// --- Event Listener for Form Submission ---
document.getElementById('createOptionForm').addEventListener('submit', async function(event) {
event.preventDefault();
const messageDiv = document.getElementById('message');
messageDiv.style.display = 'none';
messageDiv.classList.remove('success', 'error');
const formData = {
questionId: questionId,
optionText: document.getElementById('optionText').value,
optionOrder: parseInt(document.getElementById('optionOrder').value, 10)
};
if (!formData.questionId || formData.questionId === 'null' || formData.questionId === '') {
messageDiv.textContent = '문항 ID가 유효하지 않습니다. 이전 페이지에서 문항을 선택해주세요.';
messageDiv.classList.add('error');
messageDiv.style.display = 'block';
return;
}
if (!formData.optionText.trim()) {
messageDiv.textContent = '선택지 내용을 입력해주세요.';
messageDiv.classList.add('error');
messageDiv.style.display = 'block';
return;
}
const apiUrl = (typeof contextPath !== 'undefined' && contextPath !== null && contextPath !== '') ? contextPath + '/api/question-options/createQuestionOption.do' : '/api/question-options/createQuestionOption.do';
try {
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
if (response.ok) {
const result = await response.json();
messageDiv.textContent = '선택지가 성공적으로 생성되었습니다! (ID: ' + result.optionId + ')';
messageDiv.classList.add('success');
messageDiv.style.display = 'block';
document.getElementById('optionText').value = '';
document.getElementById('optionOrder').value = parseInt(formData.optionOrder, 10) + 1;
await fetchAndDisplayOptions();
} else {
const errorText = await response.text();
messageDiv.textContent = '선택지 생성 실패: ' + (errorText || '알 수 없는 오류');
messageDiv.classList.add('error');
messageDiv.style.display = 'block';
}
} catch (error) {
console.error('네트워크 오류:', error);
messageDiv.textContent = '네트워크 오류가 발생했습니다. 다시 시도해주세요.';
messageDiv.classList.add('error');
messageDiv.style.display = 'block';
}
});
// --- Initial Load Logic ---
if (!questionId || questionId === 'null' || questionId === '') {
initialMessageDiv.textContent = '경고: 문항 ID가 없습니다. 선택지를 생성할 문항을 먼저 선택해주세요.';
initialMessageDiv.classList.add('error');
initialMessageDiv.style.display = 'block';
existingOptionsListDiv.innerHTML = '<p class="no-options">문항 ID가 없어 선택지 목록을 불러올 수 없습니다.</p>';
} else {
fetchAndDisplayOptions();
}
</script>
</body>
</html>

View File

@ -1,6 +1,3 @@
설문 생성을 위한 관리자용 HTML 파일을 수정해 드릴게요. AJAX 요청 URL이 XXX.do 형식에 맞도록 업데이트했습니다.
createSurvey.jsp 수정 내용
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ 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="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%> <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
@ -114,11 +111,16 @@ createSurvey.jsp 수정 내용
<form id="createSurveyForm"> <form id="createSurveyForm">
<div class="form-group"> <div class="form-group">
<label for="surveyTitle">설문 제목:</label> <label for="surveyTitle">설문 제목:</label>
<input type="text" id="surveyTitle" name="surveyTitle" placeholder="예: 2025년 시스템 만족도 설문" required> <input type="text" id="surveyTitle" name="surveyTitle" placeholder="예: 2025년 시스템 만족도 설문" value="2025년 시스템 사용자 경험 및 만족도 평가 설문" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="surveyDescription">설문 설명:</label> <label for="surveyDescription">설문 설명:</label>
<textarea id="surveyDescription" name="surveyDescription" placeholder="설문에 대한 자세한 설명을 입력하세요."></textarea> <textarea id="surveyDescription" name="surveyDescription" placeholder="설문에 대한 자세한 설명을 입력하세요.">안녕하십니까?
저희는 사용자 여러분의 소중한 의견을 경청하고, 현재 사용하고 계신 시스템의 만족도를 면밀히 파악하여 더 나은 서비스와 환경을 제공하고자 본 설문조사를 실시하게 되었습니다.
본 설문은 2025년 한 해 동안 시스템을 이용하면서 경험하셨던 전반적인 만족도, 사용 편의성, 기능 유용성, 안정성 등에 대한 사용자님의 솔직한 평가를 듣기 위함입니다. 여러분의 피드백은 시스템 개선의 중요한 밑거름이 될 것이며, 향후 시스템 개발 및 운영 방향 설정에 큰 도움이 될 것입니다.
설문에 참여해주신 모든 분들께 진심으로 감사드립니다. 설문 응답은 통계적인 목적으로만 활용되며, 개인 정보는 철저히 보호됩니다.
참여 대상: 2025년 시스템 사용자 전체소요 시간: 약 5~10분
여러분의 적극적인 참여를 부탁드립니다.</textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="startDate">시작일시:</label> <label for="startDate">시작일시:</label>
@ -151,13 +153,11 @@ createSurvey.jsp 수정 내용
endDate: document.getElementById('endDate').value ? document.getElementById('endDate').value + ':00' : null // 초까지 포함하도록 포맷팅 endDate: document.getElementById('endDate').value ? document.getElementById('endDate').value + ':00' : null // 초까지 포함하도록 포맷팅
}; };
// eGovFrame 환경에서 컨텍스트 경로를 동적으로 가져오는 방법 (JSP 파일일 경우) // 컨텍스트 경로를 동적으로 가져오는 방법 (JSP 파일일 경우)
// HTML 파일에서는 직접 경로를 지정하거나, 웹 서버 설정에 따라 /api/surveys로 직접 접근 가능
const contextPath = "<%= request.getContextPath() %>"; // JSP일 경우에만 작동 const contextPath = "<%= request.getContextPath() %>"; // JSP일 경우에만 작동
// SurveyController의 @RequestMapping("/api/surveys")와 @PostMapping("/createSurvey.do")를 결합 // SurveyController의 @RequestMapping("/api/surveys")와 @PostMapping("/createSurvey.do")를 결합
const apiUrl = (typeof contextPath !== 'undefined' && contextPath !== null && contextPath !== '') ? contextPath + '/api/surveys/createSurvey.do' : '/api/surveys/createSurvey.do'; const apiUrl = (typeof contextPath !== 'undefined' && contextPath !== null && contextPath !== '') ? contextPath + '/api/surveys/createSurvey.do' : '/api/surveys/createSurvey.do';
try { try {
const response = await fetch(apiUrl, { const response = await fetch(apiUrl, {
method: 'POST', method: 'POST',
@ -173,6 +173,14 @@ createSurvey.jsp 수정 내용
messageDiv.classList.add('success'); messageDiv.classList.add('success');
messageDiv.style.display = 'block'; messageDiv.style.display = 'block';
this.reset(); // 폼 필드 초기화 this.reset(); // 폼 필드 초기화
// 설문 생성 성공 후 createQuestion.jsp로 이동
// AdminSurveyPageController의 /admin/survey/{surveyId}/createQuestionForm.do 경로로 이동
const redirectUrl = (typeof contextPath !== 'undefined' && contextPath !== null && contextPath !== '') ?
contextPath + '/admin/survey/' + result.surveyId + '/createQuestionForm.do' :
'/admin/survey/' + result.surveyId + '/createQuestionForm.do';
window.location.href = redirectUrl; // 페이지 리다이렉트
} else { // HTTP 상태 코드가 200-299 범위를 벗어난 경우 (실패) } else { // HTTP 상태 코드가 200-299 범위를 벗어난 경우 (실패)
const errorText = await response.text(); // 오류 메시지를 텍스트로 가져옴 const errorText = await response.text(); // 오류 메시지를 텍스트로 가져옴
messageDiv.textContent = '설문 생성 실패: ' + (errorText || '알 수 없는 오류'); messageDiv.textContent = '설문 생성 실패: ' + (errorText || '알 수 없는 오류');

View File

@ -20,6 +20,9 @@ function goUserInfo(){
function goApiInfo(){ function goApiInfo(){
location.href = '<c:url value="/sgis/adm/selectApiList.do"/>'; location.href = '<c:url value="/sgis/adm/selectApiList.do"/>';
} }
function goSurveySystem(){
location.href = '<c:url value="/admin/survey/createSurveyForm.do"/>';
}
</script> </script>
<!-- 웹접근성 본문 포커스 시작 --> <!-- 웹접근성 본문 포커스 시작 -->
<ul id="skipToContent"> <ul id="skipToContent">
@ -90,6 +93,9 @@ function goApiInfo(){
<li class="classic-menu-dropdown"> <li class="classic-menu-dropdown">
<a href="javascript:goApiInfo();">API 관리</a> <a href="javascript:goApiInfo();">API 관리</a>
</li> </li>
<li class="classic-menu-dropdown">
<a href="javascript:goSurveySystem();">설문 관리</a>
</li>
</ul> </ul>
</div> </div>

View File

@ -141,6 +141,7 @@
</if> </if>
<![CDATA[ <![CDATA[
ORDER BY A.PROJECT_CODE desc ORDER BY A.PROJECT_CODE desc
LIMIT 1
]]> ]]>
</select> </select>

View File

@ -21,8 +21,8 @@
question_order, question_order,
is_required is_required
) VALUES ( ) VALUES (
#{questionId, jdbcType=OTHER}, CAST(#{questionId, jdbcType=OTHER} AS uuid), <!-- 이 부분을 수정했습니다. -->
#{surveyId, jdbcType=OTHER}, CAST(#{surveyId, jdbcType=OTHER} AS uuid), <!-- 이 부분을 수정했습니다. -->
#{questionText}, #{questionText},
#{questionType}, #{questionType},
#{questionOrder}, #{questionOrder},
@ -30,7 +30,6 @@
) )
</insert> </insert>
<!-- 설문 ID로 문항 목록 조회 -->
<select id="findQuestionsBySurveyId" resultMap="questionResultMap"> <select id="findQuestionsBySurveyId" resultMap="questionResultMap">
SELECT SELECT
question_id, question_id,
@ -40,11 +39,10 @@
question_order, question_order,
is_required is_required
FROM tb_questions FROM tb_questions
WHERE survey_id = #{surveyId, jdbcType=OTHER} WHERE survey_id = CAST(#{surveyId, jdbcType=OTHER} AS uuid)
ORDER BY question_order ASC ORDER BY question_order ASC
</select> </select>
<!-- ID로 특정 문항 조회 -->
<select id="findQuestionById" resultMap="questionResultMap"> <select id="findQuestionById" resultMap="questionResultMap">
SELECT SELECT
question_id, question_id,
@ -54,10 +52,9 @@
question_order, question_order,
is_required is_required
FROM tb_questions FROM tb_questions
WHERE question_id = #{questionId, jdbcType=OTHER} WHERE question_id = CAST(#{questionId, jdbcType=OTHER} AS uuid)
</select> </select>
<!-- 문항 업데이트 -->
<update id="updateQuestion" parameterType="sgis.surveysystem.domain.Question"> <update id="updateQuestion" parameterType="sgis.surveysystem.domain.Question">
UPDATE tb_questions UPDATE tb_questions
SET SET
@ -65,12 +62,11 @@
question_type = #{questionType}, question_type = #{questionType},
question_order = #{questionOrder}, question_order = #{questionOrder},
is_required = #{isRequired} is_required = #{isRequired}
WHERE question_id = #{questionId, jdbcType=OTHER} WHERE question_id = CAST(#{questionId, jdbcType=OTHER} AS uuid)
</update> </update>
<!-- 문항 삭제 -->
<delete id="deleteQuestion"> <delete id="deleteQuestion">
DELETE FROM tb_questions DELETE FROM tb_questions
WHERE question_id = #{questionId, jdbcType=OTHER} WHERE question_id = CAST(#{questionId, jdbcType=OTHER} AS uuid)
</delete> </delete>
</mapper> </mapper>

View File

@ -9,6 +9,7 @@
<result property="optionOrder" column="option_order"/> <result property="optionOrder" column="option_order"/>
</resultMap> </resultMap>
<!-- 선택지 삽입 -->
<insert id="insertQuestionOption" parameterType="sgis.surveysystem.domain.QuestionOption"> <insert id="insertQuestionOption" parameterType="sgis.surveysystem.domain.QuestionOption">
INSERT INTO tb_question_options ( INSERT INTO tb_question_options (
option_id, option_id,
@ -16,8 +17,8 @@
option_text, option_text,
option_order option_order
) VALUES ( ) VALUES (
#{optionId, jdbcType=OTHER}, CAST(#{optionId, jdbcType=OTHER} AS uuid), <!-- 이 부분을 수정했습니다. -->
#{questionId, jdbcType=OTHER}, CAST(#{questionId, jdbcType=OTHER} AS uuid), <!-- 이 부분을 수정했습니다. -->
#{optionText}, #{optionText},
#{optionOrder} #{optionOrder}
) )
@ -30,7 +31,7 @@
option_text, option_text,
option_order option_order
FROM tb_question_options FROM tb_question_options
WHERE question_id = #{questionId, jdbcType=OTHER} WHERE question_id = CAST(#{questionId, jdbcType=OTHER} AS uuid)
ORDER BY option_order ASC ORDER BY option_order ASC
</select> </select>
@ -41,7 +42,7 @@
option_text, option_text,
option_order option_order
FROM tb_question_options FROM tb_question_options
WHERE option_id = #{optionId, jdbcType=OTHER} WHERE option_id = CAST(#{optionId, jdbcType=OTHER} AS uuid)
</select> </select>
<update id="updateQuestionOption" parameterType="sgis.surveysystem.domain.QuestionOption"> <update id="updateQuestionOption" parameterType="sgis.surveysystem.domain.QuestionOption">
@ -49,11 +50,11 @@
SET SET
option_text = #{optionText}, option_text = #{optionText},
option_order = #{optionOrder} option_order = #{optionOrder}
WHERE option_id = #{optionId, jdbcType=OTHER} WHERE option_id = CAST(#{optionId, jdbcType=OTHER} AS uuid)
</update> </update>
<delete id="deleteQuestionOption"> <delete id="deleteQuestionOption">
DELETE FROM tb_question_options DELETE FROM tb_question_options
WHERE option_id = #{optionId, jdbcType=OTHER} WHERE option_id = CAST(#{optionId, jdbcType=OTHER} AS uuid)
</delete> </delete>
</mapper> </mapper>

View File

@ -24,7 +24,8 @@
start_date, start_date,
end_date end_date
) VALUES ( ) VALUES (
#{surveyId, jdbcType=OTHER}, #{surveyTitle}, CAST(#{surveyId, jdbcType=OTHER} AS uuid),
#{surveyTitle},
#{surveyDescription}, #{surveyDescription},
#{createdAt}, #{createdAt},
#{updatedAt}, #{updatedAt},
@ -58,7 +59,7 @@
start_date, start_date,
end_date end_date
FROM tb_surveys FROM tb_surveys
WHERE survey_id = #{surveyId, jdbcType=OTHER} WHERE survey_id = CAST(#{surveyId, jdbcType=OTHER} AS uuid) <!-- 이 부분을 수정했습니다. -->
</select> </select>
<update id="updateSurvey" parameterType="sgis.surveysystem.domain.Survey"> <update id="updateSurvey" parameterType="sgis.surveysystem.domain.Survey">
@ -70,7 +71,7 @@
is_active = #{isActive}, is_active = #{isActive},
start_date = #{startDate}, start_date = #{startDate},
end_date = #{endDate} end_date = #{endDate}
WHERE survey_id = #{surveyId, jdbcType=OTHER} WHERE survey_id = CAST(#{surveyId, jdbcType=OTHER} AS uuid) <!-- 이 부분도 일관성을 위해 수정합니다. -->
</update> </update>
<update id="updateSurveyStatus"> <update id="updateSurveyStatus">
@ -78,11 +79,11 @@
SET SET
is_active = #{isActive}, is_active = #{isActive},
updated_at = NOW() updated_at = NOW()
WHERE survey_id = #{surveyId, jdbcType=OTHER} WHERE survey_id = CAST(#{surveyId, jdbcType=OTHER} AS uuid) <!-- 이 부분도 일관성을 위해 수정합니다. -->
</update> </update>
<delete id="deleteSurvey"> <delete id="deleteSurvey">
DELETE FROM tb_surveys DELETE FROM tb_surveys
WHERE survey_id = #{surveyId, jdbcType=OTHER} WHERE survey_id = CAST(#{surveyId, jdbcType=OTHER} AS uuid) <!-- 이 부분도 일관성을 위해 수정합니다. -->
</delete> </delete>
</mapper> </mapper>

View File

@ -1,5 +1,5 @@
#Generated by Maven Integration for Eclipse #Generated by Maven Integration for Eclipse
#Sun Jul 06 01:28:23 KST 2025 #Fri Jul 11 19:00:12 KST 2025
version=1.0.0 version=1.0.0
groupId=smart_ground groupId=smart_ground
m2e.projectName=sgis m2e.projectName=sgis

View File

@ -25,36 +25,26 @@
</properties> </properties>
<repositories> <repositories>
<repository> <repository>
<id>mvn2</id> <id>mvn2s</id>
<url>http://repo1.maven.org/maven2/</url> <url>https://repo1.maven.org/maven2/</url>
<releases> <releases>
<enabled>true</enabled> <enabled>true</enabled>
</releases> </releases>
<snapshots> <snapshots>
<enabled>true</enabled> <enabled>true</enabled>
</snapshots> </snapshots>
</repository> </repository>
<repository> <repository>
<id>egovframe</id> <id>egovframe</id>
<url>http://www.egovframe.go.kr/maven/</url> <url>https://maven.egovframe.go.kr/maven/</url>
<releases> <releases>
<enabled>true</enabled> <enabled>true</enabled>
</releases> </releases>
<snapshots> <snapshots>
<enabled>false</enabled> <enabled>false</enabled>
</snapshots> </snapshots>
</repository> </repository>
<repository>
<id>egovframe2</id>
<url>http://maven.egovframe.kr:8080/maven/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository> <repository>
<id>osgeo</id> <id>osgeo</id>
<name>OSGeo Release Repository</name> <name>OSGeo Release Repository</name>
@ -299,22 +289,22 @@
<dependency> <dependency>
<groupId>com.fasterxml.jackson.core</groupId> <groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId> <artifactId>jackson-core</artifactId>
<version>2.12.5</version> <version>2.12.4</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.fasterxml.jackson.core</groupId> <groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId> <artifactId>jackson-databind</artifactId>
<version>2.12.5</version> <version>2.12.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.12.5</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.fasterxml.jackson.core</groupId> <groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId> <artifactId>jackson-annotations</artifactId>
<version>2.12.5</version> <version>2.12.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.12.4</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.googlecode.json-simple</groupId> <groupId>com.googlecode.json-simple</groupId>
@ -480,7 +470,7 @@
<build> <build>
<defaultGoal>install</defaultGoal> <defaultGoal>install</defaultGoal>
<directory>${basedir}/target</directory> <directory>${basedir}/target</directory>
<finalName>sht_webapp</finalName> <finalName>smart_ground</finalName>
<pluginManagement> <pluginManagement>
<plugins> <plugins>
<plugin> <plugin>