feat: date course 생성 기본 기능 구현#4
Conversation
1000hyehyang
left a comment
There was a problem hiding this comment.
총평
- 구조 자체는 프로젝트의 기존 흐름과 대체로 맞습니다.
Api interface -> Controller -> Request/Response -> Application Service/DTO -> Domain/Repository분리는 잘 되어 있습니다. - 다만 현재 상태는 clean build가 실패하고, 기능적으로도 “코스를 생성한다”는 요구사항 경계에서 빈 코스 저장, 입력 검증 부족, 점수화 방식 왜곡, 조회 성능 문제 같은 수정 필요 지점이 있습니다.
- 가장 먼저 할 일은 Checkstyle 실패를 해결해서 빌드를 통과시키고, 그 다음 생성 로직의 실패 조건과 검증 정책을 정리하는 것입니다.
문제점
-
Clean build가 Checkstyle에서 실패합니다.
./gradlew clean build실행 결과 컴파일과 테스트 실행까지는 진행됐지만checkstyleMain,checkstyleTest가 실패했습니다.- 주요 원인:
- CourseScorer.java unused import
BigDecimal - DateCourseGenerationService.java unused import
DateCourseBatchResult - DateCourseGenerationService.java unused import
Instant - DateCoursePlace.java annotation indentation
- 테스트 메서드명이
openDuringBusinessHours_returnsTrue처럼 underscore를 써서 프로젝트 Checkstyle의^[a-z][a-zA-Z0-9]*$규칙을 위반합니다.
- CourseScorer.java unused import
-
빈 코스가 저장될 수 있습니다.
- DateCourseGenerationService.java에서
GENERAL,TRENDY,POPULAR을 무조건 저장합니다. - CourseSelector.java에서 모든 slot이 skip되어도
CourseSelectionResult는 반환되고, DateCourseGenerationService.java에서 빈DateCourse가 저장됩니다. - 특히
globallyUsedIds때문에 앞선 모드가 후보를 다 쓰면 뒤의POPULAR은 빈 코스가 될 수 있습니다.
- DateCourseGenerationService.java에서
-
입력 검증이 부족해서 잘못된 요청이 400이 아니라 404/빈 결과로 흐를 가능성이 큽니다.
- DateCourseGenerationRequest.java는
categorySequence크기 제한이 없습니다. - CategorySlotRequest.java는
categoryCode만NotBlank이고, 존재하는 카테고리/태그인지, 태그가 카테고리에 속하는지는 검증하지 않습니다. - DateCourseGenerationRequest.java의
sigunguCode도 존재 검증 없이 후보 조회로 바로 들어갑니다.
- DateCourseGenerationRequest.java는
-
점수화 방식이 모드별 의도를 약하게 만들 수 있습니다.
- CourseScorer.java의 거리 점수는
1 / km입니다. - 장소 간 거리가 매우 가까우면 점수가 수십~수백까지 튀고,
TRENDY최대 1.5,POPULAR최대 1.8 가중치는 거의 영향이 없어집니다. - 결과적으로 첫 장소 이후에는 “트렌디/인기”보다 “이전 장소와 가장 가까운 장소”가 과도하게 우선될 수 있습니다.
- CourseScorer.java의 거리 점수는
-
조회 API가 무제한으로 모든 코스와 장소를 로드합니다.
- DateCourseQueryService.java에서 방의 모든 코스를 조회하고, DateCourseQueryService.java에서 모든 코스의 장소를 한 번에 조회합니다.
- 생성 이력이 늘면 목록 API가 점점 무거워집니다. 기존
RoomPlaceQueryService는 page/limit를 두고 있어서 스타일도 다릅니다.
-
JSON 직렬화/역직렬화 실패를 조용히 숨깁니다.
- DateCourseGenerationService.java와 DateCourseGenerationService.java는 직렬화 실패 시
"[]"를 저장합니다. - DateCourseQueryService.java는 JSON을 수동 파싱하고 실패하면 빈 리스트를 반환합니다.
- 데이터 손상이 발생해도 API에서는 정상처럼 보이므로 운영 중 원인 파악이 어렵습니다.
- DateCourseGenerationService.java와 DateCourseGenerationService.java는 직렬화 실패 시
-
네이밍 오타가 있습니다.
- DateCourseGenerationService.java와 DateCourseQueryService.java의
dateCourseParaRepository는dateCoursePlaceRepository가 맞습니다.
- DateCourseGenerationService.java와 DateCourseQueryService.java의
수정 우선순위
- 1순위: Checkstyle 실패 해결, 빈 코스 저장 방지, 입력 검증 추가
- 2순위: 점수화 정규화, 목록 조회 pagination 추가, JSON 처리 방식 개선
- 3순위:
dateCourseParaRepository네이밍 수정, unused repository method 제거, 생성 API 반환 스타일을 기존ResponseEntity패턴과 맞출지 검토
권장 수정 방향
-
Clean build 실패는 단순합니다.
- unused import 제거
DateCoursePlace의@UniqueConstraintindentation 조정- 테스트 메서드명을 camelCase로 변경
예:openDuringBusinessHours_returnsTrue->openDuringBusinessHoursReturnsTrue
-
빈 코스는 저장 전에 막는 게 좋습니다.
if (selection.pickedPlaces().isEmpty()) {
continue; // 또는 mode별 생성 실패로 간주
}
if (results.isEmpty()) {
throw new BusinessException(ErrorCode.E404_NOT_FOUND, "생성 가능한 코스가 없습니다.");
}-
sigunguCode,categoryCode,tagCode는 서비스 초입에서 명시 검증하는 편이 좋습니다.RegionQueryService.validateFilter(null, sigunguCode)같은 기존 검증 흐름 활용- taxonomy repository 또는 별도 validator로 category/tag 존재 및 관계 검증
- 잘못된 입력은 후보 없음
404가 아니라400 Bad Request가 적절합니다.
-
점수는 곱셈보다 정규화된 weighted sum이 안전합니다.
double distanceScore = 1.0 / (1.0 + distanceKm); // 0~1 범위에 가깝게 제한
return distanceWeight * distanceScore + modeWeight * modeScore;-
목록 조회는
page,limit를 받도록 바꾸는 게 좋습니다.- batch 단위 목록이면
DateCourse전체가 아니라generationBatchId기준으로 먼저 page 조회 - 이후 해당 batch들의 course/place만 조회
- batch 단위 목록이면
-
JSON 처리는 양쪽 모두
ObjectMapper로 통일하고, 실패를 숨기지 않는 게 좋습니다.
private String serializeSkipped(List<Integer> skipped) {
try {
return objectMapper.writeValueAsString(skipped);
} catch (JsonProcessingException e) {
throw new BusinessException(ErrorCode.E500_INTERNAL, "Failed to serialize skipped slots.");
}
}- 조회 쪽도 수동 파싱 대신
objectMapper.readValue(...)를 사용하세요. 데이터가 깨져 있다면 빈 리스트로 숨기기보다 로그를 남기거나 내부 오류로 처리하는 편이 운영 안정성에 좋습니다.
또한, 아래의 기능이 필요합니다. 현재 조회까지 구현해주셨길래 확인해봤는데, 필요한 기능이 부족합니다. 같은 PR에서 구현할 것이라면 함께 구현해주세요.
데이트코스 생성 후 DB에 저장하는 기능은 있음
누가 저장했는지 userId는 저장함
하지만 응답/API에서 “어떤 멤버가 저장했는지” 보여주는 기능은 없음
(각 방 목록에서 해당 방에서 어떤 멤버가 저장한 데이트 코스인지 보여줄 수 있어야 함.)
마이페이지에서 내가 저장한 데이트코스만 방 상관없이 모아보는 기능도 없음
별도의 “저장하기” 기능은 없음. 생성하면 바로 저장되는 구조임 (생성과 저장은 별개여야함)
브랜치를 버린 후 새롭게 시작하셔도 괜찮고, 아니면 해당 브랜치에서 수정사항 반영 후 커밋 네이밍 규칙에 맞게 작성 후 올려주셔도 괜찮습니다.
|
수정사항 반영하여 fix 커밋했습니다!! [구현 완료]
POST /api/v1/rooms/{roomId}/date-courses - 코스 생성 (GENERAL/TRENDY/POPULAR 후보 3개) POST /api/v1/rooms/{roomId}/date-courses/{coursePublicId}/save - 후보 중 1개 저장 확정 GET /api/v1/rooms/{roomId}/date-courses - 방 단위 저장 코스 목록 조회 (페이지네이션) GET /api/v1/rooms/{roomId}/date-courses/{coursePublicId} - 코스 상세 조회 GET /api/v1/users/me/date-courses - 내가 저장한 코스 전체 조회 (방 구분 없음)
[미구현 (추가 필요)]
|
Checkstyle: 미사용 import 4개 제거, DateCoursePlace 들여쓰기 수정, 테스트 메서드명 28개 camelCase 변환 생성 로직: 빈 코스 저장 방지, sigunguCode/categoryCode/tagCode 입력 검증(400), categorySequence 크기 제한 생성/저장 분리: DateCourse에 savedByUserId/savedAt 추가, Save API 신설, 저장자 정보 응답 포함 조회 변경: listCourses를 saved 코스 전용으로 전환하고 페이지네이션 추가 마이페이지: GET /api/v1/users/me/date-courses API 신설 품질 개선: distScore 정규화(1/(1+dist)), weighted sum 방식으로 모드 가중치 균형 조정 기타: dateCourseParaRepository 네이밍 수정, parseSkipped objectMapper로 개선 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
0.4 × 1.5, 0.4 × 1.8 연산의 IEEE 754 반올림으로 이론 최댓값(1.2, 1.32)을 아주 미세하게 초과할 수 있어 isBetween 상한선을 각각 1.21, 1.33으로 조정 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- docker exec < 리다이렉트 방식은 PowerShell에서 동작하지 않아 Get-Content 파이프 방식으로 수정 - v_room_pid 플레이스홀더를 'INPUT ROOM_ID HERE'에서 'YOUR-ROOM-PUBLIC-ID'로 변경해 검증 조건과 일치시킴 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
f24f3cc to
5c354bc
Compare
|
커밋 메시지 컨벤션 수정으로 히스토리 rewrite했다. |
There was a problem hiding this comment.
Pull request overview
룸에 저장된 장소를 기반으로 조건(데이트 일시/지역/카테고리 시퀀스/영업시간)을 만족하는 후보를 선별하고, GENERAL/TRENDY/POPULAR 3가지 모드의 데이트 코스 후보를 생성·조회·저장할 수 있는 백엔드 기능을 추가합니다.
Changes:
- DateCourse/DateCoursePlace 엔티티 및 관련 Repository/QueryService/SaveService 추가
- 코스 후보 풀 구성(영업시간 plannedDateTime 기준 필터링) + 모드별 스코어링/선택 로직(Haversine 포함) 구현
- 코스 생성/조회/저장 및 “내가 저장한 코스” 조회 API + 테스트/로컬 시드 스크립트 추가
Reviewed changes
Copilot reviewed 46 out of 46 changed files in this pull request and generated 26 comments.
Show a summary per file
| File | Description |
|---|---|
| src/test/java/com/hufs/capstone/backend/course/application/HaversineTest.java | Haversine 거리 계산 단위 테스트 추가 |
| src/test/java/com/hufs/capstone/backend/course/application/CourseSelectorTest.java | 코스 선택 로직(스킵/중복방지/POPULAR 후보 제한) 테스트 추가 |
| src/test/java/com/hufs/capstone/backend/course/application/CourseScorerTest.java | 모드별 스코어 계산 테스트 추가 |
| src/test/java/com/hufs/capstone/backend/course/application/BusinessHoursAtTimeCheckerTest.java | 특정 시각 영업 여부 판정 테스트 추가 |
| src/main/java/com/hufs/capstone/backend/place/application/BusinessHoursDisplayResolver.java | 특정 시각 기준 영업 상태 조회(statusAt) 기능 추가 |
| src/main/java/com/hufs/capstone/backend/course/domain/repository/DateCourseRepository.java | DateCourse 조회(저장 코스/배치)용 JPA Repository 추가 |
| src/main/java/com/hufs/capstone/backend/course/domain/repository/DateCoursePlaceRepository.java | 코스-장소 조인 fetch 조회용 Repository 추가 |
| src/main/java/com/hufs/capstone/backend/course/domain/repository/DateCourseCandidateRepositoryImpl.java | 후보 풀 조회 QueryDSL 구현(지역/카테고리/영업시간 캐시 조건) |
| src/main/java/com/hufs/capstone/backend/course/domain/repository/DateCourseCandidateRepository.java | 후보 조회 Repository 인터페이스 추가 |
| src/main/java/com/hufs/capstone/backend/course/domain/enums/CourseMode.java | 코스 모드 enum 추가(GENERAL/TRENDY/POPULAR) |
| src/main/java/com/hufs/capstone/backend/course/domain/entity/DateCoursePlace.java | DateCoursePlace 엔티티 추가 |
| src/main/java/com/hufs/capstone/backend/course/domain/entity/DateCourse.java | DateCourse 엔티티 추가(배치/저장 메타 포함) |
| src/main/java/com/hufs/capstone/backend/course/application/Haversine.java | Haversine 거리 계산 유틸 추가 |
| src/main/java/com/hufs/capstone/backend/course/application/dto/NormalizationContext.java | POPULAR 정규화(소스별 max like) 컨텍스트 추가 |
| src/main/java/com/hufs/capstone/backend/course/application/dto/MyDateCourseResult.java | “내가 저장한 코스” 결과 DTO 추가 |
| src/main/java/com/hufs/capstone/backend/course/application/dto/MyDateCoursePageResult.java | “내가 저장한 코스” 페이지 DTO 추가 |
| src/main/java/com/hufs/capstone/backend/course/application/dto/DateCourseResult.java | 코스 단건/목록 응답 DTO 추가 |
| src/main/java/com/hufs/capstone/backend/course/application/dto/DateCoursePlaceResult.java | 코스 내 장소 응답 DTO 추가 |
| src/main/java/com/hufs/capstone/backend/course/application/dto/DateCoursePageResult.java | 방 저장 코스 목록 페이지 DTO 추가 |
| src/main/java/com/hufs/capstone/backend/course/application/dto/DateCourseGenerationResult.java | 코스 생성 응답 DTO 추가 |
| src/main/java/com/hufs/capstone/backend/course/application/dto/DateCourseGenerationCommand.java | 코스 생성 커맨드 DTO 추가 |
| src/main/java/com/hufs/capstone/backend/course/application/dto/CourseSelectionResult.java | 선택 결과(picked/skipped) DTO 추가 |
| src/main/java/com/hufs/capstone/backend/course/application/dto/CategorySlotCommand.java | 카테고리 슬롯 커맨드 + 와일드카드 판정 추가 |
| src/main/java/com/hufs/capstone/backend/course/application/dto/AvailableCandidate.java | 후보(룸플레이스+영업시간json) DTO 추가 |
| src/main/java/com/hufs/capstone/backend/course/application/DateCourseSaveService.java | 코스 저장 처리 서비스 추가 |
| src/main/java/com/hufs/capstone/backend/course/application/DateCourseQueryService.java | 저장 코스 목록/단건/내 저장 코스 조회 서비스 추가 |
| src/main/java/com/hufs/capstone/backend/course/application/DateCourseInputValidator.java | 시군구/카테고리/태그 코드 유효성 검증 컴포넌트 추가 |
| src/main/java/com/hufs/capstone/backend/course/application/DateCourseGenerationService.java | 코스 생성(풀 빌드→모드별 선택→DB 저장) 서비스 추가 |
| src/main/java/com/hufs/capstone/backend/course/application/CourseSelector.java | 슬롯별 후보 선택 + 전역 중복 방지 로직 추가 |
| src/main/java/com/hufs/capstone/backend/course/application/CourseScorer.java | 모드별 점수 계산(거리/최신/좋아요) 로직 추가 |
| src/main/java/com/hufs/capstone/backend/course/application/BusinessHoursAtTimeChecker.java | plannedDateTime 기준 영업 여부 판정 컴포넌트 추가 |
| src/main/java/com/hufs/capstone/backend/course/application/AvailablePoolBuilder.java | 후보 풀 구성(영업시간 json 조회+plannedDateTime 재필터) 추가 |
| src/main/java/com/hufs/capstone/backend/course/application/AvailablePool.java | 슬롯 매칭을 위한 후보 풀 래퍼 추가 |
| src/main/java/com/hufs/capstone/backend/course/api/response/MyDateCourseResponse.java | “내 저장 코스” API 응답 모델 추가 |
| src/main/java/com/hufs/capstone/backend/course/api/response/MyDateCoursePageResponse.java | “내 저장 코스” 페이지 응답 모델 추가 |
| src/main/java/com/hufs/capstone/backend/course/api/response/DateCourseResponse.java | 코스 단건/목록 응답 모델 추가 |
| src/main/java/com/hufs/capstone/backend/course/api/response/DateCoursePlaceResponse.java | 코스 장소 응답 모델 추가 |
| src/main/java/com/hufs/capstone/backend/course/api/response/DateCoursePageResponse.java | 방 저장 코스 목록 페이지 응답 모델 추가 |
| src/main/java/com/hufs/capstone/backend/course/api/response/DateCourseGenerationResponse.java | 코스 생성 응답 모델 추가 |
| src/main/java/com/hufs/capstone/backend/course/api/request/DateCourseGenerationRequest.java | 코스 생성 요청 모델(+validation) 추가 |
| src/main/java/com/hufs/capstone/backend/course/api/request/CategorySlotRequest.java | 카테고리 슬롯 요청 모델 추가 |
| src/main/java/com/hufs/capstone/backend/course/api/controller/swagger/MeDateCourseApi.java | “내 저장 코스” Swagger API 인터페이스 추가 |
| src/main/java/com/hufs/capstone/backend/course/api/controller/swagger/DateCourseApi.java | 코스 생성/저장/조회 Swagger API 인터페이스 추가 |
| src/main/java/com/hufs/capstone/backend/course/api/controller/MeDateCourseController.java | “내 저장 코스” 컨트롤러 추가 |
| src/main/java/com/hufs/capstone/backend/course/api/controller/DateCourseController.java | 코스 생성/저장/조회 컨트롤러 추가 |
| scripts/seed-local.sql | 로컬 테스트용 장소/room_places/영업시간 시드 스크립트 추가 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Copilot pull-request-reviewer 리뷰(#4402047490) 지적 사항을 반영한다. 변경 사항: - DateCourseGenerationRequest: categorySequence 리스트 요소에 @NotNull 추가 (`List<@NotNull @Valid CategorySlotRequest>`) — null 요소 입력 시 NPE 방지 - DateCourse: sigungu_code 컬럼에 nullable = false 적용 및 create() 팩토리 메서드에 sigunguCode null/blank 검증 추가 — DB/도메인 무결성 강화 - DateCourseRepository: markAsSavedIfAbsent() JPQL 조건부 UPDATE 추가 (`WHERE saved_by_user_id IS NULL`) — 동시 저장 요청 시 레이스 컨디션 방지 - DateCourseSaveService: check-then-act 패턴을 원자적 쿼리로 교체 - CourseScorerTest: trendyWeightRange → trendyScoreRange, popularWeightRange → popularScoreRange 로 메서드명/변수명 수정 — score() 검증 의도 명확화 - seed-local.sql: POPULAR 코스 테스트용 mock Instagram likeCount 데이터 추가 (카테고리별 고/저 likeCount 링크 6개 + room_places.origin_room_link_id 업데이트) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
copilot의 리뷰사항을 확인 후 반영하여 fix push했습니다. (대부분 db무결성 위한 fix임) |
Copilot pull-request-reviewer 2차 리뷰 지적 사항을 반영한다. 변경 사항: - HaversineTest: approxHundredKmBetweenSeoulAndSuwon → approxFortyFiveKmBetweenSeoulAndSuwon (메서드명이 실제 검증 거리 ~45km와 불일치하여 수정) - DateCoursePlace: create() 팩토리 메서드에 sequenceOrder >= 0 검사 추가 (정렬 키로 사용되는 필드의 도메인 불변식 명시) - CourseSelector: 선택 루프 후 O(n·m) pool 재조립 → O(n) 직접 구성으로 단순화 (List.contains/indexOf 대신 선택 시점에 pickedPlaces 직접 누적, 중복 방지용 picked Set<Long> 별도 유지) - BusinessHoursAtTimeChecker: java.util.Set FQN → import java.util.Set 추가 후 단순명 사용 (파일 내 다른 타입과 스타일 통일) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
원격 브랜치 기준으로 히스토리를 정리하고, 로컬 merge 시 반영한 seed/코스 저장 변경만 유지한다. Co-authored-by: Cursor <cursoragent@cursor.com>
4ff708c to
3dacb3e
Compare
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
✨ 무엇을 바꿨나요?
룸에 저장된 장소를 기반으로 데이트 코스를 자동 생성하는 기능을 구현
🔗 관련 이슈
Closes #3
💡 왜 바꿨나요?
사용자가 룸에 모아둔 장소들 중 데이트 일시·지역·영업시간 조건에 맞는 장소만 추려 최적 코스를 제안하기 위함. 취향 차이를 고려해 GENERAL / TRENDY / POPULAR 3가지 성향의 코스를 한 번에 제안함.
📝 주요 변경 사항
👀 리뷰어가 보면 좋은 부분
🧪 테스트
방식 (해당하는 것만 체크)
메모 (시나리오, 커맨드, 스크린샷 링크 등 — 선택)