From 2b3899b7d697e5ada1fee4c82bc6eeb603fedf5c Mon Sep 17 00:00:00 2001 From: sjungwon03 Date: Fri, 24 Jul 2026 17:20:43 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=A0=95=EC=B1=85=20=EB=AA=A9=EB=A1=9D?= =?UTF-8?q?/=EC=83=81=EC=84=B8=20=EC=9D=91=EB=8B=B5=EC=97=90=20=EC=9E=90?= =?UTF-8?q?=EA=B2=A9=EC=A1=B0=EA=B1=B4=20=EC=BD=94=EB=93=9C=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 프론트 추천 하드필터(buildRecommendations)가 결혼·전공·취업·학력·특화조건으로 부적격 정책을 제외하려면 정책 응답에 해당 코드가 있어야 하는데, PolicyCardResponse/ PolicyDetailResponse에는 이 필드가 없어 프론트 매퍼가 전부 '제한없음'으로 고정되고 있었다. Policy 엔티티는 이미 해당 필드를 갖고 있어 엔티티 변경 없이 두 응답 DTO에 jobCodes/schoolCodes/maritalStatusCode/majorCodes/specializationCodes를 추가하고 from(Policy) 매핑을 갱신했다. PolicyComparisonItemResponse는 여전히 비교표용 원문 필드만 노출하므로 관련 javadoc/문서만 최신화했다. Closes #92 Co-Authored-By: Claude Sonnet 5 --- docs/api-spec.md | 2 +- .../policy/dto/PolicyCardResponse.java | 13 +++++++++++++ .../dto/PolicyComparisonItemResponse.java | 5 +++-- .../policy/dto/PolicyDetailResponse.java | 15 ++++++++++++++- .../policy/controller/PolicyControllerTest.java | 10 ++++++++++ .../policy/service/PolicyServiceTest.java | 17 +++++++++++++++++ 6 files changed, 58 insertions(+), 4 deletions(-) diff --git a/docs/api-spec.md b/docs/api-spec.md index cc0ae27..4b69256 100644 --- a/docs/api-spec.md +++ b/docs/api-spec.md @@ -424,7 +424,7 @@ OAuth 인가 코드로 로그인을 완료하고 사용자 정보와 토큰을 ### 응답 필드 -`data`는 비교 대상 정책 배열이다. 각 원소는 `Policy` 엔티티 전체가 아니라 비교에 필요한 필드만 담은 값이며, **정책 상세 조회 응답의 부분집합으로 유지한다** — 같은 정책을 상세로 볼 때 감춰지는 값이 비교로 볼 때만 드러나면 안 되기 때문이다. 따라서 자격 판정용 내부 코드(`jobCodes`, `schoolCodes`, `majorCodes`, `specializationCodes`, `maritalStatusCode`)와 보류 필드, raw payload는 상세와 동일하게 노출하지 않는다. +`data`는 비교 대상 정책 배열이다. 각 원소는 `Policy` 엔티티 전체가 아니라 비교에 필요한 필드만 담은 값이며, **정책 상세 조회 응답의 부분집합으로 유지한다** — 같은 정책을 상세로 볼 때 감춰지는 값이 비교로 볼 때만 드러나면 안 되기 때문이다. 비교표는 자격 원문(추가 자격조건·참여 제한사항 등)만 나란히 보여주면 되므로, 상세 응답이 노출하는 자격 판정용 내부 코드(`jobCodes`, `schoolCodes`, `majorCodes`, `specializationCodes`, `maritalStatusCode` — #92부터 정책 목록/상세 응답에 노출)와 보류 필드, raw payload는 비교 응답에는 포함하지 않는다. | 필드 | 타입 | 내용 | |---|---|---| diff --git a/src/main/java/com/bop/youthpick/policy/dto/PolicyCardResponse.java b/src/main/java/com/bop/youthpick/policy/dto/PolicyCardResponse.java index ff733a2..6c3dac0 100644 --- a/src/main/java/com/bop/youthpick/policy/dto/PolicyCardResponse.java +++ b/src/main/java/com/bop/youthpick/policy/dto/PolicyCardResponse.java @@ -9,6 +9,9 @@ * *

