From c4e86ba3bcec46c1f3ae21a271abc944288554b9 Mon Sep 17 00:00:00 2001 From: sehi55 Date: Fri, 7 Aug 2026 17:31:02 +0900 Subject: [PATCH 1/7] =?UTF-8?q?refactor:=20=EC=84=A4=EB=AC=B8=20=EB=AC=B8?= =?UTF-8?q?=ED=95=AD=20=ED=82=A4=EB=A5=BC=20enum=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=AC=B8=EC=9E=90=EC=97=B4=EB=A1=9C,=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=EC=A7=80=EC=97=90=20=EC=A0=90=EC=88=98(value)=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit seoulection-server가 SurveyQuestionKey enum(AVOIDANCE/CONCERN 2개)에서 문자열 자연 키로 이미 옮겨가 있어, 어드민이 새 문항(WATER_DIRECT 등)을 읽으면 JPA enum 역직렬화가 예외로 죽는 상태였다. 어드민 쪽 도메인·JPA 엔티티·저장소·컨트롤러를 전부 String으로 맞추고, 서버가 이미 갖고 있는 선택지 점수(value, 0~100) 컬럼도 어드민에서 보고 고칠 수 있게 매핑을 추가했다. 선택지 추가 폼의 문항 선택도 하드코딩된 2개 라디오 대신 실제 문항 목록을 순회하도록 바꿨다. Co-Authored-By: Claude Sonnet 5 --- .../application/dto/SurveyOptionResult.java | 3 +- .../application/dto/SurveyQuestionResult.java | 2 +- .../service/SurveyAdminService.java | 16 +++---- .../survey/domain/entity/SurveyOption.java | 42 ++++++++++++------- .../survey/domain/entity/SurveyQuestion.java | 15 ++++--- .../domain/enums/SurveyQuestionKey.java | 17 -------- .../repository/SurveyOptionRepository.java | 5 +-- .../repository/SurveyQuestionRepository.java | 3 +- .../entity/SurveyOptionJpaEntity.java | 21 ++++++---- .../entity/SurveyQuestionJpaEntity.java | 13 +++--- .../repository/SurveyOptionJpaRepository.java | 5 +-- .../SurveyOptionRepositoryImpl.java | 5 +-- .../SurveyQuestionJpaRepository.java | 3 +- .../SurveyQuestionRepositoryImpl.java | 3 +- .../controller/SurveyAdminController.java | 10 ++--- .../dto/SurveyOptionCreateRequest.java | 15 +++++++ src/main/resources/templates/survey.html | 22 ++++++---- .../application/SurveyAdminServiceTest.java | 35 ++++++++++------ 18 files changed, 130 insertions(+), 105 deletions(-) delete mode 100644 src/main/java/com/seoulection/admin/survey/domain/enums/SurveyQuestionKey.java diff --git a/src/main/java/com/seoulection/admin/survey/application/dto/SurveyOptionResult.java b/src/main/java/com/seoulection/admin/survey/application/dto/SurveyOptionResult.java index f6a4ed7..409f585 100644 --- a/src/main/java/com/seoulection/admin/survey/application/dto/SurveyOptionResult.java +++ b/src/main/java/com/seoulection/admin/survey/application/dto/SurveyOptionResult.java @@ -7,13 +7,14 @@ public record SurveyOptionResult( Long id, String code, String label, + Integer value, int sortOrder, boolean exclusive, boolean active ) { public static SurveyOptionResult from(SurveyOption o) { - return new SurveyOptionResult(o.getId(), o.getCode(), o.getLabel(), o.getSortOrder(), + return new SurveyOptionResult(o.getId(), o.getCode(), o.getLabel(), o.getValue(), o.getSortOrder(), o.isExclusive(), o.isActive()); } } diff --git a/src/main/java/com/seoulection/admin/survey/application/dto/SurveyQuestionResult.java b/src/main/java/com/seoulection/admin/survey/application/dto/SurveyQuestionResult.java index 54029e7..293c86b 100644 --- a/src/main/java/com/seoulection/admin/survey/application/dto/SurveyQuestionResult.java +++ b/src/main/java/com/seoulection/admin/survey/application/dto/SurveyQuestionResult.java @@ -12,6 +12,6 @@ public record SurveyQuestionResult( ) { public static SurveyQuestionResult of(SurveyQuestion question, List options) { - return new SurveyQuestionResult(question.getQuestionKey().name(), question.getTitle(), options); + return new SurveyQuestionResult(question.getQuestionKey(), question.getTitle(), options); } } diff --git a/src/main/java/com/seoulection/admin/survey/application/service/SurveyAdminService.java b/src/main/java/com/seoulection/admin/survey/application/service/SurveyAdminService.java index 9ca83f1..d880f97 100644 --- a/src/main/java/com/seoulection/admin/survey/application/service/SurveyAdminService.java +++ b/src/main/java/com/seoulection/admin/survey/application/service/SurveyAdminService.java @@ -4,7 +4,6 @@ import com.seoulection.admin.survey.application.dto.SurveyQuestionResult; import com.seoulection.admin.survey.domain.entity.SurveyOption; import com.seoulection.admin.survey.domain.entity.SurveyQuestion; -import com.seoulection.admin.survey.domain.enums.SurveyQuestionKey; import com.seoulection.admin.survey.domain.repository.SurveyOptionRepository; import com.seoulection.admin.survey.domain.repository.SurveyQuestionRepository; import org.springframework.stereotype.Service; @@ -42,20 +41,21 @@ public List getQuestions() { * 가리키는지 알 수 없어진다(DB 유니크 제약도 있지만 여기서 먼저 걸러 안내 문구를 준다). */ @Transactional - public void createOption(SurveyQuestionKey questionKey, String code, String label, + public void createOption(String questionKey, String code, String label, Integer value, int sortOrder, boolean exclusive) { String normalizedCode = code == null ? "" : code.trim().toUpperCase(); if (optionRepository.existsByQuestionKeyAndCode(questionKey, normalizedCode)) { throw new IllegalArgumentException("이미 있는 코드입니다: " + normalizedCode); } - optionRepository.save(SurveyOption.create(questionKey, normalizedCode, label, sortOrder, exclusive)); + optionRepository.save( + SurveyOption.create(questionKey, normalizedCode, label, value, sortOrder, exclusive)); } - /** 문구·순서·단독선택 수정. {@code code}는 대상이 아니다(도메인이 막는다). */ + /** 문구·점수·순서·단독선택 수정. {@code code}는 대상이 아니다(도메인이 막는다). */ @Transactional - public void updateOption(Long optionId, String label, int sortOrder, boolean exclusive) { + public void updateOption(Long optionId, String label, Integer value, int sortOrder, boolean exclusive) { SurveyOption option = getOption(optionId); - optionRepository.save(option.withDetails(label, sortOrder, exclusive)); + optionRepository.save(option.withDetails(label, value, sortOrder, exclusive)); } /** @@ -69,13 +69,13 @@ public void changeOptionActive(Long optionId, boolean active) { } @Transactional - public void updateQuestionTitle(SurveyQuestionKey questionKey, String title) { + public void updateQuestionTitle(String questionKey, String title) { SurveyQuestion question = questionRepository.findByKey(questionKey) .orElseThrow(() -> new IllegalArgumentException("없는 문항입니다: " + questionKey)); questionRepository.save(question.withTitle(title)); } - private List findOptions(SurveyQuestionKey questionKey) { + private List findOptions(String questionKey) { return optionRepository.findByQuestionKey(questionKey).stream() .map(SurveyOptionResult::from) .toList(); diff --git a/src/main/java/com/seoulection/admin/survey/domain/entity/SurveyOption.java b/src/main/java/com/seoulection/admin/survey/domain/entity/SurveyOption.java index bb091a8..0c9298f 100644 --- a/src/main/java/com/seoulection/admin/survey/domain/entity/SurveyOption.java +++ b/src/main/java/com/seoulection/admin/survey/domain/entity/SurveyOption.java @@ -1,7 +1,5 @@ package com.seoulection.admin.survey.domain.entity; -import com.seoulection.admin.survey.domain.enums.SurveyQuestionKey; - import java.util.Objects; import java.util.regex.Pattern; @@ -9,7 +7,7 @@ * 설문 선택지 — 관리자가 CRUD하는 대상. * *

{@code code}는 생성 시 확정되고 이후 바뀌지 않는다. 사용자 응답이 - * {@code survey_response.avoidances} jsonb 배열에 이 문자열로 저장돼 있어서, 코드를 고치면 이미 제출된 + * {@code survey_answer.codes} jsonb 배열에 이 문자열로 저장돼 있어서, 코드를 고치면 이미 제출된 * 응답이 가리키는 대상이 사라진다. 그래서 수정 메서드({@link #withDetails})에 code가 없다 — * 화면에서 막는 게 아니라 타입에서 막는다. * @@ -21,44 +19,46 @@ public class SurveyOption { private static final Pattern CODE_FORMAT = Pattern.compile("^[A-Z][A-Z0-9_]*$"); private final Long id; - private final SurveyQuestionKey questionKey; + private final String questionKey; private final String code; private final String label; + private final Integer value; private final int sortOrder; private final boolean exclusive; private final boolean active; - private SurveyOption(Long id, SurveyQuestionKey questionKey, String code, String label, + private SurveyOption(Long id, String questionKey, String code, String label, Integer value, int sortOrder, boolean exclusive, boolean active) { this.id = id; this.questionKey = Objects.requireNonNull(questionKey, "questionKey"); this.code = requireCode(code); this.label = requireText(label, "label"); + this.value = requireValidValue(value); this.sortOrder = requireNonNegative(sortOrder); this.exclusive = exclusive; this.active = active; } - /** 새 선택지. 활성 상태로 시작한다. */ - public static SurveyOption create(SurveyQuestionKey questionKey, String code, String label, + /** 새 선택지. 활성 상태로 시작한다. {@code value}는 카테고리성 선택지(회피 항목 등)라면 null. */ + public static SurveyOption create(String questionKey, String code, String label, Integer value, int sortOrder, boolean exclusive) { - return new SurveyOption(null, questionKey, code, label, sortOrder, exclusive, true); + return new SurveyOption(null, questionKey, code, label, value, sortOrder, exclusive, true); } /** 저장소가 읽어온 기존 행을 복원할 때 쓴다. */ - public static SurveyOption of(Long id, SurveyQuestionKey questionKey, String code, String label, + public static SurveyOption of(Long id, String questionKey, String code, String label, Integer value, int sortOrder, boolean exclusive, boolean active) { - return new SurveyOption(id, questionKey, code, label, sortOrder, exclusive, active); + return new SurveyOption(id, questionKey, code, label, value, sortOrder, exclusive, active); } - /** 문구·순서·단독선택 여부를 고친다. code와 active는 대상이 아니다. */ - public SurveyOption withDetails(String newLabel, int newSortOrder, boolean newExclusive) { - return new SurveyOption(id, questionKey, code, newLabel, newSortOrder, newExclusive, active); + /** 문구·점수·순서·단독선택 여부를 고친다. code와 active는 대상이 아니다. */ + public SurveyOption withDetails(String newLabel, Integer newValue, int newSortOrder, boolean newExclusive) { + return new SurveyOption(id, questionKey, code, newLabel, newValue, newSortOrder, newExclusive, active); } /** 노출/숨김. {@code false}가 곧 삭제다(행은 남는다). */ public SurveyOption withActive(boolean newActive) { - return new SurveyOption(id, questionKey, code, label, sortOrder, exclusive, newActive); + return new SurveyOption(id, questionKey, code, label, value, sortOrder, exclusive, newActive); } private static String requireCode(String code) { @@ -83,11 +83,19 @@ private static int requireNonNegative(int sortOrder) { return sortOrder; } + /** 카테고리성 선택지는 점수 개념이 없어 null을 허용한다. 있다면 0~100 범위여야 한다. */ + private static Integer requireValidValue(Integer value) { + if (value != null && (value < 0 || value > 100)) { + throw new IllegalArgumentException("점수는 0에서 100 사이여야 합니다."); + } + return value; + } + public Long getId() { return id; } - public SurveyQuestionKey getQuestionKey() { + public String getQuestionKey() { return questionKey; } @@ -99,6 +107,10 @@ public String getLabel() { return label; } + public Integer getValue() { + return value; + } + public int getSortOrder() { return sortOrder; } diff --git a/src/main/java/com/seoulection/admin/survey/domain/entity/SurveyQuestion.java b/src/main/java/com/seoulection/admin/survey/domain/entity/SurveyQuestion.java index 80e242c..dda08f5 100644 --- a/src/main/java/com/seoulection/admin/survey/domain/entity/SurveyQuestion.java +++ b/src/main/java/com/seoulection/admin/survey/domain/entity/SurveyQuestion.java @@ -1,28 +1,27 @@ package com.seoulection.admin.survey.domain.entity; -import com.seoulection.admin.survey.domain.enums.SurveyQuestionKey; - import java.util.Objects; /** * 설문 문항 — 관리자는 문구({@code title})만 고친다. * - *

추가·삭제가 없는 이유: 문항 하나가 api-server 제출 요청 본문의 필드 하나와 1:1로 묶여 있다 - * ({@code AVOIDANCE → avoidances}). 행을 지우면 그 필드를 받는 쪽이 갈 곳을 잃는다. + *

{@code questionKey}는 문자열 식별자다(api-server와 동일). 예전엔 {@code SurveyQuestionKey} enum이었지만, + * api-server의 제출 요청이 문항마다 고정 필드를 갖는 구조를 벗어나 문항 목록 형태로 바뀌면서 문항 집합을 + * 코드에 고정할 이유가 사라졌다 — 지금은 {@link SurveyOption}과 마찬가지로 행의 추가·삭제가 데이터만으로 가능하다. */ public class SurveyQuestion { - private final SurveyQuestionKey questionKey; + private final String questionKey; private final String title; private final int sortOrder; - private SurveyQuestion(SurveyQuestionKey questionKey, String title, int sortOrder) { + private SurveyQuestion(String questionKey, String title, int sortOrder) { this.questionKey = Objects.requireNonNull(questionKey, "questionKey"); this.title = requireText(title); this.sortOrder = sortOrder; } - public static SurveyQuestion of(SurveyQuestionKey questionKey, String title, int sortOrder) { + public static SurveyQuestion of(String questionKey, String title, int sortOrder) { return new SurveyQuestion(questionKey, title, sortOrder); } @@ -37,7 +36,7 @@ private static String requireText(String title) { return title.trim(); } - public SurveyQuestionKey getQuestionKey() { + public String getQuestionKey() { return questionKey; } diff --git a/src/main/java/com/seoulection/admin/survey/domain/enums/SurveyQuestionKey.java b/src/main/java/com/seoulection/admin/survey/domain/enums/SurveyQuestionKey.java deleted file mode 100644 index fcde08c..0000000 --- a/src/main/java/com/seoulection/admin/survey/domain/enums/SurveyQuestionKey.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.seoulection.admin.survey.domain.enums; - -/** - * 설문 문항 식별자. api-server의 동명 enum과 상수명이 일치해야 한다 — - * {@code survey_question.question_key} / {@code survey_option.question_key} 컬럼에 문자열로 저장되기 때문이다. - * - *

여기에 상수를 추가해도 api-server가 모르면 아무 일도 일어나지 않는다. 문항 집합은 양쪽 코드에 - * 고정돼 있고, 관리자가 바꾸는 건 문구와 선택지다. - */ -public enum SurveyQuestionKey { - - /** Q1 — 회피 성분/상태. 제출 요청의 {@code avoidances} 필드. */ - AVOIDANCE, - - /** Q2 — 피부 고민. 제출 요청의 {@code concerns} 필드. */ - CONCERN -} diff --git a/src/main/java/com/seoulection/admin/survey/domain/repository/SurveyOptionRepository.java b/src/main/java/com/seoulection/admin/survey/domain/repository/SurveyOptionRepository.java index aa74e21..9c6c3bf 100644 --- a/src/main/java/com/seoulection/admin/survey/domain/repository/SurveyOptionRepository.java +++ b/src/main/java/com/seoulection/admin/survey/domain/repository/SurveyOptionRepository.java @@ -1,7 +1,6 @@ package com.seoulection.admin.survey.domain.repository; import com.seoulection.admin.survey.domain.entity.SurveyOption; -import com.seoulection.admin.survey.domain.enums.SurveyQuestionKey; import java.util.List; import java.util.Optional; @@ -10,11 +9,11 @@ public interface SurveyOptionRepository { /** 노출 순서로 정렬된 문항별 선택지(비활성 포함 — 관리 화면은 숨긴 것도 봐야 한다). */ - List findByQuestionKey(SurveyQuestionKey questionKey); + List findByQuestionKey(String questionKey); Optional findById(Long id); - boolean existsByQuestionKeyAndCode(SurveyQuestionKey questionKey, String code); + boolean existsByQuestionKeyAndCode(String questionKey, String code); SurveyOption save(SurveyOption option); } diff --git a/src/main/java/com/seoulection/admin/survey/domain/repository/SurveyQuestionRepository.java b/src/main/java/com/seoulection/admin/survey/domain/repository/SurveyQuestionRepository.java index 01f6f39..f3e4a66 100644 --- a/src/main/java/com/seoulection/admin/survey/domain/repository/SurveyQuestionRepository.java +++ b/src/main/java/com/seoulection/admin/survey/domain/repository/SurveyQuestionRepository.java @@ -1,7 +1,6 @@ package com.seoulection.admin.survey.domain.repository; import com.seoulection.admin.survey.domain.entity.SurveyQuestion; -import com.seoulection.admin.survey.domain.enums.SurveyQuestionKey; import java.util.List; import java.util.Optional; @@ -11,7 +10,7 @@ public interface SurveyQuestionRepository { List findAllOrdered(); - Optional findByKey(SurveyQuestionKey questionKey); + Optional findByKey(String questionKey); SurveyQuestion save(SurveyQuestion question); } diff --git a/src/main/java/com/seoulection/admin/survey/infrastructure/entity/SurveyOptionJpaEntity.java b/src/main/java/com/seoulection/admin/survey/infrastructure/entity/SurveyOptionJpaEntity.java index f1c67da..9cf0920 100644 --- a/src/main/java/com/seoulection/admin/survey/infrastructure/entity/SurveyOptionJpaEntity.java +++ b/src/main/java/com/seoulection/admin/survey/infrastructure/entity/SurveyOptionJpaEntity.java @@ -1,11 +1,8 @@ package com.seoulection.admin.survey.infrastructure.entity; import com.seoulection.admin.survey.domain.entity.SurveyOption; -import com.seoulection.admin.survey.domain.enums.SurveyQuestionKey; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -17,6 +14,9 @@ *

⚠️ 이 테이블은 api-server가 소유한다(그쪽 {@code SurveyOptionJpaEntity}의 매핑이 곧 DDL). * 여기는 {@code ddl-auto=none}으로 붙는 소비자이므로, 컬럼이 어긋나면 기동이 아니라 * 쿼리 실행 시점에 터진다. api-server 쪽 매핑을 바꾸면 이 클래스도 같이 고쳐야 한다. + * + *

{@code questionKey}는 문자열이다(enum 아님) — api-server가 문항 집합을 코드에 고정하지 않으므로 + * 여기서 enum으로 매핑하면 모르는 문항 키가 나올 때마다 조회가 예외로 터진다. */ @Entity @Table(name = "survey_option") @@ -26,9 +26,8 @@ public class SurveyOptionJpaEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Enumerated(EnumType.STRING) @Column(name = "question_key", nullable = false, length = 32) - private SurveyQuestionKey questionKey; + private String questionKey; @Column(nullable = false, length = 64, updatable = false) private String code; @@ -36,6 +35,10 @@ public class SurveyOptionJpaEntity { @Column(nullable = false) private String label; + /** 이 선택지의 점수(0~100). 카테고리성 선택지는 null. */ + @Column + private Integer value; + @Column(name = "sort_order", nullable = false) private int sortOrder; @@ -48,12 +51,13 @@ public class SurveyOptionJpaEntity { protected SurveyOptionJpaEntity() { } - private SurveyOptionJpaEntity(Long id, SurveyQuestionKey questionKey, String code, String label, + private SurveyOptionJpaEntity(Long id, String questionKey, String code, String label, Integer value, int sortOrder, boolean exclusive, boolean active) { this.id = id; this.questionKey = questionKey; this.code = code; this.label = label; + this.value = value; this.sortOrder = sortOrder; this.exclusive = exclusive; this.active = active; @@ -61,10 +65,11 @@ private SurveyOptionJpaEntity(Long id, SurveyQuestionKey questionKey, String cod public static SurveyOptionJpaEntity fromDomain(SurveyOption option) { return new SurveyOptionJpaEntity(option.getId(), option.getQuestionKey(), option.getCode(), - option.getLabel(), option.getSortOrder(), option.isExclusive(), option.isActive()); + option.getLabel(), option.getValue(), option.getSortOrder(), option.isExclusive(), + option.isActive()); } public SurveyOption toDomain() { - return SurveyOption.of(id, questionKey, code, label, sortOrder, exclusive, active); + return SurveyOption.of(id, questionKey, code, label, value, sortOrder, exclusive, active); } } diff --git a/src/main/java/com/seoulection/admin/survey/infrastructure/entity/SurveyQuestionJpaEntity.java b/src/main/java/com/seoulection/admin/survey/infrastructure/entity/SurveyQuestionJpaEntity.java index 25d7a26..afd2857 100644 --- a/src/main/java/com/seoulection/admin/survey/infrastructure/entity/SurveyQuestionJpaEntity.java +++ b/src/main/java/com/seoulection/admin/survey/infrastructure/entity/SurveyQuestionJpaEntity.java @@ -1,29 +1,26 @@ package com.seoulection.admin.survey.infrastructure.entity; import com.seoulection.admin.survey.domain.entity.SurveyQuestion; -import com.seoulection.admin.survey.domain.enums.SurveyQuestionKey; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; import jakarta.persistence.Id; import jakarta.persistence.Table; /** * {@code survey_question} 영속성 엔티티. 스키마 주인은 api-server다(여기는 {@code ddl-auto=none}). * - *

{@code questionKey}가 자연 PK라 {@code save()}가 곧 upsert다 — 관리자의 문구 수정이 새 행을 만들지 않는다. + *

{@code questionKey}가 자연 PK(문자열, enum 아님)라 {@code save()}가 곧 upsert다 — + * 관리자의 문구 수정이 새 행을 만들지 않는다. */ @Entity @Table(name = "survey_question") public class SurveyQuestionJpaEntity { @Id - @Enumerated(EnumType.STRING) @Column(name = "question_key", nullable = false, length = 32) - private SurveyQuestionKey questionKey; + private String questionKey; - @Column(nullable = false) + @Column(nullable = false, length = 512) private String title; @Column(name = "sort_order", nullable = false) @@ -32,7 +29,7 @@ public class SurveyQuestionJpaEntity { protected SurveyQuestionJpaEntity() { } - private SurveyQuestionJpaEntity(SurveyQuestionKey questionKey, String title, int sortOrder) { + private SurveyQuestionJpaEntity(String questionKey, String title, int sortOrder) { this.questionKey = questionKey; this.title = title; this.sortOrder = sortOrder; diff --git a/src/main/java/com/seoulection/admin/survey/infrastructure/repository/SurveyOptionJpaRepository.java b/src/main/java/com/seoulection/admin/survey/infrastructure/repository/SurveyOptionJpaRepository.java index 2d2833c..1de56f5 100644 --- a/src/main/java/com/seoulection/admin/survey/infrastructure/repository/SurveyOptionJpaRepository.java +++ b/src/main/java/com/seoulection/admin/survey/infrastructure/repository/SurveyOptionJpaRepository.java @@ -1,6 +1,5 @@ package com.seoulection.admin.survey.infrastructure.repository; -import com.seoulection.admin.survey.domain.enums.SurveyQuestionKey; import com.seoulection.admin.survey.infrastructure.entity.SurveyOptionJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; @@ -9,7 +8,7 @@ /** Spring Data JPA 리포지토리(영속성 엔티티 전용, 내부용). */ interface SurveyOptionJpaRepository extends JpaRepository { - List findByQuestionKeyOrderBySortOrderAscCodeAsc(SurveyQuestionKey questionKey); + List findByQuestionKeyOrderBySortOrderAscCodeAsc(String questionKey); - boolean existsByQuestionKeyAndCode(SurveyQuestionKey questionKey, String code); + boolean existsByQuestionKeyAndCode(String questionKey, String code); } diff --git a/src/main/java/com/seoulection/admin/survey/infrastructure/repository/SurveyOptionRepositoryImpl.java b/src/main/java/com/seoulection/admin/survey/infrastructure/repository/SurveyOptionRepositoryImpl.java index b04f893..7c9d49e 100644 --- a/src/main/java/com/seoulection/admin/survey/infrastructure/repository/SurveyOptionRepositoryImpl.java +++ b/src/main/java/com/seoulection/admin/survey/infrastructure/repository/SurveyOptionRepositoryImpl.java @@ -1,7 +1,6 @@ package com.seoulection.admin.survey.infrastructure.repository; import com.seoulection.admin.survey.domain.entity.SurveyOption; -import com.seoulection.admin.survey.domain.enums.SurveyQuestionKey; import com.seoulection.admin.survey.domain.repository.SurveyOptionRepository; import com.seoulection.admin.survey.infrastructure.entity.SurveyOptionJpaEntity; import org.springframework.stereotype.Repository; @@ -20,7 +19,7 @@ public SurveyOptionRepositoryImpl(SurveyOptionJpaRepository jpaRepository) { } @Override - public List findByQuestionKey(SurveyQuestionKey questionKey) { + public List findByQuestionKey(String questionKey) { // 정렬 기준(sortOrder → code)은 api-server의 SurveyOptionCatalog와 같아야 한다. // 관리 화면에서 보이는 순서가 곧 사용자 화면 순서여야 관리자가 결과를 예측할 수 있다. return jpaRepository.findByQuestionKeyOrderBySortOrderAscCodeAsc(questionKey).stream() @@ -34,7 +33,7 @@ public Optional findById(Long id) { } @Override - public boolean existsByQuestionKeyAndCode(SurveyQuestionKey questionKey, String code) { + public boolean existsByQuestionKeyAndCode(String questionKey, String code) { return jpaRepository.existsByQuestionKeyAndCode(questionKey, code); } diff --git a/src/main/java/com/seoulection/admin/survey/infrastructure/repository/SurveyQuestionJpaRepository.java b/src/main/java/com/seoulection/admin/survey/infrastructure/repository/SurveyQuestionJpaRepository.java index 1acf3ca..3f32b0c 100644 --- a/src/main/java/com/seoulection/admin/survey/infrastructure/repository/SurveyQuestionJpaRepository.java +++ b/src/main/java/com/seoulection/admin/survey/infrastructure/repository/SurveyQuestionJpaRepository.java @@ -1,13 +1,12 @@ package com.seoulection.admin.survey.infrastructure.repository; -import com.seoulection.admin.survey.domain.enums.SurveyQuestionKey; import com.seoulection.admin.survey.infrastructure.entity.SurveyQuestionJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; /** Spring Data JPA 리포지토리(영속성 엔티티 전용, 내부용). */ -interface SurveyQuestionJpaRepository extends JpaRepository { +interface SurveyQuestionJpaRepository extends JpaRepository { List findAllByOrderBySortOrderAsc(); } diff --git a/src/main/java/com/seoulection/admin/survey/infrastructure/repository/SurveyQuestionRepositoryImpl.java b/src/main/java/com/seoulection/admin/survey/infrastructure/repository/SurveyQuestionRepositoryImpl.java index 36bec3a..2e4c1a9 100644 --- a/src/main/java/com/seoulection/admin/survey/infrastructure/repository/SurveyQuestionRepositoryImpl.java +++ b/src/main/java/com/seoulection/admin/survey/infrastructure/repository/SurveyQuestionRepositoryImpl.java @@ -1,7 +1,6 @@ package com.seoulection.admin.survey.infrastructure.repository; import com.seoulection.admin.survey.domain.entity.SurveyQuestion; -import com.seoulection.admin.survey.domain.enums.SurveyQuestionKey; import com.seoulection.admin.survey.domain.repository.SurveyQuestionRepository; import com.seoulection.admin.survey.infrastructure.entity.SurveyQuestionJpaEntity; import org.springframework.stereotype.Repository; @@ -27,7 +26,7 @@ public List findAllOrdered() { } @Override - public Optional findByKey(SurveyQuestionKey questionKey) { + public Optional findByKey(String questionKey) { return jpaRepository.findById(questionKey).map(SurveyQuestionJpaEntity::toDomain); } diff --git a/src/main/java/com/seoulection/admin/survey/presentation/controller/SurveyAdminController.java b/src/main/java/com/seoulection/admin/survey/presentation/controller/SurveyAdminController.java index 2841309..629fc1b 100644 --- a/src/main/java/com/seoulection/admin/survey/presentation/controller/SurveyAdminController.java +++ b/src/main/java/com/seoulection/admin/survey/presentation/controller/SurveyAdminController.java @@ -1,7 +1,6 @@ package com.seoulection.admin.survey.presentation.controller; import com.seoulection.admin.survey.application.service.SurveyAdminService; -import com.seoulection.admin.survey.domain.enums.SurveyQuestionKey; import com.seoulection.admin.survey.presentation.dto.SurveyOptionCreateRequest; import jakarta.validation.Valid; import org.springframework.stereotype.Controller; @@ -49,8 +48,8 @@ public String createOption(@Valid @ModelAttribute("request") SurveyOptionCreateR } try { - service.createOption(SurveyQuestionKey.valueOf(request.getQuestionKey()), - request.getCode(), request.getLabel(), request.getSortOrder(), request.isExclusive()); + service.createOption(request.getQuestionKey(), request.getCode(), request.getLabel(), + request.getValue(), request.getSortOrder(), request.isExclusive()); } catch (IllegalArgumentException e) { // 코드 중복·형식 위반은 사용자가 고칠 수 있는 입력 오류다 → 폼으로 되돌려 사유를 보여준다. bindingResult.rejectValue("code", "invalid", e.getMessage()); @@ -64,10 +63,11 @@ public String createOption(@Valid @ModelAttribute("request") SurveyOptionCreateR @PostMapping("/admin/survey/options/{optionId}") public String updateOption(@PathVariable Long optionId, @RequestParam String label, + @RequestParam(required = false) Integer value, @RequestParam int sortOrder, @RequestParam(defaultValue = "false") boolean exclusive, RedirectAttributes redirectAttributes) { - service.updateOption(optionId, label, sortOrder, exclusive); + service.updateOption(optionId, label, value, sortOrder, exclusive); redirectAttributes.addFlashAttribute("successMessage", "선택지를 수정했습니다."); return "redirect:/admin/survey"; } @@ -84,7 +84,7 @@ public String changeOptionActive(@PathVariable Long optionId, } @PostMapping("/admin/survey/questions/{questionKey}") - public String updateQuestionTitle(@PathVariable SurveyQuestionKey questionKey, + public String updateQuestionTitle(@PathVariable String questionKey, @RequestParam String title, RedirectAttributes redirectAttributes) { service.updateQuestionTitle(questionKey, title); diff --git a/src/main/java/com/seoulection/admin/survey/presentation/dto/SurveyOptionCreateRequest.java b/src/main/java/com/seoulection/admin/survey/presentation/dto/SurveyOptionCreateRequest.java index d02cf7e..d331014 100644 --- a/src/main/java/com/seoulection/admin/survey/presentation/dto/SurveyOptionCreateRequest.java +++ b/src/main/java/com/seoulection/admin/survey/presentation/dto/SurveyOptionCreateRequest.java @@ -1,5 +1,7 @@ package com.seoulection.admin.survey.presentation.dto; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; @@ -26,6 +28,11 @@ public class SurveyOptionCreateRequest { @Size(max = 255, message = "노출 문구는 255자 이하여야 합니다.") private String label; + /** 카테고리성 선택지(회피 항목 등)는 점수 개념이 없으므로 비워 둘 수 있다. */ + @Min(value = 0, message = "점수는 0 이상이어야 합니다.") + @Max(value = 100, message = "점수는 100 이하여야 합니다.") + private Integer value; + @NotNull(message = "노출 순서를 입력해 주세요.") @PositiveOrZero(message = "노출 순서는 0 이상이어야 합니다.") private Integer sortOrder; @@ -56,6 +63,14 @@ public void setLabel(String label) { this.label = label; } + public Integer getValue() { + return value; + } + + public void setValue(Integer value) { + this.value = value; + } + public Integer getSortOrder() { return sortOrder; } diff --git a/src/main/resources/templates/survey.html b/src/main/resources/templates/survey.html index e8051fb..90a68ea 100644 --- a/src/main/resources/templates/survey.html +++ b/src/main/resources/templates/survey.html @@ -64,9 +64,9 @@

설문 관리

- 코드(code)는 만든 뒤 바꿀 수 없습니다. - 이미 제출된 사용자 응답이 이 문자열을 그대로 참조하고 있어, 코드를 바꾸면 과거 응답이 가리키는 대상이 사라집니다. - 같은 이유로 삭제는 '숨김'으로 동작합니다 — 신규 설문에서만 사라지고 기존 응답은 보존됩니다. + 문항 키(questionKey)·선택지 코드(code)는 만든 뒤 바꿀 수 없습니다. + 이미 제출된 사용자 응답이 이 문자열을 그대로 참조하고 있어, 바꾸면 과거 응답이 가리키는 대상이 사라집니다. + 선택지의 삭제는 '숨김'으로 동작합니다 — 신규 설문에서만 사라지고 기존 응답은 보존됩니다.

@@ -76,8 +76,10 @@

선택지 추가

문항
- - +

@@ -91,6 +93,11 @@

선택지 추가

+
+ + +

+
@@ -119,15 +126,16 @@

선택지 추가

- + -
코드노출 문구순서단독상태
코드노출 문구점수순서단독상태
+
+