Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e654535
📂 file : 공고 유즈케이스 폴더화
doup2001 Aug 18, 2025
1abd1f8
✨feat : 청약 진단 기능 1차 구현
doup2001 Aug 18, 2025
bad3b11
✨feat : Rule 기반 청약 진단 2차 기능 구현
doup2001 Aug 18, 2025
2168f26
♻️ refactor : Rule 기반 청약진단 클래스 정리
doup2001 Aug 19, 2025
3c69140
♻️ refactor : Rule 기반 주석 추가 및 기능 수정
doup2001 Aug 19, 2025
263bacb
📂 file : 폴더 내 파일 이동
doup2001 Aug 25, 2025
2a4b802
✨ feat : 청약통장 기간 관련 enum 추가
doup2001 Aug 25, 2025
e747b8e
✨ feat : 학교 관련 기능 구현
doup2001 Aug 25, 2025
7270a42
✨ feat : 와이어프레임에 따른 도메인 수정
doup2001 Aug 25, 2025
b7c2046
✨ feat : 청약 질문을 받기 위한 로직
doup2001 Aug 25, 2025
dc1fb9e
📂 file : 도메인 이름 수정
doup2001 Aug 25, 2025
b388490
✨ feat : 규칙 조건 수정
doup2001 Aug 25, 2025
5716d5e
✨ feat : 질문 내용 도메인 수정
doup2001 Aug 25, 2025
d10b4ff
✨ feat : Rule 수정
doup2001 Aug 25, 2025
4c1420c
✨ feat : 진단 도메인 수정
doup2001 Sep 4, 2025
3a7724d
✨ feat : 학교 도메인 정의
doup2001 Sep 4, 2025
b40add6
✨ feat : 검색 유즈케이스 정의 및 검색 엔지 수정
doup2001 Sep 4, 2025
de74030
📂 file : 청약진단 1차 재구조화
doup2001 Sep 4, 2025
94b6d4c
📂 file : 청약진단 포함 2차 재구조화
doup2001 Sep 4, 2025
684f6e3
✨ feat : 청약 진단 부가 설명 도메인 정의
doup2001 Sep 4, 2025
77f342f
♻️ refactor : 청약 진단 부가 파일 수정
doup2001 Sep 4, 2025
e40fc74
✨feat : 질문 설명 API 기능 1차 구현
doup2001 Sep 4, 2025
98ff1ff
📂 file : 질문 설명 및 질문에 대한 파일 통합
doup2001 Sep 4, 2025
664f41f
📂 file : Rule 파일 위치 이동
doup2001 Sep 4, 2025
4ff8991
♻️ refactor : 진단 관련 지역 거주 요건 제거
doup2001 Sep 4, 2025
800f368
♻️ refactor : 진단 결과 응답 포맷 수정
doup2001 Sep 4, 2025
c2ee9be
♻️ refactor : 진단 로직 단순화 구현중
doup2001 Sep 6, 2025
88bc64e
✨ feat : 진단 로직 1차 구현
doup2001 Sep 7, 2025
428fa01
✨ feat : 신생아 공급 및 한부모가정 Rule 추가
doup2001 Sep 7, 2025
0027375
✨ feat : 특수계층 추가 구현
doup2001 Sep 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/main/java/com/pinHouse/server/core/util/BirthDayUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.time.Period;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;

Expand All @@ -23,6 +24,18 @@ public static LocalDate parseBirthday(String year, String monthDay) {
}
}

/**
* 생년월일로 나이 계산 (만 나이)
* @param birthday LocalDate 형태의 생년월일
*/
public static int calculateAge(LocalDate birthday) {
if (birthday == null) {
throw new IllegalArgumentException("생년월일이 null일 수 없습니다.");
}
LocalDate today = LocalDate.now();
return Period.between(birthday, today).getYears();
}



}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.pinHouse.server.core.entity;
package com.pinHouse.server.platform;

import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.pinHouse.server.platform.housing.facility.domain.entity;

