diff --git a/src/main/java/com/seoulection/admin/skinpolicy/application/SkinScorePolicyService.java b/src/main/java/com/seoulection/admin/skinpolicy/application/SkinScorePolicyService.java new file mode 100644 index 0000000..acdb19b --- /dev/null +++ b/src/main/java/com/seoulection/admin/skinpolicy/application/SkinScorePolicyService.java @@ -0,0 +1,34 @@ +package com.seoulection.admin.skinpolicy.application; + +import com.seoulection.admin.skinpolicy.domain.SkinScorePolicy; +import com.seoulection.admin.skinpolicy.infrastructure.SkinScorePolicyJpaEntity; +import com.seoulection.admin.skinpolicy.infrastructure.SkinScorePolicyJpaRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +public class SkinScorePolicyService { + private final SkinScorePolicyJpaRepository repository; + + public SkinScorePolicyService(SkinScorePolicyJpaRepository repository) { + this.repository = repository; + } + + @Transactional(readOnly = true) + public List findAll() { + return repository.findAll().stream().map(SkinScorePolicyJpaEntity::toDomain) + .sorted(java.util.Comparator.comparing(SkinScorePolicy::featureKey)).toList(); + } + + @Transactional + public void update(String featureKey, double badBoundary, double goodBoundary, String policyVersion) { + SkinScorePolicy current = repository.findById(featureKey) + .orElseThrow(() -> new IllegalArgumentException("없는 피부 항목입니다: " + featureKey)) + .toDomain(); + repository.save(SkinScorePolicyJpaEntity.from(new SkinScorePolicy(featureKey, current.label(), + current.surveyReliability(), current.photoReliability(), current.inconsistentSurveyFactor(), + badBoundary, goodBoundary, policyVersion.trim()))); + } +} diff --git a/src/main/java/com/seoulection/admin/skinpolicy/domain/SkinScorePolicy.java b/src/main/java/com/seoulection/admin/skinpolicy/domain/SkinScorePolicy.java new file mode 100644 index 0000000..e0829ef --- /dev/null +++ b/src/main/java/com/seoulection/admin/skinpolicy/domain/SkinScorePolicy.java @@ -0,0 +1,27 @@ +package com.seoulection.admin.skinpolicy.domain; + +public record SkinScorePolicy(String featureKey, String label, double surveyReliability, + double photoReliability, double inconsistentSurveyFactor, + double badBoundary, double goodBoundary, + String policyVersion) { + public SkinScorePolicy { + validate(surveyReliability, "설문 신뢰도"); + validate(photoReliability, "사진 신뢰도"); + validate(inconsistentSurveyFactor, "불일치 계수"); + validate(badBoundary, "Bad 경계"); + validate(goodBoundary, "Good 경계"); + if (badBoundary > goodBoundary) { + throw new IllegalArgumentException("Bad 경계는 Good 경계보다 클 수 없습니다."); + } + if (surveyReliability + photoReliability <= 0) { + throw new IllegalArgumentException("설문·사진 신뢰도가 모두 0일 수 없습니다."); + } + if (policyVersion == null || policyVersion.isBlank()) { + throw new IllegalArgumentException("정책 버전은 비워둘 수 없습니다."); + } + } + + private static void validate(double value, String name) { + if (value < 0 || value > 1) throw new IllegalArgumentException(name + "는 0~1이어야 합니다."); + } +} diff --git a/src/main/java/com/seoulection/admin/skinpolicy/infrastructure/SkinScorePolicyJpaEntity.java b/src/main/java/com/seoulection/admin/skinpolicy/infrastructure/SkinScorePolicyJpaEntity.java new file mode 100644 index 0000000..f818ec1 --- /dev/null +++ b/src/main/java/com/seoulection/admin/skinpolicy/infrastructure/SkinScorePolicyJpaEntity.java @@ -0,0 +1,50 @@ +package com.seoulection.admin.skinpolicy.infrastructure; + +import com.seoulection.admin.skinpolicy.domain.SkinScorePolicy; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "skin_score_policy") +public class SkinScorePolicyJpaEntity { + @Id @Column(name = "feature_key", length = 64) + private String featureKey; + @Column(nullable = false, length = 64) + private String label; + @Column(name = "survey_reliability", nullable = false) + private double surveyReliability; + @Column(name = "photo_reliability", nullable = false) + private double photoReliability; + @Column(name = "inconsistent_survey_factor", nullable = false) + private double inconsistentSurveyFactor; + @Column(name = "bad_boundary", nullable = false) + private double badBoundary; + @Column(name = "good_boundary", nullable = false) + private double goodBoundary; + @Column(name = "policy_version", nullable = false, length = 32) + private String policyVersion; + + protected SkinScorePolicyJpaEntity() {} + + private SkinScorePolicyJpaEntity(SkinScorePolicy policy) { + this.featureKey = policy.featureKey(); + this.label = policy.label(); + this.surveyReliability = policy.surveyReliability(); + this.photoReliability = policy.photoReliability(); + this.inconsistentSurveyFactor = policy.inconsistentSurveyFactor(); + this.badBoundary = policy.badBoundary(); + this.goodBoundary = policy.goodBoundary(); + this.policyVersion = policy.policyVersion(); + } + + public static SkinScorePolicyJpaEntity from(SkinScorePolicy policy) { + return new SkinScorePolicyJpaEntity(policy); + } + + public SkinScorePolicy toDomain() { + return new SkinScorePolicy(featureKey, label, surveyReliability, photoReliability, + inconsistentSurveyFactor, badBoundary, goodBoundary, policyVersion); + } +} diff --git a/src/main/java/com/seoulection/admin/skinpolicy/infrastructure/SkinScorePolicyJpaRepository.java b/src/main/java/com/seoulection/admin/skinpolicy/infrastructure/SkinScorePolicyJpaRepository.java new file mode 100644 index 0000000..1ee16cc --- /dev/null +++ b/src/main/java/com/seoulection/admin/skinpolicy/infrastructure/SkinScorePolicyJpaRepository.java @@ -0,0 +1,6 @@ +package com.seoulection.admin.skinpolicy.infrastructure; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SkinScorePolicyJpaRepository extends JpaRepository { +} diff --git a/src/main/java/com/seoulection/admin/skinpolicy/presentation/SkinScorePolicyController.java b/src/main/java/com/seoulection/admin/skinpolicy/presentation/SkinScorePolicyController.java new file mode 100644 index 0000000..b489c9c --- /dev/null +++ b/src/main/java/com/seoulection/admin/skinpolicy/presentation/SkinScorePolicyController.java @@ -0,0 +1,40 @@ +package com.seoulection.admin.skinpolicy.presentation; + +import com.seoulection.admin.skinpolicy.application.SkinScorePolicyService; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +@Controller +public class SkinScorePolicyController { + private final SkinScorePolicyService service; + + public SkinScorePolicyController(SkinScorePolicyService service) { + this.service = service; + } + + @GetMapping("/admin/skin-score-policy") + public String page(Model model) { + model.addAttribute("policies", service.findAll()); + return "skin-score-policy"; + } + + @PostMapping("/admin/skin-score-policy/{featureKey}") + public String update(@PathVariable String featureKey, + @RequestParam double badBoundary, + @RequestParam double goodBoundary, + @RequestParam String policyVersion, + RedirectAttributes redirectAttributes) { + try { + service.update(featureKey, badBoundary, goodBoundary, policyVersion); + redirectAttributes.addFlashAttribute("successMessage", featureKey + " 정책을 저장했습니다."); + } catch (IllegalArgumentException e) { + redirectAttributes.addFlashAttribute("errorMessage", e.getMessage()); + } + return "redirect:/admin/skin-score-policy"; + } +} diff --git a/src/main/resources/static/css/admin.css b/src/main/resources/static/css/admin.css index 9e9b8ac..e9acc41 100644 --- a/src/main/resources/static/css/admin.css +++ b/src/main/resources/static/css/admin.css @@ -1,5 +1,5 @@ /* - * Seoulection Admin — 공용 디자인 시스템. + * MySeoulection Admin — 공용 디자인 시스템. * * 이전엔 페이지마다(products.html, videos.html, ...)