Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package co.kr.pinhouse.domain.home.application.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "홈 공고 목록이 비어 있는 이유")
public enum HomeNoticeEmptyReason {

@Schema(description = "진단 기록이 없어 추천 공고를 생성할 수 없음")
NO_DIAGNOSIS,

@Schema(description = "진단 결과상 추천 가능한 임대주택 유형이 없음")
NO_ELIGIBLE_RENTAL_TYPES,

@Schema(description = "진단 결과를 공고 필터 조건으로 매핑할 수 없음")
NO_MAPPED_SUPPLY_TYPES,

@Schema(description = "진단 결과에 맞는 공고가 현재 없음")
NO_MATCHING_NOTICES
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

import java.util.List;

import com.fasterxml.jackson.annotation.JsonInclude;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

@Schema(name = "[응답][홈] 홈 화면 공고 목록 조회 응답", description = "홈 화면에서 마감임박 공고 목록을 조회하기 위한 DTO입니다. SliceResponse와 유사한 구조에 region 필드를 추가했습니다.")
@Builder
public record HomeNoticeListResponse(

@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = "공통 지역", example = "성남시")
String region,

Expand All @@ -22,7 +25,19 @@ public record HomeNoticeListResponse(
boolean hasNext,

@Schema(description = "전체 공고 개수", example = "100")
long totalElements
long totalElements,

@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = "진단 기반 추천 응답에서 진단 기록 존재 여부를 제공합니다.", example = "true", nullable = true)
Boolean hasDiagnosis,

@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = "목록이 비어 있는 이유. 진단 기반 추천 응답에서만 사용됩니다.", example = "NO_DIAGNOSIS", nullable = true)
HomeNoticeEmptyReason emptyReason,