import com.pinHouse.server.core.entity.Location;
import com.pinHouse.server.platform.region.domain.entity.Location;
import com.pinHouse.server.platform.housing.facility.domain.entity.infra.Facility;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.pinHouse.server.platform.housing.facility.domain.entity;

import com.pinHouse.server.core.entity.Location;
import com.pinHouse.server.platform.region.domain.entity.Location;
import com.pinHouse.server.platform.housing.facility.domain.entity.infra.Facility;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.pinHouse.server.platform.housing.facility.domain.entity;

import com.pinHouse.server.core.entity.Location;
import com.pinHouse.server.platform.region.domain.entity.Location;
import com.pinHouse.server.platform.housing.facility.domain.entity.infra.Facility;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.GeoSpatialIndexType;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.pinHouse.server.platform.housing.facility.domain.entity;

import com.pinHouse.server.core.entity.Location;
import com.pinHouse.server.platform.region.domain.entity.Location;
import com.pinHouse.server.platform.housing.facility.domain.entity.infra.Facility;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.pinHouse.server.platform.housing.facility.domain.entity;

import com.pinHouse.server.core.entity.Location;
import com.pinHouse.server.platform.region.domain.entity.Location;
import com.pinHouse.server.platform.housing.facility.domain.entity.infra.Facility;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.GeoSpatialIndexType;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.pinHouse.server.platform.housing.facility.domain.entity.infra;

import com.pinHouse.server.core.entity.Location;
import com.pinHouse.server.platform.region.domain.entity.Location;

