From e0b6104c10660814e5c88c25ee4bbfbe4fc00fb0 Mon Sep 17 00:00:00 2001 From: yuyeol3 Date: Sun, 5 Apr 2026 01:15:22 +0900 Subject: [PATCH 01/16] =?UTF-8?q?feat=20:=20SearchController=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80,=20DynamicForm,=20MedianIncome=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EC=BD=94=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 --- .../search/controller/SearchController.java | 14 ++++++++ .../search/dto/DynamicFormResponseDto.java | 9 +++++ .../fin/search/dto/MedianIncomesDto.java | 13 +++++++ .../fin/search/entity/MedianIncomeEntity.java | 35 +++++++++++++++++++ .../repository/MedianIncomeRepository.java | 8 +++++ src/main/resources/schema.sql | 5 ++- 6 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 src/main/java/apptive/fin/search/controller/SearchController.java create mode 100644 src/main/java/apptive/fin/search/dto/DynamicFormResponseDto.java create mode 100644 src/main/java/apptive/fin/search/dto/MedianIncomesDto.java create mode 100644 src/main/java/apptive/fin/search/entity/MedianIncomeEntity.java create mode 100644 src/main/java/apptive/fin/search/repository/MedianIncomeRepository.java diff --git a/src/main/java/apptive/fin/search/controller/SearchController.java b/src/main/java/apptive/fin/search/controller/SearchController.java new file mode 100644 index 0000000..87c143d --- /dev/null +++ b/src/main/java/apptive/fin/search/controller/SearchController.java @@ -0,0 +1,14 @@ +package apptive.fin.search.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/search") +public class SearchController { + + + +} diff --git a/src/main/java/apptive/fin/search/dto/DynamicFormResponseDto.java b/src/main/java/apptive/fin/search/dto/DynamicFormResponseDto.java new file mode 100644 index 0000000..083192e --- /dev/null +++ b/src/main/java/apptive/fin/search/dto/DynamicFormResponseDto.java @@ -0,0 +1,9 @@ +package apptive.fin.search.dto; + +public record DynamicFormResponseDto( + boolean showTenure, + int yearlyEarnDefault, + boolean showBankInterestRateCheckList, + MedianIncomesDto medianIncomes +) { +} diff --git a/src/main/java/apptive/fin/search/dto/MedianIncomesDto.java b/src/main/java/apptive/fin/search/dto/MedianIncomesDto.java new file mode 100644 index 0000000..bcdc02a --- /dev/null +++ b/src/main/java/apptive/fin/search/dto/MedianIncomesDto.java @@ -0,0 +1,13 @@ +package apptive.fin.search.dto; + +public record MedianIncomesDto( + Integer year, + Integer householdSize, + Integer p60, + Integer p80, + Integer p100, + Integer p120, + Integer p150, + Integer p180 +) { +} diff --git a/src/main/java/apptive/fin/search/entity/MedianIncomeEntity.java b/src/main/java/apptive/fin/search/entity/MedianIncomeEntity.java new file mode 100644 index 0000000..aa77052 --- /dev/null +++ b/src/main/java/apptive/fin/search/entity/MedianIncomeEntity.java @@ -0,0 +1,35 @@ +package apptive.fin.search.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class MedianIncomeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "year", nullable = false) + private Integer year; + + @Column(name = "household_size", nullable = false) + private Integer householdSize; + + @Column(name = "earn_percent", nullable = false) + private Integer earnPercent; + + @Column(name = "monthly_income", nullable = false) + private Integer monthlyIncome; + + @Builder + public MedianIncomeEntity(int year, int householdSize, int earnPercent, int monthlyIncome) { + this.year = year; + this.householdSize = householdSize; + this.earnPercent = earnPercent; + this.monthlyIncome = monthlyIncome; + } + + +} diff --git a/src/main/java/apptive/fin/search/repository/MedianIncomeRepository.java b/src/main/java/apptive/fin/search/repository/MedianIncomeRepository.java new file mode 100644 index 0000000..475ff8d --- /dev/null +++ b/src/main/java/apptive/fin/search/repository/MedianIncomeRepository.java @@ -0,0 +1,8 @@ +package apptive.fin.search.repository; + + +import apptive.fin.search.dto.MedianIncomesDto; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MedianIncomeRepository extends JpaRepository { +} diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 0aa3b85..7c5a1c5 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -223,4 +223,7 @@ INSERT INTO term_versions ( - 동의 후에도 마이페이지 설정을 통해 언제든지 수신 거부 및 동의 철회가 가능합니다.$$, TRUE, '2026-03-12 00:00:00' - ); \ No newline at end of file + ); + + + From ec93439fc4cf416666b938790ddb4b9320fbf3d2 Mon Sep 17 00:00:00 2001 From: yuyeol3 Date: Sun, 5 Apr 2026 01:55:55 +0900 Subject: [PATCH 02/16] =?UTF-8?q?feat=20:=20MedianIncomeService=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../search/dto/DynamicFormResponseDto.java | 2 ++ .../fin/search/dto/MedianIncomesDto.java | 30 +++++++++++++++++ ...ianIncomeEntity.java => MedianIncome.java} | 5 +-- .../repository/MedianIncomeRepository.java | 6 +++- .../search/service/DynamicFormService.java | 13 ++++++++ .../search/service/MedianIncomeService.java | 32 +++++++++++++++++++ src/main/resources/schema.sql | 12 +++++++ 7 files changed, 97 insertions(+), 3 deletions(-) rename src/main/java/apptive/fin/search/entity/{MedianIncomeEntity.java => MedianIncome.java} (83%) create mode 100644 src/main/java/apptive/fin/search/service/DynamicFormService.java create mode 100644 src/main/java/apptive/fin/search/service/MedianIncomeService.java diff --git a/src/main/java/apptive/fin/search/dto/DynamicFormResponseDto.java b/src/main/java/apptive/fin/search/dto/DynamicFormResponseDto.java index 083192e..cf66d7b 100644 --- a/src/main/java/apptive/fin/search/dto/DynamicFormResponseDto.java +++ b/src/main/java/apptive/fin/search/dto/DynamicFormResponseDto.java @@ -6,4 +6,6 @@ public record DynamicFormResponseDto( boolean showBankInterestRateCheckList, MedianIncomesDto medianIncomes ) { + + } diff --git a/src/main/java/apptive/fin/search/dto/MedianIncomesDto.java b/src/main/java/apptive/fin/search/dto/MedianIncomesDto.java index bcdc02a..a16a6f3 100644 --- a/src/main/java/apptive/fin/search/dto/MedianIncomesDto.java +++ b/src/main/java/apptive/fin/search/dto/MedianIncomesDto.java @@ -1,5 +1,11 @@ package apptive.fin.search.dto; +import apptive.fin.search.entity.MedianIncome; +import lombok.Builder; + +import java.util.List; + +@Builder public record MedianIncomesDto( Integer year, Integer householdSize, @@ -10,4 +16,28 @@ public record MedianIncomesDto( Integer p150, Integer p180 ) { + public static MedianIncomesDto from(List medianIncomes) { + MedianIncomesDtoBuilder builder = MedianIncomesDto.builder(); + + builder.year(medianIncomes.getFirst().getYear()); + builder.householdSize(medianIncomes.getFirst().getHouseholdSize()); + + for (MedianIncome income : medianIncomes) { + switch (income.getEarnPercent()) { + case 60 -> builder.p60(income.getMonthlyIncome()); + case 80 -> builder.p80(income.getMonthlyIncome()); + case 100 -> builder.p100(income.getMonthlyIncome()); + case 120 -> builder.p120(income.getMonthlyIncome()); + case 150 -> builder.p150(income.getMonthlyIncome()); + case 180 -> builder.p180(income.getMonthlyIncome()); + } + } + + return builder.build(); + } + + public boolean isEmpty() { + return p60 == 0 && p80 == 0 && p100 == 0 + && p120 == 0 && p150 == 0 && p180 == 0; + } } diff --git a/src/main/java/apptive/fin/search/entity/MedianIncomeEntity.java b/src/main/java/apptive/fin/search/entity/MedianIncome.java similarity index 83% rename from src/main/java/apptive/fin/search/entity/MedianIncomeEntity.java rename to src/main/java/apptive/fin/search/entity/MedianIncome.java index aa77052..17a26fb 100644 --- a/src/main/java/apptive/fin/search/entity/MedianIncomeEntity.java +++ b/src/main/java/apptive/fin/search/entity/MedianIncome.java @@ -4,9 +4,10 @@ import lombok.*; @Entity +@Table(name = "median_incomes") @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter -public class MedianIncomeEntity { +public class MedianIncome { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -24,7 +25,7 @@ public class MedianIncomeEntity { private Integer monthlyIncome; @Builder - public MedianIncomeEntity(int year, int householdSize, int earnPercent, int monthlyIncome) { + public MedianIncome(int year, int householdSize, int earnPercent, int monthlyIncome) { this.year = year; this.householdSize = householdSize; this.earnPercent = earnPercent; diff --git a/src/main/java/apptive/fin/search/repository/MedianIncomeRepository.java b/src/main/java/apptive/fin/search/repository/MedianIncomeRepository.java index 475ff8d..ccf7368 100644 --- a/src/main/java/apptive/fin/search/repository/MedianIncomeRepository.java +++ b/src/main/java/apptive/fin/search/repository/MedianIncomeRepository.java @@ -2,7 +2,11 @@ import apptive.fin.search.dto.MedianIncomesDto; +import apptive.fin.search.entity.MedianIncome; import org.springframework.data.jpa.repository.JpaRepository; -public interface MedianIncomeRepository extends JpaRepository { +import java.util.List; + +public interface MedianIncomeRepository extends JpaRepository { + List findAllByYearAndHouseholdSize(int year, int householdSize); } diff --git a/src/main/java/apptive/fin/search/service/DynamicFormService.java b/src/main/java/apptive/fin/search/service/DynamicFormService.java new file mode 100644 index 0000000..3134d84 --- /dev/null +++ b/src/main/java/apptive/fin/search/service/DynamicFormService.java @@ -0,0 +1,13 @@ +package apptive.fin.search.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class DynamicFormService { + private final MedianIncomeService medianIncomeService; + + + +} diff --git a/src/main/java/apptive/fin/search/service/MedianIncomeService.java b/src/main/java/apptive/fin/search/service/MedianIncomeService.java new file mode 100644 index 0000000..b58481c --- /dev/null +++ b/src/main/java/apptive/fin/search/service/MedianIncomeService.java @@ -0,0 +1,32 @@ +package apptive.fin.search.service; + +import apptive.fin.search.dto.MedianIncomesDto; +import apptive.fin.search.entity.MedianIncome; +import apptive.fin.search.repository.MedianIncomeRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class MedianIncomeService { + private final MedianIncomeRepository medianIncomeRepository; + + @Cacheable(value = "medianIncome", unless = "#result.isEmpty()") + public MedianIncomesDto getMedianIncomesDto(int year, int householdSize) { + List medianIncomes = medianIncomeRepository.findAllByYearAndHouseholdSize(year, householdSize); + + if (medianIncomes.isEmpty()) { + return MedianIncomesDto.builder() + .year(year) + .householdSize(householdSize) + .p60(0).p80(0).p100(0).p120(0).p150(0).p180(0) + .build(); + } + + return MedianIncomesDto.from(medianIncomes); + } + +} diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 7c5a1c5..8af67a1 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -35,6 +35,18 @@ CREATE TABLE refresh_tokens ( CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id); CREATE INDEX idx_refresh_tokens_expires_at ON refresh_tokens(expires_at); +CREATE TABLE median_incomes ( + id BIGSERIAL PRIMARY KEY, + year INT NOT NULL CHECK (year > 0), + household_size INT NOT NULL CHECK (household_size > 0), + earn_percent INT NOT NULL CHECK (earn_percent > 0), + monthly_income INT NOT NULL CHECK (monthly_income >= 0), + + CONSTRAINT uq_year_household_size_earn_percent UNIQUE (year, household_size, earn_percent) +); + + + CREATE TABLE terms ( id BIGSERIAL PRIMARY KEY, code VARCHAR(100) NOT NULL UNIQUE, From dbcc8c405461c5adb616865f65326ca3b67a00a0 Mon Sep 17 00:00:00 2001 From: yuyeol3 Date: Sun, 5 Apr 2026 02:15:12 +0900 Subject: [PATCH 03/16] =?UTF-8?q?fix=20:=20DynamicFormResponse=20ageBound?= =?UTF-8?q?=C3=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fin/search/dto/DynamicFormResponseDto.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/main/java/apptive/fin/search/dto/DynamicFormResponseDto.java b/src/main/java/apptive/fin/search/dto/DynamicFormResponseDto.java index cf66d7b..c7dabd9 100644 --- a/src/main/java/apptive/fin/search/dto/DynamicFormResponseDto.java +++ b/src/main/java/apptive/fin/search/dto/DynamicFormResponseDto.java @@ -1,11 +1,22 @@ package apptive.fin.search.dto; +import lombok.Builder; + +@Builder public record DynamicFormResponseDto( - boolean showTenure, - int yearlyEarnDefault, - boolean showBankInterestRateCheckList, + Boolean showTenure, + Integer ageBound, + Integer yearlyEarnDefault, + Boolean showBankInterestRateCheckList, MedianIncomesDto medianIncomes ) { + public DynamicFormResponseDto { + if (showTenure == null) showTenure = true; + if (ageBound == null) ageBound = 34; + // if (yearlyEarnDefault == null); + if (showBankInterestRateCheckList == null) showBankInterestRateCheckList = false; + // if (medianIncomes == null) medianIncomes = null; + } } From 812cb0837b7842bd089c2b0d7ab8de8dccda4e65 Mon Sep 17 00:00:00 2001 From: yuyeol3 Date: Sun, 5 Apr 2026 14:49:58 +0900 Subject: [PATCH 04/16] =?UTF-8?q?fix=20:=20MedianIncomeService=20@Transact?= =?UTF-8?q?ional(readOnly=3Dtrue)=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/apptive/fin/search/dto/DynamicFormRequestDto.java | 4 ++++ .../java/apptive/fin/search/service/MedianIncomeService.java | 2 ++ 2 files changed, 6 insertions(+) create mode 100644 src/main/java/apptive/fin/search/dto/DynamicFormRequestDto.java diff --git a/src/main/java/apptive/fin/search/dto/DynamicFormRequestDto.java b/src/main/java/apptive/fin/search/dto/DynamicFormRequestDto.java new file mode 100644 index 0000000..91f5677 --- /dev/null +++ b/src/main/java/apptive/fin/search/dto/DynamicFormRequestDto.java @@ -0,0 +1,4 @@ +package apptive.fin.search.dto; + +public record DynamicFormRequestDto() { +} diff --git a/src/main/java/apptive/fin/search/service/MedianIncomeService.java b/src/main/java/apptive/fin/search/service/MedianIncomeService.java index b58481c..f5f8c6f 100644 --- a/src/main/java/apptive/fin/search/service/MedianIncomeService.java +++ b/src/main/java/apptive/fin/search/service/MedianIncomeService.java @@ -6,11 +6,13 @@ import lombok.RequiredArgsConstructor; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.List; @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class MedianIncomeService { private final MedianIncomeRepository medianIncomeRepository; From 0b37e11d4e1e29302f5a51fa2fb25c8b7170dd94 Mon Sep 17 00:00:00 2001 From: yuyeol3 Date: Sun, 5 Apr 2026 22:58:10 +0900 Subject: [PATCH 05/16] =?UTF-8?q?feat=20:=20=EB=8F=99=EC=A0=81=20=ED=8F=BC?= =?UTF-8?q?=20=EB=B0=8F=20=EC=A0=95=EB=B3=B4=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apptive/fin/search/KeywordValueEnum.java | 55 +++++++++++++++++++ .../search/controller/SearchController.java | 19 +++++++ .../fin/search/dto/DetailedOptionsDto.java | 18 ++++++ .../fin/search/dto/DynamicFormRequestDto.java | 18 +++++- .../fin/search/dto/OptionRequestDto.java | 9 +++ .../fin/search/dto/SearchRequestDto.java | 11 ++++ .../search/service/DynamicFormService.java | 43 +++++++++++++++ 7 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 src/main/java/apptive/fin/search/KeywordValueEnum.java create mode 100644 src/main/java/apptive/fin/search/dto/DetailedOptionsDto.java create mode 100644 src/main/java/apptive/fin/search/dto/OptionRequestDto.java create mode 100644 src/main/java/apptive/fin/search/dto/SearchRequestDto.java diff --git a/src/main/java/apptive/fin/search/KeywordValueEnum.java b/src/main/java/apptive/fin/search/KeywordValueEnum.java new file mode 100644 index 0000000..bfdafe8 --- /dev/null +++ b/src/main/java/apptive/fin/search/KeywordValueEnum.java @@ -0,0 +1,55 @@ +package apptive.fin.search; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum KeywordValueEnum { + + // 1. 거주 지역 + REGION_SEOUL("REGION_SEOUL"), + REGION_BUSAN("REGION_BUSAN"), + REGION_DAEGU("REGION_DAEGU"), + REGION_INCHEON("REGION_INCHEON"), + REGION_GWANGJU("REGION_GWANGJU"), + REGION_DAEJEON("REGION_DAEJEON"), + REGION_ULSAN("REGION_ULSAN"), + REGION_SEJONG("REGION_SEJONG"), + REGION_GYEONGGI("REGION_GYEONGGI"), + REGION_GANGWON("REGION_GANGWON"), + REGION_CHUNGBUK("REGION_CHUNGBUK"), + REGION_CHUNGNAM("REGION_CHUNGNAM"), + REGION_JEONBUK("REGION_JEONBUK"), + REGION_JEONNAM("REGION_JEONNAM"), + REGION_GYEONGBUK("REGION_GYEONGBUK"), + REGION_GYEONGNAM("REGION_GYEONGNAM"), + REGION_JEJU("REGION_JEJU"), + + // 2. 현재 신분 + STATUS_UNEMPLOYED("STATUS_UNEMPLOYED"), + STATUS_PART_TIME("STATUS_PART_TIME"), + STATUS_SME_WORKER("STATUS_SME_WORKER"), + STATUS_MILITARY("STATUS_MILITARY"), + + // 3. 저축 기간 + TERM_OVER_5_YEARS("TERM_OVER_5_YEARS"), + TERM_2_TO_3_YEARS("TERM_2_TO_3_YEARS"), + TERM_AROUND_1_YEAR("TERM_AROUND_1_YEAR"), + + // 4. 핵심 혜택 (핵심 기간) + BENEFIT_MAX_INTEREST("BENEFIT_MAX_INTEREST"), + BENEFIT_TAX_FREE("BENEFIT_TAX_FREE"), + BENEFIT_EASY_CONDITION("BENEFIT_EASY_CONDITION"), + BENEFIT_GOV_SUBSIDY("BENEFIT_GOV_SUBSIDY"), + + // 5. 상품 관심사 + INTEREST_SAVINGS("INTEREST_SAVINGS"), + INTEREST_LOAN("INTEREST_LOAN"), + + // 6. 은행 거래 + BANK_FIRST_TRANSACTION("BANK_FIRST_TRANSACTION"), + BANK_SALARY_TRANSFER("BANK_SALARY_TRANSFER"), + BANK_CARD_USAGE("BANK_CARD_USAGE"); + + private final String code; + +} diff --git a/src/main/java/apptive/fin/search/controller/SearchController.java b/src/main/java/apptive/fin/search/controller/SearchController.java index 87c143d..ade16f0 100644 --- a/src/main/java/apptive/fin/search/controller/SearchController.java +++ b/src/main/java/apptive/fin/search/controller/SearchController.java @@ -1,14 +1,33 @@ package apptive.fin.search.controller; +import apptive.fin.search.dto.DynamicFormResponseDto; +import apptive.fin.search.dto.OptionRequestDto; +import apptive.fin.search.dto.SearchRequestDto; +import apptive.fin.search.service.DynamicFormService; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.List; + @RestController @RequiredArgsConstructor @RequestMapping("/search") public class SearchController { + private final DynamicFormService dynamicFormService; + + @PostMapping("/dynamic-form") + public DynamicFormResponseDto dynamicForm(SearchRequestDto searchRequestDto) { + return dynamicFormService.calcFormCondition(searchRequestDto); + } + @PostMapping + public ResponseEntity search(SearchRequestDto searchRequestDto) { + return ResponseEntity.noContent().build(); + } } diff --git a/src/main/java/apptive/fin/search/dto/DetailedOptionsDto.java b/src/main/java/apptive/fin/search/dto/DetailedOptionsDto.java new file mode 100644 index 0000000..0b15d66 --- /dev/null +++ b/src/main/java/apptive/fin/search/dto/DetailedOptionsDto.java @@ -0,0 +1,18 @@ +package apptive.fin.search.dto; + +import java.time.LocalDate; +import java.util.List; + +public record DetailedOptionsDto( + LocalDate birthdate, + Long annualIncome, + Integer householdSize, + Integer householdIncomePercent, + Integer tenureMonths, + Boolean isFirstJob, + Boolean isHomeless, + Boolean isHouseholder, // 세대주 여부 + Long monthlySavingsGoal, + List mainBanks +) { +} diff --git a/src/main/java/apptive/fin/search/dto/DynamicFormRequestDto.java b/src/main/java/apptive/fin/search/dto/DynamicFormRequestDto.java index 91f5677..d74a65c 100644 --- a/src/main/java/apptive/fin/search/dto/DynamicFormRequestDto.java +++ b/src/main/java/apptive/fin/search/dto/DynamicFormRequestDto.java @@ -1,4 +1,20 @@ package apptive.fin.search.dto; -public record DynamicFormRequestDto() { +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import tools.jackson.databind.JsonNode; + +import java.util.List; + +public record DynamicFormRequestDto( + List<@Valid Option> options +) { + + public record Option( + @NotBlank String categoryName, + @NotBlank String optionName, + JsonNode optionValue + ) {} } diff --git a/src/main/java/apptive/fin/search/dto/OptionRequestDto.java b/src/main/java/apptive/fin/search/dto/OptionRequestDto.java new file mode 100644 index 0000000..ebf4c8b --- /dev/null +++ b/src/main/java/apptive/fin/search/dto/OptionRequestDto.java @@ -0,0 +1,9 @@ +package apptive.fin.search.dto; + +import jakarta.validation.constraints.NotNull; + +public record OptionRequestDto( + @NotNull Long categoryId, + @NotNull Long optionId +) { +} diff --git a/src/main/java/apptive/fin/search/dto/SearchRequestDto.java b/src/main/java/apptive/fin/search/dto/SearchRequestDto.java new file mode 100644 index 0000000..03a9736 --- /dev/null +++ b/src/main/java/apptive/fin/search/dto/SearchRequestDto.java @@ -0,0 +1,11 @@ +package apptive.fin.search.dto; + +import jakarta.validation.Valid; + +import java.util.List; + +public record SearchRequestDto( + List<@Valid OptionRequestDto> options, + DetailedOptionsDto detailedOptions +) { +} diff --git a/src/main/java/apptive/fin/search/service/DynamicFormService.java b/src/main/java/apptive/fin/search/service/DynamicFormService.java index 3134d84..ba7ef54 100644 --- a/src/main/java/apptive/fin/search/service/DynamicFormService.java +++ b/src/main/java/apptive/fin/search/service/DynamicFormService.java @@ -1,13 +1,56 @@ package apptive.fin.search.service; +import apptive.fin.search.KeywordValueEnum; +import apptive.fin.search.dto.DynamicFormRequestDto; +import apptive.fin.search.dto.DynamicFormResponseDto; +import apptive.fin.search.dto.OptionRequestDto; +import apptive.fin.search.dto.SearchRequestDto; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.time.LocalDateTime; +import java.util.List; + + @Service @RequiredArgsConstructor public class DynamicFormService { private final MedianIncomeService medianIncomeService; + // TODO : 지원님 코드 머지한 뒤에 enum 정의하고 입력받는 로직 만들기 + public DynamicFormResponseDto calcFormCondition(SearchRequestDto searchRequestDto) { + + List keywords = optionsToKeywords(searchRequestDto.options()); + var builder = DynamicFormResponseDto.builder(); + + for (KeywordValueEnum keyword : keywords) { + switch (keyword) { + // 현재 신분이 미취업이면 연소득 기본값을 0으로 설정한다. + case KeywordValueEnum.STATUS_UNEMPLOYED -> builder.yearlyEarnDefault(0); + // 현재 신분이 군복무이면 생년월일 상한을 39로 확장한다. + case KeywordValueEnum.STATUS_MILITARY -> builder.ageBound(39); + } + } + + // 사용자가 입력한 가구원 수에 따라 중위소득 데이터를 반환한다 + if (searchRequestDto.detailedOptions().householdSize() != null) + builder.medianIncomes( + medianIncomeService.getMedianIncomesDto( + LocalDateTime.now().getYear(), + searchRequestDto.detailedOptions().householdSize() + ) + ); + + // 주거래 은행을 선택하면 은행의 우대금리 목록을 노출한다 + if (searchRequestDto.detailedOptions().mainBanks() != null && + !searchRequestDto.detailedOptions().mainBanks().isEmpty() + ) + builder.showBankInterestRateCheckList(true); + + return builder.build(); + } + private List optionsToKeywords(List options) { + } } From 1e79c14ee6a2bcb4158d65717e60a0a751a8c178 Mon Sep 17 00:00:00 2001 From: yuyeol3 Date: Sun, 5 Apr 2026 23:00:48 +0900 Subject: [PATCH 06/16] =?UTF-8?q?fix=20:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20Dto=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fin/search/dto/DynamicFormRequestDto.java | 20 ------------------- .../search/service/DynamicFormService.java | 2 -- 2 files changed, 22 deletions(-) delete mode 100644 src/main/java/apptive/fin/search/dto/DynamicFormRequestDto.java diff --git a/src/main/java/apptive/fin/search/dto/DynamicFormRequestDto.java b/src/main/java/apptive/fin/search/dto/DynamicFormRequestDto.java deleted file mode 100644 index d74a65c..0000000 --- a/src/main/java/apptive/fin/search/dto/DynamicFormRequestDto.java +++ /dev/null @@ -1,20 +0,0 @@ -package apptive.fin.search.dto; - -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; -import tools.jackson.databind.JsonNode; - -import java.util.List; - -public record DynamicFormRequestDto( - List<@Valid Option> options -) { - - public record Option( - @NotBlank String categoryName, - @NotBlank String optionName, - JsonNode optionValue - ) {} -} diff --git a/src/main/java/apptive/fin/search/service/DynamicFormService.java b/src/main/java/apptive/fin/search/service/DynamicFormService.java index ba7ef54..b7c681c 100644 --- a/src/main/java/apptive/fin/search/service/DynamicFormService.java +++ b/src/main/java/apptive/fin/search/service/DynamicFormService.java @@ -1,7 +1,6 @@ package apptive.fin.search.service; import apptive.fin.search.KeywordValueEnum; -import apptive.fin.search.dto.DynamicFormRequestDto; import apptive.fin.search.dto.DynamicFormResponseDto; import apptive.fin.search.dto.OptionRequestDto; import apptive.fin.search.dto.SearchRequestDto; @@ -16,7 +15,6 @@ @RequiredArgsConstructor public class DynamicFormService { private final MedianIncomeService medianIncomeService; - // TODO : 지원님 코드 머지한 뒤에 enum 정의하고 입력받는 로직 만들기 public DynamicFormResponseDto calcFormCondition(SearchRequestDto searchRequestDto) { List keywords = optionsToKeywords(searchRequestDto.options()); From 4ed81e5854a74f997f83cc8648c648e7f641c8ee Mon Sep 17 00:00:00 2001 From: yuyeol3 Date: Mon, 6 Apr 2026 00:03:45 +0900 Subject: [PATCH 07/16] =?UTF-8?q?feat=20:=20=EC=9A=B0=EB=8C=80=EA=B8=88?= =?UTF-8?q?=EB=A6=AC=20=EC=A1=B0=EA=B1=B4=20=EB=8F=99=EC=A0=81=20=ED=8F=BC?= =?UTF-8?q?=20=EC=A1=B0=EA=B1=B4=20=EB=B0=8F=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/apptive/fin/search/dto/DetailedOptionsDto.java | 3 ++- .../apptive/fin/search/dto/DynamicFormResponseDto.java | 6 +++++- .../fin/search/dto/PreferentialInterestRateOption.java | 4 ++++ .../apptive/fin/search/service/DynamicFormService.java | 7 +++++++ 4 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 src/main/java/apptive/fin/search/dto/PreferentialInterestRateOption.java diff --git a/src/main/java/apptive/fin/search/dto/DetailedOptionsDto.java b/src/main/java/apptive/fin/search/dto/DetailedOptionsDto.java index 0b15d66..acdbafe 100644 --- a/src/main/java/apptive/fin/search/dto/DetailedOptionsDto.java +++ b/src/main/java/apptive/fin/search/dto/DetailedOptionsDto.java @@ -13,6 +13,7 @@ public record DetailedOptionsDto( Boolean isHomeless, Boolean isHouseholder, // 세대주 여부 Long monthlySavingsGoal, - List mainBanks + List mainBanks, + List selectedInterestRateOptions ) { } diff --git a/src/main/java/apptive/fin/search/dto/DynamicFormResponseDto.java b/src/main/java/apptive/fin/search/dto/DynamicFormResponseDto.java index c7dabd9..f1a4eaa 100644 --- a/src/main/java/apptive/fin/search/dto/DynamicFormResponseDto.java +++ b/src/main/java/apptive/fin/search/dto/DynamicFormResponseDto.java @@ -2,13 +2,16 @@ import lombok.Builder; +import java.util.List; + @Builder public record DynamicFormResponseDto( Boolean showTenure, Integer ageBound, Integer yearlyEarnDefault, Boolean showBankInterestRateCheckList, - MedianIncomesDto medianIncomes + MedianIncomesDto medianIncomes, + List preferentialInterestRateOptions ) { public DynamicFormResponseDto { @@ -17,6 +20,7 @@ public record DynamicFormResponseDto( // if (yearlyEarnDefault == null); if (showBankInterestRateCheckList == null) showBankInterestRateCheckList = false; // if (medianIncomes == null) medianIncomes = null; + if (preferentialInterestRateOptions == null) preferentialInterestRateOptions = List.of(); } } diff --git a/src/main/java/apptive/fin/search/dto/PreferentialInterestRateOption.java b/src/main/java/apptive/fin/search/dto/PreferentialInterestRateOption.java new file mode 100644 index 0000000..9dbd3c7 --- /dev/null +++ b/src/main/java/apptive/fin/search/dto/PreferentialInterestRateOption.java @@ -0,0 +1,4 @@ +package apptive.fin.search.dto; + +public record PreferentialInterestRateOption() { +} diff --git a/src/main/java/apptive/fin/search/service/DynamicFormService.java b/src/main/java/apptive/fin/search/service/DynamicFormService.java index b7c681c..93001cb 100644 --- a/src/main/java/apptive/fin/search/service/DynamicFormService.java +++ b/src/main/java/apptive/fin/search/service/DynamicFormService.java @@ -44,6 +44,13 @@ public DynamicFormResponseDto calcFormCondition(SearchRequestDto searchRequestDt ) builder.showBankInterestRateCheckList(true); + + // 추후 은행 상품으로 확장시 우대금리 조건 추가... +// if (searchRequestDto.detailedOptions().mainBanks() != null && +// !searchRequestDto.detailedOptions().mainBanks().isEmpty()) { +// +// } + return builder.build(); } From a517264b18259e1106d4d975613118e6e6fe9aeb Mon Sep 17 00:00:00 2001 From: yuyeol3 Date: Mon, 6 Apr 2026 00:16:50 +0900 Subject: [PATCH 08/16] =?UTF-8?q?fix=20:=20Transactional=20import=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/apptive/fin/category/service/CategoryService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/apptive/fin/category/service/CategoryService.java b/src/main/java/apptive/fin/category/service/CategoryService.java index ba9b909..4c91f78 100644 --- a/src/main/java/apptive/fin/category/service/CategoryService.java +++ b/src/main/java/apptive/fin/category/service/CategoryService.java @@ -4,6 +4,7 @@ import apptive.fin.category.repository.CategoryRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.*; import java.util.stream.Collectors; From f991704da9865c3c68f93ce5b2107c23d59e6402 Mon Sep 17 00:00:00 2001 From: yuyeol3 Date: Mon, 6 Apr 2026 00:27:26 +0900 Subject: [PATCH 09/16] =?UTF-8?q?feat:=20category=5Foption=EC=97=90=20code?= =?UTF-8?q?=20=EB=B6=80=EC=97=AC=ED=95=B4=20enum=EC=9D=84=20=ED=86=B5?= =?UTF-8?q?=ED=95=B4=20=EA=B5=AC=EB=B6=84=20=EA=B0=80=EB=8A=A5=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fin/category/entity/CategoryOption.java | 2 + src/main/resources/schema.sql | 132 ++++++++++++------ 2 files changed, 92 insertions(+), 42 deletions(-) diff --git a/src/main/java/apptive/fin/category/entity/CategoryOption.java b/src/main/java/apptive/fin/category/entity/CategoryOption.java index b58ac7d..45f0750 100644 --- a/src/main/java/apptive/fin/category/entity/CategoryOption.java +++ b/src/main/java/apptive/fin/category/entity/CategoryOption.java @@ -13,6 +13,8 @@ public class CategoryOption { private String value; + private String code; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "category_id") private Category category ; diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 9f10606..132c724 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -45,6 +45,54 @@ CREATE TABLE median_incomes ( CONSTRAINT uq_year_household_size_earn_percent UNIQUE (year, household_size, earn_percent) ); +INSERT INTO median_incomes (year, household_size, earn_percent, monthly_income) VALUES +-- 1인 가구 +(2026, 1, 60, 154), +(2026, 1, 80, 205), +(2026, 1, 100, 256), +(2026, 1, 120, 308), +(2026, 1, 150, 385), +(2026, 1, 180, 462), + +-- 2인 가구 +(2026, 2, 60, 252), +(2026, 2, 80, 336), +(2026, 2, 100, 420), +(2026, 2, 120, 504), +(2026, 2, 150, 630), +(2026, 2, 180, 756), + +-- 3인 가구 +(2026, 3, 60, 322), +(2026, 3, 80, 429), +(2026, 3, 100, 536), +(2026, 3, 120, 643), +(2026, 3, 150, 804), +(2026, 3, 180, 965), + +-- 4인 가구 +(2026, 4, 60, 390), +(2026, 4, 80, 520), +(2026, 4, 100, 649), +(2026, 4, 120, 779), +(2026, 4, 150, 974), +(2026, 4, 180, 1169), + +-- 5인 가구 +(2026, 5, 60, 453), +(2026, 5, 80, 605), +(2026, 5, 100, 756), +(2026, 5, 120, 907), +(2026, 5, 150, 1134), +(2026, 5, 180, 1360), + +-- 6인 이상 가구 +(2026, 6, 60, 513), +(2026, 6, 80, 684), +(2026, 6, 100, 856), +(2026, 6, 120, 1027), +(2026, 6, 150, 1283), +(2026, 6, 180, 1540); CREATE TABLE terms ( @@ -252,6 +300,7 @@ CREATE TABLE IF NOT EXISTS category_option ( id BIGSERIAL PRIMARY KEY, category_id BIGINT NOT NULL, value VARCHAR(100) NOT NULL, + code VARCHAR(100) NOT NULL, FOREIGN KEY (category_id) REFERENCES category(id) ); @@ -267,53 +316,52 @@ INSERT INTO category (name) VALUES -- 키워드 value 삽입 -- 1) 거주 지역 -INSERT INTO category_option (category_id, value) VALUES -(1, '서울'), -(1, '부산'), -(1, '대구'), -(1, '인천'), -(1, '광주'), -(1, '대전'), -(1, '울산'), -(1, '세종'), -(1, '경기'), -(1, '강원'), -(1, '충북'), -(1, '충남'), -(1, '전북'), -(1, '전남'), -(1, '경북'), -(1, '경남'), -(1, '제주'); +INSERT INTO category_option (category_id, value, code) VALUES + (1, '서울', 'REGION_SEOUL'), + (1, '부산', 'REGION_BUSAN'), + (1, '대구', 'REGION_DAEGU'), + (1, '인천', 'REGION_INCHEON'), + (1, '광주', 'REGION_GWANGJU'), + (1, '대전', 'REGION_DAEJEON'), + (1, '울산', 'REGION_ULSAN'), + (1, '세종', 'REGION_SEJONG'), + (1, '경기', 'REGION_GYEONGGI'), + (1, '강원', 'REGION_GANGWON'), + (1, '충북', 'REGION_CHUNGBUK'), + (1, '충남', 'REGION_CHUNGNAM'), + (1, '전북', 'REGION_JEONBUK'), + (1, '전남', 'REGION_JEONNAM'), + (1, '경북', 'REGION_GYEONGBUK'), + (1, '경남', 'REGION_GYEONGNAM'), + (1, '제주', 'REGION_JEJU'); -- 2) 현재 신분 -INSERT INTO category_option (category_id, value) VALUES -(2, '미취업'), -(2, '알바/프리랜서'), -(2, '중소기업 재직'), -(2, '군복무'); +INSERT INTO category_option (category_id, value, code) VALUES + (2, '미취업', 'STATUS_UNEMPLOYED'), + (2, '알바/프리랜서', 'STATUS_PART_TIME'), + (2, '중소기업 재직', 'STATUS_SME_WORKER'), + (2, '군복무', 'STATUS_MILITARY'); -- 3) 저축 기간 -INSERT INTO category_option (category_id, value) VALUES -(3, '5년 이상'), -(3, '2~3년'), -(3, '1년 내외'); - --- 4) 핵심 기간 -INSERT INTO category_option (category_id, value) VALUES -(4, '최고이율 중심'), -(4, '비과세'), -(4, '우대조건 간편'), -(4, '정부기여금'); +INSERT INTO category_option (category_id, value, code) VALUES + (3, '5년 이상', 'TERM_OVER_5_YEARS'), + (3, '2~3년', 'TERM_2_TO_3_YEARS'), + (3, '1년 내외', 'TERM_AROUND_1_YEAR'); + +-- 4) 핵심 혜택 +INSERT INTO category_option (category_id, value, code) VALUES + (4, '최고이율 중심', 'BENEFIT_MAX_INTEREST'), + (4, '비과세', 'BENEFIT_TAX_FREE'), + (4, '우대조건 간편', 'BENEFIT_EASY_CONDITION'), + (4, '정부기여금', 'BENEFIT_GOV_SUBSIDY'); -- 5) 상품 관심사 -INSERT INTO category_option (category_id, value) VALUES -(5, '저축'), -(5, '대출'); +INSERT INTO category_option (category_id, value, code) VALUES + (5, '저축', 'INTEREST_SAVINGS'), + (5, '대출', 'INTEREST_LOAN'); -- 6) 은행 거래 -INSERT INTO category_option (category_id, value) VALUES -(6, '첫거래 고객'), -(6, '급여이체 가능'), -(6, '카드실적 연동'); - +INSERT INTO category_option (category_id, value, code) VALUES + (6, '첫거래 고객', 'BANK_FIRST_TRANSACTION'), + (6, '급여이체 가능', 'BANK_SALARY_TRANSFER'), + (6, '카드실적 연동', 'BANK_CARD_USAGE'); From f8e5d2f9a950711e2dcc2c1f327a44d564a7ef4e Mon Sep 17 00:00:00 2001 From: yuyeol3 Date: Mon, 6 Apr 2026 00:52:45 +0900 Subject: [PATCH 10/16] =?UTF-8?q?feat=20:=20=EC=95=84=EC=9D=B4=EB=94=94?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=9B=EC=9D=80=20=ED=82=A4=EC=9B=8C=EB=93=9C=20?= =?UTF-8?q?=EC=98=B5=EC=85=98=EC=9D=84=20KeywordValueEnum=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/apptive/fin/FinApplication.java | 2 + .../repository/CategoryOptionRepository.java | 9 +++++ .../service/CategoryOptionService.java | 40 +++++++++++++++++++ .../apptive/fin/search/KeywordValueEnum.java | 8 ++++ .../search/service/DynamicFormService.java | 9 ++++- 5 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 src/main/java/apptive/fin/category/repository/CategoryOptionRepository.java create mode 100644 src/main/java/apptive/fin/category/service/CategoryOptionService.java diff --git a/src/main/java/apptive/fin/FinApplication.java b/src/main/java/apptive/fin/FinApplication.java index d644335..49b8991 100644 --- a/src/main/java/apptive/fin/FinApplication.java +++ b/src/main/java/apptive/fin/FinApplication.java @@ -4,10 +4,12 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cache.annotation.EnableCaching; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication @EnableJpaAuditing +@EnableCaching @ConfigurationPropertiesScan public class FinApplication { diff --git a/src/main/java/apptive/fin/category/repository/CategoryOptionRepository.java b/src/main/java/apptive/fin/category/repository/CategoryOptionRepository.java new file mode 100644 index 0000000..8a124ce --- /dev/null +++ b/src/main/java/apptive/fin/category/repository/CategoryOptionRepository.java @@ -0,0 +1,9 @@ +package apptive.fin.category.repository; + +import apptive.fin.category.entity.CategoryOption; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface CategoryOptionRepository extends JpaRepository { +} diff --git a/src/main/java/apptive/fin/category/service/CategoryOptionService.java b/src/main/java/apptive/fin/category/service/CategoryOptionService.java new file mode 100644 index 0000000..546623a --- /dev/null +++ b/src/main/java/apptive/fin/category/service/CategoryOptionService.java @@ -0,0 +1,40 @@ +package apptive.fin.category.service; + +import apptive.fin.category.entity.CategoryOption; +import apptive.fin.category.repository.CategoryOptionRepository; +import apptive.fin.category.repository.CategoryRepository; +import apptive.fin.search.KeywordValueEnum; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CategoryOptionService { + + private final CategoryOptionRepository categoryOptionRepository; + + @Cacheable(cacheNames = "keywordOptionMap") + public Map getOptionMap() { + List allOptions = categoryOptionRepository.findAll(); + Map map = new HashMap<>(); + + for (CategoryOption option : allOptions) { + if (option.getCode() != null && !option.getCode().isBlank()) { + KeywordValueEnum keyword = KeywordValueEnum.from(option.getCode()); + if (keyword != null) { + map.put(option.getId(), keyword); + } + } + } + + return map; + } + +} diff --git a/src/main/java/apptive/fin/search/KeywordValueEnum.java b/src/main/java/apptive/fin/search/KeywordValueEnum.java index bfdafe8..73c64dc 100644 --- a/src/main/java/apptive/fin/search/KeywordValueEnum.java +++ b/src/main/java/apptive/fin/search/KeywordValueEnum.java @@ -52,4 +52,12 @@ public enum KeywordValueEnum { private final String code; + public static KeywordValueEnum from(String code) { + try { + return KeywordValueEnum.valueOf(code); + } + catch (IllegalArgumentException e) { + return null; + } + } } diff --git a/src/main/java/apptive/fin/search/service/DynamicFormService.java b/src/main/java/apptive/fin/search/service/DynamicFormService.java index 93001cb..e9de293 100644 --- a/src/main/java/apptive/fin/search/service/DynamicFormService.java +++ b/src/main/java/apptive/fin/search/service/DynamicFormService.java @@ -1,5 +1,6 @@ package apptive.fin.search.service; +import apptive.fin.category.service.CategoryOptionService; import apptive.fin.search.KeywordValueEnum; import apptive.fin.search.dto.DynamicFormResponseDto; import apptive.fin.search.dto.OptionRequestDto; @@ -9,12 +10,15 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Map; @Service @RequiredArgsConstructor public class DynamicFormService { private final MedianIncomeService medianIncomeService; + private final CategoryOptionService categoryOptionService; + public DynamicFormResponseDto calcFormCondition(SearchRequestDto searchRequestDto) { List keywords = optionsToKeywords(searchRequestDto.options()); @@ -50,12 +54,15 @@ public DynamicFormResponseDto calcFormCondition(SearchRequestDto searchRequestDt // !searchRequestDto.detailedOptions().mainBanks().isEmpty()) { // // } - return builder.build(); } private List optionsToKeywords(List options) { + Map mapping = categoryOptionService.getOptionMap(); + return options.stream() + .map((e)->mapping.get(e.optionId())) + .toList(); } } From 5c2e85c14041960846edda3f304547bf22c5fbe4 Mon Sep 17 00:00:00 2001 From: yuyeol3 Date: Mon, 6 Apr 2026 01:02:56 +0900 Subject: [PATCH 11/16] =?UTF-8?q?fix=20:=20=EB=B3=80=ED=99=98=20=EC=8B=9C?= =?UTF-8?q?=20null=EB=A1=9C=20=EB=A7=A4=EC=B9=AD=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=A0=9C=EC=99=B8=EB=90=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=EB=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/apptive/fin/search/service/DynamicFormService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/apptive/fin/search/service/DynamicFormService.java b/src/main/java/apptive/fin/search/service/DynamicFormService.java index e9de293..69cfc5a 100644 --- a/src/main/java/apptive/fin/search/service/DynamicFormService.java +++ b/src/main/java/apptive/fin/search/service/DynamicFormService.java @@ -11,6 +11,7 @@ import java.time.LocalDateTime; import java.util.List; import java.util.Map; +import java.util.Objects; @Service @@ -62,6 +63,7 @@ private List optionsToKeywords(List options) return options.stream() .map((e)->mapping.get(e.optionId())) + .filter(Objects::nonNull) .toList(); } From f40ee51295842db9f7b296c1d645928eacf20a91 Mon Sep 17 00:00:00 2001 From: yuyeol3 Date: Mon, 6 Apr 2026 01:05:32 +0900 Subject: [PATCH 12/16] =?UTF-8?q?test=20:=20CategoryOptionService,=20Dynam?= =?UTF-8?q?icFormService,=20MedianIncomeService=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../category/CategoryOptionServiceTest.java | 71 ++++++++++ .../fin/search/DynamicFormServiceTest.java | 133 ++++++++++++++++++ .../fin/search/MedianIncomeServiceTest.java | 81 +++++++++++ 4 files changed, 286 insertions(+) create mode 100644 src/test/java/apptive/fin/category/CategoryOptionServiceTest.java create mode 100644 src/test/java/apptive/fin/search/DynamicFormServiceTest.java create mode 100644 src/test/java/apptive/fin/search/MedianIncomeServiceTest.java diff --git a/.gitignore b/.gitignore index 55608a8..f88cea6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ HELP.md .gradle build/ +.gradle-user/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ diff --git a/src/test/java/apptive/fin/category/CategoryOptionServiceTest.java b/src/test/java/apptive/fin/category/CategoryOptionServiceTest.java new file mode 100644 index 0000000..a6155e4 --- /dev/null +++ b/src/test/java/apptive/fin/category/CategoryOptionServiceTest.java @@ -0,0 +1,71 @@ +package apptive.fin.category; + +import apptive.fin.category.entity.CategoryOption; +import apptive.fin.category.repository.CategoryOptionRepository; +import apptive.fin.category.service.CategoryOptionService; +import apptive.fin.search.KeywordValueEnum; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CategoryOptionServiceTest { + + @Mock + private CategoryOptionRepository categoryOptionRepository; + + @InjectMocks + private CategoryOptionService categoryOptionService; + + @Test + void 유효한_코드가_있는_옵션만_키워드맵으로_반환한다() { + when(categoryOptionRepository.findAll()).thenReturn(List.of( + createOption(1L, "STATUS_UNEMPLOYED"), + createOption(2L, "STATUS_MILITARY"), + createOption(3L, null), + createOption(4L, " "), + createOption(5L, "NOT_EXISTING_KEYWORD") + )); + + Map result = categoryOptionService.getOptionMap(); + + assertThat(result).hasSize(2); + assertThat(result).containsEntry(1L, KeywordValueEnum.STATUS_UNEMPLOYED); + assertThat(result).containsEntry(2L, KeywordValueEnum.STATUS_MILITARY); + assertThat(result).doesNotContainKeys(3L, 4L, 5L); + + verify(categoryOptionRepository).findAll(); + } + + @Test + void 매핑가능한_코드가_없으면_빈_맵을_반환한다() { + when(categoryOptionRepository.findAll()).thenReturn(List.of( + createOption(1L, null), + createOption(2L, ""), + createOption(3L, "INVALID") + )); + + Map result = categoryOptionService.getOptionMap(); + + assertThat(result).isEmpty(); + + verify(categoryOptionRepository).findAll(); + } + + private CategoryOption createOption(Long id, String code) { + CategoryOption option = new CategoryOption(); + ReflectionTestUtils.setField(option, "id", id); + ReflectionTestUtils.setField(option, "code", code); + return option; + } +} diff --git a/src/test/java/apptive/fin/search/DynamicFormServiceTest.java b/src/test/java/apptive/fin/search/DynamicFormServiceTest.java new file mode 100644 index 0000000..c760ba6 --- /dev/null +++ b/src/test/java/apptive/fin/search/DynamicFormServiceTest.java @@ -0,0 +1,133 @@ +package apptive.fin.search; + +import apptive.fin.category.service.CategoryOptionService; +import apptive.fin.search.dto.DetailedOptionsDto; +import apptive.fin.search.dto.DynamicFormResponseDto; +import apptive.fin.search.dto.MedianIncomesDto; +import apptive.fin.search.dto.OptionRequestDto; +import apptive.fin.search.dto.SearchRequestDto; +import apptive.fin.search.service.DynamicFormService; +import apptive.fin.search.service.MedianIncomeService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class DynamicFormServiceTest { + + @Mock + private MedianIncomeService medianIncomeService; + + @Mock + private CategoryOptionService categoryOptionService; + + @InjectMocks + private DynamicFormService dynamicFormService; + + @Test + void 미취업_군복무_은행정보_가구원수를_포함하면_동적폼조건을_함께_반영한다() { + int currentYear = LocalDateTime.now().getYear(); + MedianIncomesDto medianIncomesDto = MedianIncomesDto.builder() + .year(currentYear) + .householdSize(3) + .p60(200) + .p80(300) + .p100(400) + .p120(500) + .p150(600) + .p180(700) + .build(); + SearchRequestDto request = new SearchRequestDto( + List.of( + new OptionRequestDto(1L, 10L), + new OptionRequestDto(2L, 20L) + ), + createDetailedOptions(3, List.of("KB국민은행")) + ); + + when(categoryOptionService.getOptionMap()).thenReturn(Map.of( + 10L, KeywordValueEnum.STATUS_UNEMPLOYED, + 20L, KeywordValueEnum.STATUS_MILITARY + )); + when(medianIncomeService.getMedianIncomesDto(currentYear, 3)).thenReturn(medianIncomesDto); + + DynamicFormResponseDto result = dynamicFormService.calcFormCondition(request); + + assertThat(result.yearlyEarnDefault()).isEqualTo(0); + assertThat(result.ageBound()).isEqualTo(39); + assertThat(result.showBankInterestRateCheckList()).isTrue(); + assertThat(result.medianIncomes()).isEqualTo(medianIncomesDto); + assertThat(result.showTenure()).isTrue(); + assertThat(result.preferentialInterestRateOptions()).isEmpty(); + + verify(medianIncomeService).getMedianIncomesDto(currentYear, 3); + } + + @Test + void 추가조건이_없으면_기본값으로_응답한다() { + SearchRequestDto request = new SearchRequestDto( + List.of(), + createDetailedOptions(null, List.of()) + ); + + when(categoryOptionService.getOptionMap()).thenReturn(Map.of()); + + DynamicFormResponseDto result = dynamicFormService.calcFormCondition(request); + + assertThat(result.showTenure()).isTrue(); + assertThat(result.ageBound()).isEqualTo(34); + assertThat(result.yearlyEarnDefault()).isNull(); + assertThat(result.showBankInterestRateCheckList()).isFalse(); + assertThat(result.medianIncomes()).isNull(); + assertThat(result.preferentialInterestRateOptions()).isEmpty(); + + verifyNoInteractions(medianIncomeService); + } + + @Test + void 키워드로_매핑되지_않는_옵션은_무시한다() { + SearchRequestDto request = new SearchRequestDto( + List.of(new OptionRequestDto(1L, 999L)), + createDetailedOptions(null, List.of()) + ); + + when(categoryOptionService.getOptionMap()).thenReturn(Map.of()); + + DynamicFormResponseDto result = dynamicFormService.calcFormCondition(request); + + assertThat(result.showTenure()).isTrue(); + assertThat(result.ageBound()).isEqualTo(34); + assertThat(result.yearlyEarnDefault()).isNull(); + assertThat(result.showBankInterestRateCheckList()).isFalse(); + + verifyNoInteractions(medianIncomeService); + } + + private DetailedOptionsDto createDetailedOptions(Integer householdSize, List mainBanks) { + return new DetailedOptionsDto( + LocalDate.of(2000, 1, 1), + 30_000_000L, + householdSize, + null, + null, + null, + null, + null, + null, + mainBanks, + List.of() + ); + } +} diff --git a/src/test/java/apptive/fin/search/MedianIncomeServiceTest.java b/src/test/java/apptive/fin/search/MedianIncomeServiceTest.java new file mode 100644 index 0000000..715c5ee --- /dev/null +++ b/src/test/java/apptive/fin/search/MedianIncomeServiceTest.java @@ -0,0 +1,81 @@ +package apptive.fin.search; + +import apptive.fin.search.dto.MedianIncomesDto; +import apptive.fin.search.entity.MedianIncome; +import apptive.fin.search.repository.MedianIncomeRepository; +import apptive.fin.search.service.MedianIncomeService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class MedianIncomeServiceTest { + + @Mock + private MedianIncomeRepository medianIncomeRepository; + + @InjectMocks + private MedianIncomeService medianIncomeService; + + @Test + void 중위소득_데이터가_없으면_0으로_채운_dto를_반환한다() { + when(medianIncomeRepository.findAllByYearAndHouseholdSize(2026, 2)).thenReturn(List.of()); + + MedianIncomesDto result = medianIncomeService.getMedianIncomesDto(2026, 2); + + assertThat(result.year()).isEqualTo(2026); + assertThat(result.householdSize()).isEqualTo(2); + assertThat(result.p60()).isZero(); + assertThat(result.p80()).isZero(); + assertThat(result.p100()).isZero(); + assertThat(result.p120()).isZero(); + assertThat(result.p150()).isZero(); + assertThat(result.p180()).isZero(); + assertThat(result.isEmpty()).isTrue(); + + verify(medianIncomeRepository).findAllByYearAndHouseholdSize(2026, 2); + } + + @Test + void 중위소득_데이터가_있으면_비율별_금액을_dto로_변환한다() { + when(medianIncomeRepository.findAllByYearAndHouseholdSize(2026, 3)).thenReturn(List.of( + createMedianIncome(2026, 3, 60, 1_200_000), + createMedianIncome(2026, 3, 80, 1_600_000), + createMedianIncome(2026, 3, 100, 2_000_000), + createMedianIncome(2026, 3, 120, 2_400_000), + createMedianIncome(2026, 3, 150, 3_000_000), + createMedianIncome(2026, 3, 180, 3_600_000) + )); + + MedianIncomesDto result = medianIncomeService.getMedianIncomesDto(2026, 3); + + assertThat(result.year()).isEqualTo(2026); + assertThat(result.householdSize()).isEqualTo(3); + assertThat(result.p60()).isEqualTo(1_200_000); + assertThat(result.p80()).isEqualTo(1_600_000); + assertThat(result.p100()).isEqualTo(2_000_000); + assertThat(result.p120()).isEqualTo(2_400_000); + assertThat(result.p150()).isEqualTo(3_000_000); + assertThat(result.p180()).isEqualTo(3_600_000); + assertThat(result.isEmpty()).isFalse(); + + verify(medianIncomeRepository).findAllByYearAndHouseholdSize(2026, 3); + } + + private MedianIncome createMedianIncome(int year, int householdSize, int earnPercent, int monthlyIncome) { + return MedianIncome.builder() + .year(year) + .householdSize(householdSize) + .earnPercent(earnPercent) + .monthlyIncome(monthlyIncome) + .build(); + } +} From 985778e29c05b92a66346abf55012922b4cc8875 Mon Sep 17 00:00:00 2001 From: yuyeol3 Date: Mon, 6 Apr 2026 01:08:09 +0900 Subject: [PATCH 13/16] =?UTF-8?q?fix=20:=20spring-boot-starter-cache=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle b/build.gradle index 1f2c06b..7d307c2 100644 --- a/build.gradle +++ b/build.gradle @@ -25,8 +25,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation "org.springframework.boot:spring-boot-starter-validation" implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-cache' implementation 'io.jsonwebtoken:jjwt-api:0.12.3' + runtimeOnly 'org.postgresql:postgresql' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3' From 7309f1e00b834e82c9735067ce121c34652a2849 Mon Sep 17 00:00:00 2001 From: yuyeol3 Date: Mon, 6 Apr 2026 01:23:11 +0900 Subject: [PATCH 14/16] =?UTF-8?q?fix=20:=20SearchController=20@RequestBody?= =?UTF-8?q?=20=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apptive/fin/search/controller/SearchController.java | 7 ++++--- src/main/java/apptive/fin/search/dto/SearchRequestDto.java | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/apptive/fin/search/controller/SearchController.java b/src/main/java/apptive/fin/search/controller/SearchController.java index ade16f0..ce9df2a 100644 --- a/src/main/java/apptive/fin/search/controller/SearchController.java +++ b/src/main/java/apptive/fin/search/controller/SearchController.java @@ -8,6 +8,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -21,13 +22,13 @@ public class SearchController { private final DynamicFormService dynamicFormService; @PostMapping("/dynamic-form") - public DynamicFormResponseDto dynamicForm(SearchRequestDto searchRequestDto) { + public DynamicFormResponseDto dynamicForm(@Valid @RequestBody SearchRequestDto searchRequestDto) { return dynamicFormService.calcFormCondition(searchRequestDto); } @PostMapping - public ResponseEntity search(SearchRequestDto searchRequestDto) { - return ResponseEntity.noContent().build(); + public SearchRequestDto search(@Valid @RequestBody SearchRequestDto searchRequestDto) { + return searchRequestDto; } } diff --git a/src/main/java/apptive/fin/search/dto/SearchRequestDto.java b/src/main/java/apptive/fin/search/dto/SearchRequestDto.java index 03a9736..1c0d4db 100644 --- a/src/main/java/apptive/fin/search/dto/SearchRequestDto.java +++ b/src/main/java/apptive/fin/search/dto/SearchRequestDto.java @@ -1,11 +1,12 @@ package apptive.fin.search.dto; import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; import java.util.List; public record SearchRequestDto( - List<@Valid OptionRequestDto> options, - DetailedOptionsDto detailedOptions + @NotNull List<@Valid OptionRequestDto> options, + @NotNull DetailedOptionsDto detailedOptions ) { } From fef96d5d6c808f74ed2d858ddcc2a4c5acff2512 Mon Sep 17 00:00:00 2001 From: yuyeol3 Date: Mon, 6 Apr 2026 01:33:30 +0900 Subject: [PATCH 15/16] =?UTF-8?q?fix=20:=20=EB=AF=B8=EC=B7=A8=EC=97=85=20?= =?UTF-8?q?=EC=8B=9C=20=EA=B7=BC=EC=86=8D=EC=97=B0=EC=88=98=20=EC=88=A8?= =?UTF-8?q?=EA=B8=B0=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/apptive/fin/search/service/DynamicFormService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/apptive/fin/search/service/DynamicFormService.java b/src/main/java/apptive/fin/search/service/DynamicFormService.java index 69cfc5a..9214756 100644 --- a/src/main/java/apptive/fin/search/service/DynamicFormService.java +++ b/src/main/java/apptive/fin/search/service/DynamicFormService.java @@ -28,7 +28,7 @@ public DynamicFormResponseDto calcFormCondition(SearchRequestDto searchRequestDt for (KeywordValueEnum keyword : keywords) { switch (keyword) { // 현재 신분이 미취업이면 연소득 기본값을 0으로 설정한다. - case KeywordValueEnum.STATUS_UNEMPLOYED -> builder.yearlyEarnDefault(0); + case KeywordValueEnum.STATUS_UNEMPLOYED -> builder.yearlyEarnDefault(0).showTenure(false); // 현재 신분이 군복무이면 생년월일 상한을 39로 확장한다. case KeywordValueEnum.STATUS_MILITARY -> builder.ageBound(39); } From 2c74f8f7c849ba90643a56302c92dc61d245846b Mon Sep 17 00:00:00 2001 From: yuyeol3 Date: Mon, 6 Apr 2026 01:40:31 +0900 Subject: [PATCH 16/16] =?UTF-8?q?test=20:=20DynamicFormService=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../search/service/DynamicFormService.java | 2 +- .../fin/search/DynamicFormServiceTest.java | 179 ++++++++++++++---- 2 files changed, 146 insertions(+), 35 deletions(-) diff --git a/src/main/java/apptive/fin/search/service/DynamicFormService.java b/src/main/java/apptive/fin/search/service/DynamicFormService.java index 9214756..fdd8dc2 100644 --- a/src/main/java/apptive/fin/search/service/DynamicFormService.java +++ b/src/main/java/apptive/fin/search/service/DynamicFormService.java @@ -27,7 +27,7 @@ public DynamicFormResponseDto calcFormCondition(SearchRequestDto searchRequestDt for (KeywordValueEnum keyword : keywords) { switch (keyword) { - // 현재 신분이 미취업이면 연소득 기본값을 0으로 설정한다. + // 현재 신분이 미취업이면 연소득 기본값을 0으로, 근속연수 메뉴를 숨기도록 설정한다. case KeywordValueEnum.STATUS_UNEMPLOYED -> builder.yearlyEarnDefault(0).showTenure(false); // 현재 신분이 군복무이면 생년월일 상한을 39로 확장한다. case KeywordValueEnum.STATUS_MILITARY -> builder.ageBound(39); diff --git a/src/test/java/apptive/fin/search/DynamicFormServiceTest.java b/src/test/java/apptive/fin/search/DynamicFormServiceTest.java index c760ba6..424ca1f 100644 --- a/src/test/java/apptive/fin/search/DynamicFormServiceTest.java +++ b/src/test/java/apptive/fin/search/DynamicFormServiceTest.java @@ -37,49 +37,114 @@ class DynamicFormServiceTest { private DynamicFormService dynamicFormService; @Test - void 미취업_군복무_은행정보_가구원수를_포함하면_동적폼조건을_함께_반영한다() { - int currentYear = LocalDateTime.now().getYear(); - MedianIncomesDto medianIncomesDto = MedianIncomesDto.builder() - .year(currentYear) - .householdSize(3) - .p60(200) - .p80(300) - .p100(400) - .p120(500) - .p150(600) - .p180(700) - .build(); - SearchRequestDto request = new SearchRequestDto( - List.of( - new OptionRequestDto(1L, 10L), - new OptionRequestDto(2L, 20L) - ), - createDetailedOptions(3, List.of("KB국민은행")) + void 미취업_옵션이면_연소득_기본값을_0으로_근속기간을_숨김으로_설정한다() { + SearchRequestDto request = createRequest( + List.of(new OptionRequestDto(1L, 10L)), + null, + List.of() ); when(categoryOptionService.getOptionMap()).thenReturn(Map.of( - 10L, KeywordValueEnum.STATUS_UNEMPLOYED, - 20L, KeywordValueEnum.STATUS_MILITARY + 10L, KeywordValueEnum.STATUS_UNEMPLOYED )); - when(medianIncomeService.getMedianIncomesDto(currentYear, 3)).thenReturn(medianIncomesDto); DynamicFormResponseDto result = dynamicFormService.calcFormCondition(request); assertThat(result.yearlyEarnDefault()).isEqualTo(0); + assertThat(result.showTenure()).isEqualTo(false); + assertThat(result.showTenure()).isFalse(); + assertThat(result.ageBound()).isEqualTo(34); + assertThat(result.showBankInterestRateCheckList()).isFalse(); + assertThat(result.medianIncomes()).isNull(); + assertThat(result.preferentialInterestRateOptions()).isEmpty(); + + verifyNoInteractions(medianIncomeService); + } + + @Test + void 군복무_옵션이면_나이상한을_39로_설정한다() { + SearchRequestDto request = createRequest( + List.of(new OptionRequestDto(1L, 20L)), + null, + List.of() + ); + + when(categoryOptionService.getOptionMap()).thenReturn(Map.of( + 20L, KeywordValueEnum.STATUS_MILITARY + )); + + DynamicFormResponseDto result = dynamicFormService.calcFormCondition(request); + assertThat(result.ageBound()).isEqualTo(39); - assertThat(result.showBankInterestRateCheckList()).isTrue(); + assertThat(result.yearlyEarnDefault()).isNull(); + assertThat(result.showBankInterestRateCheckList()).isFalse(); + assertThat(result.medianIncomes()).isNull(); + + verifyNoInteractions(medianIncomeService); + } + + @Test + void 가구원수가_있으면_현재연도와_가구원수로_중위소득을_조회한다() { + int currentYear = LocalDateTime.now().getYear(); + MedianIncomesDto medianIncomesDto = createMedianIncomesDto(currentYear, 3); + SearchRequestDto request = createRequest(List.of(), 3, List.of()); + + when(categoryOptionService.getOptionMap()).thenReturn(Map.of()); + when(medianIncomeService.getMedianIncomesDto(currentYear, 3)).thenReturn(medianIncomesDto); + + DynamicFormResponseDto result = dynamicFormService.calcFormCondition(request); + assertThat(result.medianIncomes()).isEqualTo(medianIncomesDto); - assertThat(result.showTenure()).isTrue(); - assertThat(result.preferentialInterestRateOptions()).isEmpty(); + assertThat(result.showBankInterestRateCheckList()).isFalse(); verify(medianIncomeService).getMedianIncomesDto(currentYear, 3); } @Test - void 추가조건이_없으면_기본값으로_응답한다() { - SearchRequestDto request = new SearchRequestDto( - List.of(), - createDetailedOptions(null, List.of()) + void 가구원수가_없으면_중위소득을_조회하지_않는다() { + SearchRequestDto request = createRequest(List.of(), null, List.of()); + + when(categoryOptionService.getOptionMap()).thenReturn(Map.of()); + + DynamicFormResponseDto result = dynamicFormService.calcFormCondition(request); + + assertThat(result.medianIncomes()).isNull(); + + verifyNoInteractions(medianIncomeService); + } + + @Test + void 주거래은행이_있으면_우대금리_체크리스트를_노출한다() { + SearchRequestDto request = createRequest(List.of(), null, List.of("KB")); + + when(categoryOptionService.getOptionMap()).thenReturn(Map.of()); + + DynamicFormResponseDto result = dynamicFormService.calcFormCondition(request); + + assertThat(result.showBankInterestRateCheckList()).isTrue(); + + verifyNoInteractions(medianIncomeService); + } + + @Test + void 주거래은행이_null이면_우대금리_체크리스트를_노출하지_않는다() { + SearchRequestDto request = createRequest(List.of(), null, null); + + when(categoryOptionService.getOptionMap()).thenReturn(Map.of()); + + DynamicFormResponseDto result = dynamicFormService.calcFormCondition(request); + + assertThat(result.showBankInterestRateCheckList()).isFalse(); + + verifyNoInteractions(medianIncomeService); + } + + @Test + void 키워드로_매핑되지_않는_옵션은_무시한다() { + SearchRequestDto request = createRequest( + List.of(new OptionRequestDto(1L, 999L)), + null, + List.of() ); when(categoryOptionService.getOptionMap()).thenReturn(Map.of()); @@ -91,17 +156,13 @@ class DynamicFormServiceTest { assertThat(result.yearlyEarnDefault()).isNull(); assertThat(result.showBankInterestRateCheckList()).isFalse(); assertThat(result.medianIncomes()).isNull(); - assertThat(result.preferentialInterestRateOptions()).isEmpty(); verifyNoInteractions(medianIncomeService); } @Test - void 키워드로_매핑되지_않는_옵션은_무시한다() { - SearchRequestDto request = new SearchRequestDto( - List.of(new OptionRequestDto(1L, 999L)), - createDetailedOptions(null, List.of()) - ); + void 추가조건이_없으면_기본값으로_응답한다() { + SearchRequestDto request = createRequest(List.of(), null, List.of()); when(categoryOptionService.getOptionMap()).thenReturn(Map.of()); @@ -111,10 +172,47 @@ class DynamicFormServiceTest { assertThat(result.ageBound()).isEqualTo(34); assertThat(result.yearlyEarnDefault()).isNull(); assertThat(result.showBankInterestRateCheckList()).isFalse(); + assertThat(result.medianIncomes()).isNull(); + assertThat(result.preferentialInterestRateOptions()).isEmpty(); verifyNoInteractions(medianIncomeService); } + @Test + void 복합조건이_들어오면_여러_분기를_동시에_반영한다() { + int currentYear = LocalDateTime.now().getYear(); + MedianIncomesDto medianIncomesDto = createMedianIncomesDto(currentYear, 3); + SearchRequestDto request = createRequest( + List.of( + new OptionRequestDto(1L, 10L), + new OptionRequestDto(2L, 20L) + ), + 3, + List.of("KB") + ); + + when(categoryOptionService.getOptionMap()).thenReturn(Map.of( + 10L, KeywordValueEnum.STATUS_UNEMPLOYED, + 20L, KeywordValueEnum.STATUS_MILITARY + )); + when(medianIncomeService.getMedianIncomesDto(currentYear, 3)).thenReturn(medianIncomesDto); + + DynamicFormResponseDto result = dynamicFormService.calcFormCondition(request); + + assertThat(result.yearlyEarnDefault()).isEqualTo(0); + assertThat(result.ageBound()).isEqualTo(39); + assertThat(result.showBankInterestRateCheckList()).isTrue(); + assertThat(result.medianIncomes()).isEqualTo(medianIncomesDto); + assertThat(result.showTenure()).isFalse(); + assertThat(result.preferentialInterestRateOptions()).isEmpty(); + + verify(medianIncomeService).getMedianIncomesDto(currentYear, 3); + } + + private SearchRequestDto createRequest(List options, Integer householdSize, List mainBanks) { + return new SearchRequestDto(options, createDetailedOptions(householdSize, mainBanks)); + } + private DetailedOptionsDto createDetailedOptions(Integer householdSize, List mainBanks) { return new DetailedOptionsDto( LocalDate.of(2000, 1, 1), @@ -130,4 +228,17 @@ private DetailedOptionsDto createDetailedOptions(Integer householdSize, List