@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = "목록이 비어 있는 사유를 설명하는 메시지. 진단 기반 추천 응답에서만 사용됩니다.", example = "진단 기록이 없어 추천 공고를 제공할 수 없습니다.", nullable = true)
String emptyMessage

) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import co.kr.pinhouse.common.exception.code.DiagnosisErrorCode;
import co.kr.pinhouse.common.exception.code.PinPointErrorCode;
import co.kr.pinhouse.common.response.CustomException;
import co.kr.pinhouse.common.response.pageable.SliceRequest;
Expand All @@ -24,6 +23,7 @@
import co.kr.pinhouse.domain.diagnostic.diagnosis.application.dto.response.DiagnosisDetailResponse;
import co.kr.pinhouse.domain.diagnostic.diagnosis.application.usecase.DiagnosisUseCase;
import co.kr.pinhouse.domain.home.application.dto.HomeSearchCategoryType;
import co.kr.pinhouse.domain.home.application.dto.response.HomeNoticeEmptyReason;
import co.kr.pinhouse.domain.home.application.dto.response.HomeNoticeListResponse;
import co.kr.pinhouse.domain.home.application.dto.response.HomeNoticeResponse;
import co.kr.pinhouse.domain.home.application.dto.response.HomeSearchCategoryPageResponse;
Expand Down Expand Up @@ -63,6 +63,7 @@ public class HomeService implements HomeUseCase {
*/
private static final int HOME_SEARCH_PREVIEW_LIMIT = 5;
private static final int HOME_SEARCH_CATEGORY_PAGE_SIZE = 5;
private static final String RECOMMENDED_NOTICES_TITLE = "진단 기반 추천";
private final NoticeDocumentRepository noticeRepository;
private final ComplexDocumentRepository complexRepository;
private final LikeQueryUseCase likeService;
Expand Down Expand Up @@ -403,8 +404,12 @@ public HomeNoticeListResponse getRecommendedNoticesByDiagnosis(

// 2. 진단 기록 없음 처리
if (diagnosis == null) {
log.warn("진단 기록이 없습니다 - userId={}", sanitize(userId));
throw new CustomException(DiagnosisErrorCode.NOT_FOUND_DIAGNOSIS);
log.info("진단 기록이 없어 추천 공고를 비워서 반환합니다 - userId={}", sanitize(userId));
return emptyRecommendedNoticeResponse(
false,
HomeNoticeEmptyReason.NO_DIAGNOSIS,
"진단 기록이 없어 추천 공고를 제공할 수 없습니다."
);
}

// 3. 추천 임대주택 유형 추출
Expand All @@ -415,13 +420,11 @@ public HomeNoticeListResponse getRecommendedNoticesByDiagnosis(
|| availableRentalTypes.isEmpty()
|| availableRentalTypes.contains("해당 없음")) {
log.info("추천 가능한 임대주택이 없습니다 - userId={}", sanitize(userId));
return HomeNoticeListResponse.builder()
.region(null)
.title("진단 기반 추천")
.content(List.of())
.hasNext(false)
.totalElements(0L)
.build();
return emptyRecommendedNoticeResponse(
true,
HomeNoticeEmptyReason.NO_ELIGIBLE_RENTAL_TYPES,
"진단 결과상 추천 가능한 임대주택 유형이 없습니다."
);
}

// 5. 진단 결과 → 공고 supplyType 매핑
Expand All @@ -433,13 +436,11 @@ public HomeNoticeListResponse getRecommendedNoticesByDiagnosis(
// 진단 결과에 매핑될 공고 유형이 없는 경우 빈 응답 반환
if (targetSupplyTypes.isEmpty()) {
log.info("진단 결과에 매핑 가능한 주택 유형이 없습니다 - userId={}", sanitize(userId));
return HomeNoticeListResponse.builder()
.region(null)
.title("진단 기반 추천")
.content(List.of())
.hasNext(false)
.totalElements(0L)
.build();
return emptyRecommendedNoticeResponse(
true,
HomeNoticeEmptyReason.NO_MAPPED_SUPPLY_TYPES,
"진단 결과를 공고 필터 조건으로 변환할 수 없습니다."
);
}

log.debug("진단 기반 필터링 - rentalTypes={}, supplyTypes={}",
Expand All @@ -455,6 +456,16 @@ public HomeNoticeListResponse getRecommendedNoticesByDiagnosis(
pageable
);

if (page.isEmpty()) {
log.info("진단 결과에 맞는 공고가 없습니다 - userId={}, supplyTypes={}",
sanitize(userId), sanitize(targetSupplyTypes));
return emptyRecommendedNoticeResponse(
true,
HomeNoticeEmptyReason.NO_MATCHING_NOTICES,
"진단 결과에 맞는 공고가 없습니다."
);
}

// 8. 좋아요 상태 조회
List<String> likedNoticeIds = likeService.getLikeNoticeIds(userId);

Expand All @@ -469,10 +480,28 @@ public HomeNoticeListResponse getRecommendedNoticesByDiagnosis(
// 10. 최종 응답
return HomeNoticeListResponse.builder()
.region(null)
.title("진단 기반 추천")
.title(RECOMMENDED_NOTICES_TITLE)
.content(content)
.hasNext(page.hasNext())
.totalElements(page.getTotalElements())
.hasDiagnosis(true)
.build();
}

private HomeNoticeListResponse emptyRecommendedNoticeResponse(
boolean hasDiagnosis,
HomeNoticeEmptyReason emptyReason,
String emptyMessage
) {
return HomeNoticeListResponse.builder()
.region(null)
.title(RECOMMENDED_NOTICES_TITLE)
.content(List.of())
.hasNext(false)
.totalElements(0L)
.hasDiagnosis(hasDiagnosis)
.emptyReason(emptyReason)
.emptyMessage(emptyMessage)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ public void savePinPoint(UUID userId, PinPointRequest request) {

/// 유저 검증
User user = userService.loadUser(userId);
boolean isFirstPinPoint = repository.countByUserId(user.getId().toString()) == 0;
boolean shouldSetFirst = isFirstPinPoint || request.first();

/// 새로운 핀포인트가 first=true인 경우, 기존 first=true인 핀포인트를 false로 변경
if (request.first()) {
/// 최초 저장이거나 새로운 핀포인트가 first=true인 경우, 핀포인트를 대표로 설정
if (shouldSetFirst && !isFirstPinPoint) {
Optional<PinPoint> existingFirstPinPoint = repository.findByUserIdAndIsFirst(user.getId().toString(), true);
existingFirstPinPoint.ifPresent(pinPoint -> {
pinPoint.setFirst(false);
Expand All @@ -58,7 +60,7 @@ public void savePinPoint(UUID userId, PinPointRequest request) {

/// 도메인 생성
var entity = PinPoint.of(user.getId().toString(), request.address(), request.name(), location.getLatitude(),
location.getLongitude(), request.first());
location.getLongitude(), shouldSetFirst);

/// 저장하기
repository.save(entity);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,20 +98,16 @@ ApiResponse<NoticeCountResponse> getNoticeCountWithinTravelTime(
description = "사용자의 최근 청약 진단 결과를 기반으로 추천 공고를 조회하는 API입니다. "
+ "진단 결과의 신청가능한 임대주택 유형(availableRentalTypes)을 기준으로 공고를 필터링하며, "
+ "마감임박순으로 정렬됩니다. 모든 공고 상태(모집중 + 마감)를 포함합니다. "
+ "진단 기록이 없는 경우 404 에러가 발생합니다. "
+ "진단 결과 자격이 없는 경우(해당 없음) 200 OK와 함께 빈 리스트를 반환합니다."
+ "진단 기록이 없는 경우에도 200 OK와 함께 빈 리스트를 반환하며, "
+ "`hasDiagnosis=false`, `emptyReason=NO_DIAGNOSIS` 로 상태를 구분합니다. "
+ "진단 결과상 추천 가능한 유형이 없거나, 추천 조건에 맞는 공고가 없는 경우에도 200 OK와 함께 빈 리스트를 반환합니다."
)
@ApiResponses(value = {
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "조회 성공 (진단 결과에 맞는 공고가 없어도 200 반환)",
description = "조회 성공 (진단 기록이 없거나, 추천 결과가 비어 있어도 200 반환)",
content = @Content(schema = @Schema(implementation = HomeNoticeListResponse.class))
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "404",
description = "진단 기록을 찾을 수 없음",
content = @Content(schema = @Schema(implementation = ApiResponse.class))
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "401",
description = "인증 실패 (로그인 필요)",
Expand Down
Loading