public interface Facility {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.pinHouse.server.platform.housing.notice.domain.entity;

import com.pinHouse.server.core.entity.Location;
import com.pinHouse.server.platform.region.domain.entity.Location;
import com.pinHouse.server.platform.housing.deposit.domain.entity.NoticeSupply;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.pinHouse.server.platform.housingFit.diagnosis.application.dto.request;

import com.pinHouse.server.platform.housingFit.diagnosis.domain.entity.*;
import com.pinHouse.server.platform.user.domain.entity.Gender;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;

import java.time.LocalDate;
import java.util.List;

@Data
@Builder
@AllArgsConstructor
@Schema(description = "진단 요청 DTO")
public class DiagnosisRequest {

/** 1) 기초 자격: 성별 */
@Schema(description = "성별", example = "남성")
private Gender gender; // 성별

/** 2) 기초 자격: 나이 */
@Schema(description = "생일", example = "2000-06-15")
private LocalDate birthday; // 생일

/** 5-6) 소득 요건(일자리 종사) */
@Schema(description = "월 소득", example = "2000000")
private int monthPay; // 소득 여부

/** 7-10) 청약통장 요건(가입기간/예치금/상품유형) */
@Schema(description = "청약통장 보유 여부", example = "true")
private boolean hasAccount;

@Schema(description = "청약통장 가입 기간", example = "6개월 이상 ~ 1년 미만")
private SubscriptionPeriod accountYears; // 가입 년수(년)

@Schema(description = "청약통장 납입 횟수", example = "6회 ~ 11회")
private SubscriptionCount accountDeposit; // 가입 횟수

@Schema(description = "청약통장 금액", example = "600만원 이상")
private SubscriptionAccount account; // 가입 금액

/** 11) 신혼 부부 요건 */
@Schema(description = "결혼 여부", example = "false")
private boolean maritalStatus; // 결혼 여부

@Schema(description = "결혼 기간(년)", example = "0")
private Integer marriageYears; // 결혼 기간

/** 12) 자녀(다자녀) 요건 */
@Schema(description = "태아 수", example = "0")
private int unbornChildrenCount; // 태아 수

@Schema(description = "6세 이하 자녀 수", example = "0")
private int under6ChildrenCount; // 6세 이하 자녀 수

@Schema(description = "7세 이상 미성년 자녀 수", example = "0")
private int over7MinorChildrenCount; // 7세 이상 미성년 자녀 수

/** 14) 대학생 요건 */
@Schema(description = "교육 상태", example = "대학교 휴학 중이며 다음 학기 복학 예정")
private EducationStatus educationStatus; // 학생 정보

@Schema(description = "자동차 보유 여부", example = "false")
private boolean hasCar; // 자동차 소유 여부

@Schema(description = "자동차 가격", example = "0")
private long carValue; // 자동차 가격

/** 16-17) 세대 관련 정보 */
@Schema(description = "세대주 여부", example = "true")
private boolean isHouseholdHead; // 세대원, 세대주

@Schema(description = "1인 가구 여부", example = "true")
private boolean isSingle; // 1인 가구 여부 - false면 가족들과 거주

@Schema(description = "태아 가구 수", example = "0")
private int fetusCount; // 태아 가구수

@Schema(description = "미성년자 가구 수", example = "0")
private int minorCount; // 미성년자 가구수

@Schema(description = "성인 가구 수", example = "1")
private int adultCount; // 성인 가구수

/** 18) 세대 소득 요건 */
@Schema(description = "가구 소득 수준", example = "2구간")
private IncomeLevel incomeLevel; // 가구 소득

/** 19) 세대 주택 요건 */
@Schema(description = "주택 소유 상태", example = "우리집 가구원 모두 주택을 소유하고 있지 않아요")
private HousingOwnershipStatus housingStatus; // 주택 소유 여부

@Schema(description = "무주택 기간(년)", example = "3")
private int housingYears; // 무주택 기간 여부

/** 20-22) 세대 자산 요건 */
@Schema(description = "부동산/토지 자산", example = "0")
private long propertyAsset; // 부동산/토지 자산

@Schema(description = "자동차 자산", example = "0")
private long carAsset; // 자동차 가격

@Schema(description = "금융 자산", example = "1000000")
private long financialAsset; // 금융자산

/** 최종) 특수 계층 요건 */
@Schema(description = "특수 계층 목록", example = "[\"주거급여 수급자\"]")
private List<SpecialCategory> hasSpecialCategory; // 특수 계층인지

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.pinHouse.server.platform.housingFit.diagnosis.application.dto.response;

import com.pinHouse.server.platform.housingFit.diagnosis.domain.entity.Diagnosis;
import com.pinHouse.server.platform.housingFit.rule.application.dto.response.RuleResult;
import com.pinHouse.server.platform.housingFit.rule.domain.entity.EvaluationContext;
import com.pinHouse.server.platform.housingFit.rule.domain.entity.SupplyType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;

import java.util.List;
import java.util.UUID;

/**
* 최종 진단 응답 DTO
* - 어떤 Rule들을 통과/실패했는지
* - 최종 적합 여부
* - 요약 메시지
*/
@Data
@Builder
@AllArgsConstructor
public class DiagnosisResponse {

private Long id; // 진단 기록 ID
private UUID userId; // 유저 ID

private boolean eligible; // 최종 자격 여부
private String decisionMessage; // 최종 요약 메시지 ("국민임대주택 가능", "부적합" 등)
private List<String> recommended; // 추천 후보 리스트

/// 정적 팩토리 메서드
public static DiagnosisResponse from(EvaluationContext context) {

Diagnosis diagnosis = context.getDiagnosis();

// 실패 이유만 추출
List<String> failureReasons = context.getRuleResults().stream()
.filter(r -> !r.pass())
.map(RuleResult::message)
.toList();

// 추천 후보 (후보군이 남아있으면 추천, 없으면 "해당 없음")
List<String> recommended = context.getCurrentCandidates().isEmpty() ?
List.of("해당 없음") :
context.getCurrentCandidates().stream()
.map((c -> c.rentalType().getValue() + " : " + c.supplyType().getValue()))
.toList();

return DiagnosisResponse.builder()
.id(diagnosis.getId())
.userId(diagnosis.getUser().getId())
.eligible(!recommended.contains("해당 없음"))
.decisionMessage(!recommended.contains("해당 없음") ?
"추천 임대주택이 있습니다" : "모든 조건 미충족")
.recommended(recommended)
.build();
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.pinHouse.server.platform.housingFit.diagnosis.application.service;

import com.pinHouse.server.core.response.response.ErrorCode;
import com.pinHouse.server.platform.housingFit.diagnosis.application.dto.request.DiagnosisRequest;
import com.pinHouse.server.platform.housingFit.diagnosis.application.dto.response.DiagnosisResponse;
import com.pinHouse.server.platform.housingFit.diagnosis.application.usecase.DiagnosisUseCase;
import com.pinHouse.server.platform.housingFit.diagnosis.domain.repository.DiagnosisJpaRepository;
import com.pinHouse.server.platform.housingFit.rule.domain.entity.EvaluationContext;
import com.pinHouse.server.platform.housingFit.diagnosis.domain.entity.Diagnosis;
import com.pinHouse.server.platform.housingFit.rule.application.usecase.RuleChainUseCase;
import com.pinHouse.server.platform.user.application.usecase.UserUseCase;
import com.pinHouse.server.platform.user.domain.entity.User;
import lombok.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.NoSuchElementException;
import java.util.UUID;

/**
* 청약 진단 서비스
* - 단계적 파이프라인(기초자격 → 지역/통장 → 소득/자산 → 특별공급별 규칙 → 일반공급/가점 → 우선순위 산정)
* - 규칙(Rule) 인터페이스 + 체인(Chain of Responsibility)
* - 정책(Policy) 외부화: Threshold들을 Provider에서 주입(향후 YAML/DB/어드민에서 변경 가능)
* - Explainable Result: 모든 통과/실패 사유를 코드 + 메시지 + 세부값으로 축적하여 응답으로 반환
*/
@Service
@Transactional
@RequiredArgsConstructor
public class DiagnosisService implements DiagnosisUseCase {

/// 의존성
private final DiagnosisJpaRepository repository;

/// 외부 의존성
private final UserUseCase userService;
private final RuleChainUseCase ruleChain;

/**
* 임대주택 진단하기
* @param userId 진단할 유저
* @param request 요청 DTO
* @return 진단 DTO
*/
@Override
public DiagnosisResponse diagnose(UUID userId, DiagnosisRequest request) {

/// 유저 예외 처리
User user = getUser(userId);

/// 진단 도메인 생성
var diagnosis = Diagnosis.of(user, request);
Diagnosis entity = repository.save(diagnosis);

/// 작성한 내용을 바탕으로 진단 실행
EvaluationContext context = ruleChain.evaluateAll(entity);

/// DTO 생성
return DiagnosisResponse.from(context);
}

/**
* 나의 최근 청약진단 가져오기
* @param userId 유저ID
* @return 청약진단 DTO
*/
@Override
public DiagnosisResponse getDiagnose(UUID userId) {

/// 유저 예외 처리
User user = getUser(userId);

/// DB에서 결과 조회하기, 영속성 컨텍스트에 저장
Diagnosis diagnosis = repository.findByUser(user);

/// 작성한 내용을 바탕으로 진단 실행 (규칙에 따라서 바뀔 수 있기에 매번 실행하도록 수정)
EvaluationContext context = ruleChain.evaluateAll(diagnosis);

/// DTO 생성
return DiagnosisResponse.from(context);
}


/**
* 나의 진단 목록 결과하기
* @param userId 유저 ID
*/
private User getUser(UUID userId) {
return userService.loadUserById(userId)
.orElseThrow(() -> new NoSuchElementException(ErrorCode.USER_NOT_FOUND.getMessage()));
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.pinHouse.server.platform.housingFit.diagnosis.application.usecase;

import com.pinHouse.server.platform.housingFit.diagnosis.application.dto.request.DiagnosisRequest;
import com.pinHouse.server.platform.housingFit.diagnosis.application.dto.response.DiagnosisResponse;

import java.util.UUID;

public interface DiagnosisUseCase {

/// 청약 진단하기
DiagnosisResponse diagnose(UUID userId, DiagnosisRequest request);

/// 나의 진단 목록 조회하기
DiagnosisResponse getDiagnose(UUID userId);

}
Loading
Loading