{@code provinces}는 정책이 적용되는 시도명 목록(중복 제거·정렬)이다. 지역 매핑이 없으면 빈 리스트 — "전국/OO 외 N" 같은 표시 문구 조립은 * 프론트 책임이며, 상세({@link PolicyDetailResponse#regions()})와 같은 규칙을 쓴다. + * + *

자격 판정용 내부 코드(jobCodes/schoolCodes/majorCodes/specializationCodes/maritalStatusCode)는 목록 화면에서도 + * 프론트가 추천 하드필터(결혼·전공·신분 등 자격조건으로 부적격 정책 제외)를 적용할 수 있도록 상세와 동일하게 노출한다(#92). */ public record PolicyCardResponse( Long id, @@ -17,6 +20,11 @@ public record PolicyCardResponse( String description, Integer minAge, Integer maxAge, + String jobCodes, + String schoolCodes, + String maritalStatusCode, + String majorCodes, + String specializationCodes, LocalDate applicationEndDate, List provinces) { @@ -28,6 +36,11 @@ public static PolicyCardResponse from(Policy policy, List provinces) { policy.getDescription(), policy.getMinAge(), policy.getMaxAge(), + policy.getJobCodes(), + policy.getSchoolCodes(), + policy.getMaritalStatusCode(), + policy.getMajorCodes(), + policy.getSpecializationCodes(), policy.getApplicationEndDate(), provinces); } diff --git a/src/main/java/com/bop/youthpick/policy/dto/PolicyComparisonItemResponse.java b/src/main/java/com/bop/youthpick/policy/dto/PolicyComparisonItemResponse.java index 2f0bf69..1488c6d 100644 --- a/src/main/java/com/bop/youthpick/policy/dto/PolicyComparisonItemResponse.java +++ b/src/main/java/com/bop/youthpick/policy/dto/PolicyComparisonItemResponse.java @@ -8,8 +8,9 @@ * 비교표 한 열(정책 한 건)의 응답. 자격요건 원문 노출용 필드를 나열해 프론트가 항목별로 나란히 비교할 수 있게 한다. * *

필드 구성은 {@link PolicyDetailResponse}의 부분집합으로 유지한다 — 같은 정책을 상세로 볼 때는 감춰지는 값이 비교로 볼 때만 드러나면 안 되기 - * 때문이다. 따라서 자격 판정용 내부 코드(jobCodes/schoolCodes/majorCodes/specializationCodes/maritalStatusCode)와 보류 - * 필드, raw payload는 상세와 동일하게 노출하지 않는다. + * 때문이다. 비교표는 자격 원문(추가 자격조건·참여 제한사항 등)만 나란히 보여주면 되므로, 상세가 노출하는 자격 판정용 내부 코드 + * (jobCodes/schoolCodes/majorCodes/specializationCodes/maritalStatusCode, #92부터 상세에 노출)와 보류 필드, raw + * payload는 비교 응답에는 포함하지 않는다. */ public record PolicyComparisonItemResponse( Long policyId, diff --git a/src/main/java/com/bop/youthpick/policy/dto/PolicyDetailResponse.java b/src/main/java/com/bop/youthpick/policy/dto/PolicyDetailResponse.java index 310b788..10f0437 100644 --- a/src/main/java/com/bop/youthpick/policy/dto/PolicyDetailResponse.java +++ b/src/main/java/com/bop/youthpick/policy/dto/PolicyDetailResponse.java @@ -4,7 +4,10 @@ import java.time.LocalDate; import java.util.List; -/** 정책 상세 조회 응답 (유저 대상). 자격 판정용 내부 코드·보류 필드·raw payload는 노출하지 않는다. */ +/** + * 정책 상세 조회 응답 (유저 대상). 자격 판정용 내부 코드(jobCodes/schoolCodes/incomeConditionCode 등)는 프론트가 추천 + * 하드필터(결혼·전공·신분 등 자격조건으로 부적격 정책 제외)를 복구할 수 있도록 노출한다(#92). 보류 필드·raw payload는 계속 노출하지 않는다. + */ public record PolicyDetailResponse( Long id, String policyNo, @@ -17,9 +20,14 @@ public record PolicyDetailResponse( String organizationName, Integer minAge, Integer maxAge, + String jobCodes, + String schoolCodes, String incomeConditionCode, Integer incomeMaxAmount, String incomeEtcContent, + String maritalStatusCode, + String majorCodes, + String specializationCodes, String additionalQualification, String participationRestriction, String applicationPeriodType, @@ -52,9 +60,14 @@ public static PolicyDetailResponse from(Policy policy, List regi policy.getOrganizationName(), policy.getMinAge(), policy.getMaxAge(), + policy.getJobCodes(), + policy.getSchoolCodes(), policy.getIncomeConditionCode(), policy.getIncomeMaxAmount(), policy.getIncomeEtcContent(), + policy.getMaritalStatusCode(), + policy.getMajorCodes(), + policy.getSpecializationCodes(), policy.getAdditionalQualification(), policy.getParticipationRestriction(), policy.getApplicationPeriodType(), diff --git a/src/test/java/com/bop/youthpick/policy/controller/PolicyControllerTest.java b/src/test/java/com/bop/youthpick/policy/controller/PolicyControllerTest.java index 042ff5c..f0ba22b 100644 --- a/src/test/java/com/bop/youthpick/policy/controller/PolicyControllerTest.java +++ b/src/test/java/com/bop/youthpick/policy/controller/PolicyControllerTest.java @@ -48,9 +48,14 @@ class PolicyControllerTest { "국토교통부", 19, 34, + "0013010", + "0043010", null, null, null, + "0055010", + "0066010", + "0077010", null, null, null, @@ -83,6 +88,11 @@ void clearContext() { .andExpect(status().isOk()) .andExpect(jsonPath("$.data.id").value(1)) .andExpect(jsonPath("$.data.title").value("청년 월세 지원")) + .andExpect(jsonPath("$.data.jobCodes").value("0013010")) + .andExpect(jsonPath("$.data.schoolCodes").value("0043010")) + .andExpect(jsonPath("$.data.maritalStatusCode").value("0055010")) + .andExpect(jsonPath("$.data.majorCodes").value("0066010")) + .andExpect(jsonPath("$.data.specializationCodes").value("0077010")) .andExpect(jsonPath("$.data.regions[0].regionCode").value("11680")) .andExpect(jsonPath("$.data.regions[0].provinceName").value("서울특별시")) .andExpect(jsonPath("$.data.regions[0].districtName").value("강남구")); diff --git a/src/test/java/com/bop/youthpick/policy/service/PolicyServiceTest.java b/src/test/java/com/bop/youthpick/policy/service/PolicyServiceTest.java index 4dfda4e..4f7cba4 100644 --- a/src/test/java/com/bop/youthpick/policy/service/PolicyServiceTest.java +++ b/src/test/java/com/bop/youthpick/policy/service/PolicyServiceTest.java @@ -75,6 +75,12 @@ void setUp() { assertThat(response.id()).isEqualTo(1L); assertThat(response.title()).isEqualTo("청년 월세 지원"); assertThat(response.regions()).containsExactly(new RegionResponse("11680", "서울특별시", "강남구")); + // #92: 자격 판정용 내부 코드도 상세 응답에 노출해 프론트 추천 하드필터를 복구할 수 있게 한다. + assertThat(response.jobCodes()).isEqualTo("0013010"); + assertThat(response.schoolCodes()).isEqualTo("0043010"); + assertThat(response.maritalStatusCode()).isEqualTo("0055010"); + assertThat(response.majorCodes()).isEqualTo("0066010"); + assertThat(response.specializationCodes()).isEqualTo("0077010"); } @Test @@ -175,6 +181,12 @@ void setUp() { assertThat(card.id()).isEqualTo(1L); assertThat(card.title()).isEqualTo("한 시도 정책"); assertThat(card.category()).isEqualTo("주거"); + // #92: 목록(카드) 응답도 상세와 동일하게 자격 판정용 내부 코드를 노출해 프론트 추천 하드필터를 복구할 수 있게 한다. + assertThat(card.jobCodes()).isEqualTo("0013010"); + assertThat(card.schoolCodes()).isEqualTo("0043010"); + assertThat(card.maritalStatusCode()).isEqualTo("0055010"); + assertThat(card.majorCodes()).isEqualTo("0066010"); + assertThat(card.specializationCodes()).isEqualTo("0077010"); assertThat(page.getContent()) .extracting(PolicyCardResponse::provinces) .containsExactly( @@ -312,6 +324,11 @@ private Policy newPolicy(Long id, String title) { ReflectionTestUtils.setField(policy, "id", id); ReflectionTestUtils.setField(policy, "policyNo", "R2026" + id); ReflectionTestUtils.setField(policy, "title", title); + ReflectionTestUtils.setField(policy, "jobCodes", "0013010"); + ReflectionTestUtils.setField(policy, "schoolCodes", "0043010"); + ReflectionTestUtils.setField(policy, "maritalStatusCode", "0055010"); + ReflectionTestUtils.setField(policy, "majorCodes", "0066010"); + ReflectionTestUtils.setField(policy, "specializationCodes", "0077010"); return policy; }