-
Notifications
You must be signed in to change notification settings - Fork 0
[feat] 마이페이지 구현 #51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[feat] 마이페이지 구현 #51
Conversation
Walkthrough마이페이지 기능 추가 및 인증 연동. 마이페이지 컨트롤러/서비스/DTO/리포지토리 구현, 코스 기록/선호 관광지/사용자 조회 제공. 코스 좋아요·추천에 인증 사용자 주입. 사용자/투어스팟 관련 커스텀 리포지토리 패키지 재구성, QueryDSL 구현 추가. JWT 필터 Bearer 처리, Swagger 보안 스키마, 에러 코드 및 엔티티/DTO 추가. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant API as MypageController
participant SVC as MypageService
participant Repo as Repositories
participant Ext as TourSpotService
User->>API: GET /mypage/courses (JWT)
API->>SVC: getCourses(user)
SVC->>Repo: findAllByUserId(userId)
Repo-->>SVC: Courses(+components)
SVC-->>API: List<CourseHistoryResponse>
API-->>User: 200 ApiResponse
User->>API: POST /mypage/preferences?tourSpotId
API->>SVC: saveUserTourSpot(user, tourSpotId)
SVC->>Repo: exists/count, save
SVC-->>API: void
API-->>User: 200 ApiResponse
sequenceDiagram
actor User
participant CAPI as CourseController
participant CSVC as CourseService
participant URepo as UserRepository
participant Redis as Redis
participant DB as JPA
User->>CAPI: POST /courses/{id}/like (JWT)
CAPI->>CSVC: likeCourse(courseId, user)
CSVC->>URepo: findUserByEmail(email)
URepo-->>CSVC: User
CSVC->>Redis: get course draft
CSVC->>DB: persist like/course
CSVC-->>CAPI: void
CAPI-->>User: 200 ApiResponse
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 15
🔭 Outside diff range comments (2)
src/main/java/com/opendata/global/jwt/JwtFilter.java (1)
37-44: 만료 토큰이 그대로 통과되는 로직 버그: isExpired 반환값을 체크하지 않아 인증이 설정됩니다현재 jwtUtil.isExpired(accessToken)의 반환값을 무시하고 있으며, ExpiredJwtException만 캐치하고 있습니다. 제공된 JwtUtil 구현은 만료 시 예외를 던지지 않고 boolean을 반환합니다. 결과적으로 만료 토큰도 SecurityContext에 인증이 설정되는 심각한 버그가 발생합니다.
다음과 같이 만료 여부를 명시적으로 확인하고, 파싱/검증 예외에 대해서도 401을 반환하도록 처리해 주세요.
- try { - jwtUtil.isExpired(accessToken); - } catch (ExpiredJwtException e) { - PrintWriter writer = response.getWriter(); - writer.print("access token expired"); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return; - } + try { + if (jwtUtil.isExpired(accessToken)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().print("access token expired"); + return; + } + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().print("invalid access token"); + return; + }src/main/java/com/opendata/domain/course/service/CourseService.java (1)
218-221: Redis 키 접두사 중복 버그 — likeCourse에서 courseId가 이미 "tempCourse:"인지 검사하세요검증 결과: recommendCourses에서 tempCourseId = "tempCourse:" + UUID.randomUUID()로 생성해 CourseResponse에 그대로 포함하고(및 Redis에 저장) 있으므로, 클라이언트는 이미 "tempCourse:..." 형태의 ID를 전달합니다. 그런데 likeCourse는 redisTemplate.opsForValue().get("tempCourse:" + courseId)로 무조건 접두사를 붙여 조회해, "tempCourse:tempCourse:..."가 되어 조회에 실패할 수 있음이 확인되었습니다.
주의가 필요한 위치:
- src/main/java/com/opendata/domain/course/service/CourseService.java
- lines ~94-100: recommendCourses — String tempCourseId = "tempCourse:" + UUID.randomUUID(); 및 redisTemplate.opsForValue().set(tempCourseId, ...)
- line 218: likeCourse — List rawList = (List) redisTemplate.opsForValue().get("tempCourse:" + courseId);
권장 수정(예시):
- List<?> rawList = (List<?>) redisTemplate.opsForValue().get("tempCourse:" + courseId); + String redisKey = (courseId != null && courseId.startsWith("tempCourse:")) ? courseId : "tempCourse:" + courseId; + List<?> rawList = (List<?>) redisTemplate.opsForValue().get(redisKey);추가 권장: CourseResponse의 courseId가 접두사 포함 형태임을 API 문서(Swagger)에 명시하거나, 클라이언트에 UUID만 전달하도록 응답 형태를 변경해 혼동을 줄이세요.
🧹 Nitpick comments (61)
src/main/java/com/opendata/domain/tourspot/repository/custom/related/CustomTourSpotRelatedRepository.java (1)
3-5: 빈 커스텀 리포지토리 인터페이스 — 목적 명시 또는 제거 고려현재 메서드가 전무하여 조각(fragment) 자체가 불필요해 보입니다. 유지가 목적(향후 확장)이라면 최소한의 Javadoc으로 의도를 남기거나 TODO를 추가해 주세요. 불필요하다면 인터페이스/구현체를 제거해도 됩니다.
적용 예시(간단 Javadoc 추가):
+/** + * TourSpot 간 연관관계 전용 커스텀 리포지토리 조각. + * 향후 연관 조회(LEFT JOIN 최적화), 벌크 업데이트 등의 커스텀 메서드를 선언합니다. + */ public interface CustomTourSpotRelatedRepository { }src/main/java/com/opendata/domain/tourspot/repository/custom/related/CustomTourSpotRelatedRepositoryImpl.java (2)
6-8: 불필요한 애너테이션 정리 제안(@repository, @requiredargsconstructor)Spring Data fragment 구현체는 관례상 애너테이션 없이도 조합되며, 현재 필드 의존성도 없어 RequiredArgsConstructor가 무의미합니다. 제거를 고려해 주세요.
간소화 예시:
package com.opendata.domain.tourspot.repository.custom.related; public class CustomTourSpotRelatedRepositoryImpl implements CustomTourSpotRelatedRepository { }
8-10: 구현 메서드가 없는 빈 구현체 — 필요 없으면 파일 제거 고려인터페이스에 선언된 메서드가 없다면 구현체도 불필요합니다. 유지 비용/스캔 비용을 줄이려면 실제 커스텀 메서드가 추가될 때 도입하는 방식을 권장합니다.
src/main/java/com/opendata/domain/tourspot/repository/TourSpotRelatedRepository.java (1)
8-9: @repository 애너테이션은 선택 사항(간소화 가능)Spring Data JPA는 인터페이스 기반 리포지토리를 자동 감지/등록합니다. 일관된 컨벤션을 위해 프로젝트 전반에서 @repository를 붙이지 않는 방향이라면 여기서도 제거 가능입니다.
간소화 예시:
package com.opendata.domain.tourspot.repository; import com.opendata.domain.tourspot.entity.TourSpotRelated; import com.opendata.domain.tourspot.repository.custom.related.CustomTourSpotRelatedRepository; import org.springframework.data.jpa.repository.JpaRepository; public interface TourSpotRelatedRepository extends JpaRepository<TourSpotRelated, Long>, CustomTourSpotRelatedRepository { }src/main/java/com/opendata/domain/tourspot/repository/custom/event/CustomTourSpotEventRepository.java (1)
8-11: findAllByTourSpot 추가는 구현과 일치합니다. null 인자 방어를 고려해주세요.현재 구현체에서 null이 들어오면
is null조건으로 동작할 수 있습니다. 명시적으로 null을 금지(사전 검증 또는 주석/어노테이션)하거나, null이면 빈 리스트를 반환하도록 정책을 정해두는 것이 안전합니다.src/main/java/com/opendata/domain/tourspot/repository/TourSpotEventRepository.java (1)
10-12: exists 메서드는 Spring Data 파생 쿼리로 대체하는 것을 고려해 주세요.커스텀 레포지토리를 경유하지 않고도 간단히 표현 가능합니다. 유지보수성과 테스트 용이성이 개선됩니다.
아래처럼 메서드 시그니처를 본 레포지토리에 직접 추가하면 커스텀 인터페이스/구현의 동일 메서드를 제거할 수 있습니다.
@Repository public interface TourSpotEventRepository extends JpaRepository<TourSpotEvent, Long>, CustomTourSpotEventRepository { + boolean existsByEventNameAndEventPeriod(String eventName, String eventPeriod); }src/main/java/com/opendata/domain/tourspot/repository/custom/event/CustomTourSpotEventRepositoryImpl.java (3)
22-31: exists 쿼리를 간결하게 정리하고 입력값 방어를 고려해 주세요.로컬 변수 없이 바로 반환하면 가독성이 좋아집니다. 또한
eventName,eventPeriod가 null일 경우를 명확히 금지하거나 처리 정책을 정해두면 예기치 않은 "is null" 비교를 피할 수 있습니다. 반복 호출이 잦다면 DB 인덱스(event_name, event_period) 추가도 검토 가치가 있습니다.아래처럼 간소화할 수 있습니다:
- Integer fetchOne = queryFactory - .selectOne() - .from(qTourSpotEvent) - .where( - qTourSpotEvent.eventName.eq(eventName), - qTourSpotEvent.eventPeriod.eq(eventPeriod) - ) - .fetchFirst(); - return fetchOne != null; + return queryFactory + .selectOne() + .from(qTourSpotEvent) + .where( + qTourSpotEvent.eventName.eq(eventName), + qTourSpotEvent.eventPeriod.eq(eventPeriod) + ) + .fetchFirst() != null;
18-19: 읽기 전용 트랜잭션 적용을 고려해 주세요.조회 전용 메서드는
@Transactional(readOnly = true)를 부여하면 JPA 플러시/더티체크 비용을 줄일 수 있습니다. 서비스 계층에서 트랜잭션을 관리한다면 생략 가능하지만, 리포지토리 단에서도 명확히 해두면 의도가 드러납니다.아래와 같이 어노테이션을 추가할 수 있습니다(메서드/클래스 단위 택1):
+ @Transactional(readOnly = true) @Override public boolean existsByEventNameAndEventPeriod(String eventName, String eventPeriod) {+ @Transactional(readOnly = true) @Override public List<TourSpotEvent> findAllByTourSpot(TourSpot tourSpot) {추가 import(파일 상단에 필요):
import org.springframework.transaction.annotation.Transactional;
33-39: 엔티티 비교(eq(entity)) 대신 ID 비교 또는 null 인자 방어를 고려해 주세요.
eq(tourSpot)은 정상 동작하지만, 호출부에서 영속성 컨텍스트 밖의 프록시/분리된 엔티티가 전달될 가능성이 있다면 ID 비교가 더 안전하고 의도가 분명합니다. 최소한tourSpot이 null일 경우를 방어(예: IllegalArgumentException)하거나 빈 리스트 반환 정책을 문서화해 주세요.src/main/java/com/opendata/domain/tourspot/repository/custom/tag/CustomTourSpotTagRepositoryImpl.java (2)
12-15: Custom 구현 클래스에 @repository는 불필요 — 중복 빈/모호한 주입 가능성프래그먼트 구현체는 Spring Data가 조합하여 사용하므로 일반적으로
@Repository를 붙이지 않습니다. 현재 애노테이션이 있으면 동일 인터페이스 타입 빈이 2개가 되어(프래그먼트 포함된 리포지토리, 그리고 구현체 자체)CustomTourSpotTagRepository타입 주입 시 모호성이 생길 수 있습니다. 제거를 권장합니다.적용 예:
-import org.springframework.stereotype.Repository; @@ -@Repository @RequiredArgsConstructor public class CustomTourSpotTagRepositoryImpl implements CustomTourSpotTagRepository {
17-22: 입력 파라미터 null 가드(방어적 코드) 소폭 추가 제안서비스 레이어에서 이미 보장한다면 생략 가능하나, 리포지토리 단에서도 명시적으로 실패시키면 디버깅이 수월합니다.
public List<TourSpotTag> findAllByTourSpot(TourSpot tourSpot) { + if (tourSpot == null) { + throw new IllegalArgumentException("tourSpot must not be null"); + } QTourSpotTag qTourSpotTag = QTourSpotTag.tourSpotTag; return queryFactory.selectFrom(qTourSpotTag) .where(qTourSpotTag.tourspot.eq(tourSpot)) .fetch(); }src/main/java/com/opendata/domain/tourspot/repository/custom/tag/CustomTourSpotTagRepository.java (1)
8-10: FK 기반 조회 오버로드 메서드 추가 제안(컴포넌트는 FK만 저장 목표와 정합성)PR 요약에 맞춰 FK만 들고 다니는 경로를 지원하려면 ID 기반 오버로드가 유용합니다. 기존 메서드는 유지하고, 아래 메서드를 추가하는 방식을 권장합니다.
public interface CustomTourSpotTagRepository { List<TourSpotTag> findAllByTourSpot(TourSpot tourSpot); + /** + * FK 기반 조회를 위한 경량 오버로드. + */ + List<TourSpotTag> findAllByTourSpotId(Long tourspotId); }원하시면 구현부(QueryDSL)까지 포함해 추가 커밋 패치를 드리겠습니다.
src/main/java/com/opendata/domain/tourspot/repository/custom/tourSpot/CustomTourSpotRepository.java (2)
1-1: 패키지명에 대문자(S) 사용 — Java 컨벤션과 파일시스템 이슈 가능패키지 경로에 대문자
tourSpot가 포함되어 있습니다. Java 패키지는 소문자 사용이 관례이며, 대소문자 구분 파일시스템(예: Linux)에서 경로 혼선을 유발할 수 있습니다. 일관성을 위해 소문자tourspot로 변경을 권장합니다.적용 예(해당 파일 기준):
-package com.opendata.domain.tourspot.repository.custom.tourSpot; +package com.opendata.domain.tourspot.repository.custom.tourspot;참고: 같은 변경이 구현체와 import를 사용하는 모든 파일에도 필요합니다.
8-11: findByAddress의 반환 타입/도메인 제약 확인 필요현재 시그니처는 Optional 반환인데, 동일 Address에 여러 TourSpot이 존재할 수 있다면 모호합니다. 비즈니스 상 Address별 단일 TourSpot이 보장되지 않는다면 아래 중 하나를 권장합니다.
옵션:
- Address별 복수 가능: 메서드를 컬렉션 반환으로 변경
- Optional findByAddress(Address address);
- List findAllByAddress(Address address);
- 단일만 기대하되 “아무거나 1개” 의도라면 메서드명 명확화 + 구현부 정렬 보장
- Optional findByAddress(Address address);
- Optional findFirstByAddress(Address address);
(구현부에는 orderBy 추가 권장 — 구현체 코멘트 참고)또한 findByName 역시 유일성이 보장되지 않으면 동일한 논의가 필요합니다.
src/main/java/com/opendata/domain/tourspot/repository/custom/tourSpot/CustomTourSpotRepositoryImpl.java (6)
1-1: 패키지명에 대문자(S) 사용 — 소문자 일원화 권장상위 인터페이스와 동일하게
tourSpot→tourspot로 정리하는 것이 안전합니다.적용 예:
-package com.opendata.domain.tourspot.repository.custom.tourSpot; +package com.opendata.domain.tourspot.repository.custom.tourspot;
18-27: findByName: fetchFirst 사용 시 결과 비결정성 — 정렬 추가 또는 유일성 보장 확인동일 이름이 다수일 수 있다면 fetchFirst는 정렬 없이 DB 임의 순서를 반환할 수 있어 비결정적입니다. 의도에 따라 아래 중 하나를 권장합니다.
- 유일성 보장(스키마/업무 규칙)이라면 fetchOne 사용 + DB 유니크 인덱스 검토
- 유일성 미보장이라면 결정적 정렬 추가
정렬 추가 예:
TourSpot result = queryFactory.selectFrom(qTourSpot) .where(qTourSpot.tourspotNm.eq(name)) + .orderBy(qTourSpot.tourspotId.asc()) .fetchFirst();
29-37: findByAddress: 복수 가능성 고려 시 정렬 또는 반환 타입 조정Address로 여러 TourSpot이 조회될 수 있으면 fetchFirst의 반환이 임의일 수 있습니다. 인터페이스 논의(해당 파일 코멘트)와 함께 정렬 추가 또는 List 반환으로 일관성을 확보해 주세요.
정렬 추가 예:
TourSpot tourSpot = queryFactory.selectFrom(qTourSpot) .where(qTourSpot.address.eq(address)) + .orderBy(qTourSpot.tourspotId.asc()) .fetchFirst();
16-16: 경미한 중복 제거 제안(Q 인스턴스 재사용)메서드마다
QTourSpot qTourSpot = QTourSpot.tourSpot;를 생성하기보다 클래스 필드로 재사용하면 가독성이 좋아집니다. 선택사항입니다.예:
private static final QTourSpot Q = QTourSpot.tourSpot; // 사용처 queryFactory.selectFrom(Q).where(Q.tourspotNm.eq(name))...
33-35: 인덱스/성능 관점 제안: address_id, tourspot_nm 인덱스 확인where 절이 address, name 동등 비교라면 DB 인덱스 유무가 크게 영향을 줍니다. 운영 데이터 규모를 고려해 인덱스 존재 여부를 확인해 주세요.
- 추천 인덱스: tourspot(address_id), tourspot(tourspot_nm)
- 대안: 조회 빈도/카디널리티에 따라 복합 인덱스 검토
18-37: 리포지토리 테스트 보강 제안새 메서드(findByAddress)와 정렬/유일성 의도에 대한 단위/슬라이스 테스트를 추가하면 회귀 방지에 도움이 됩니다. 필요 시 테스트 템플릿 제공 가능합니다.
src/main/java/com/opendata/domain/tourspot/repository/custom/futureCongestion/CustomFutureCongestionRepository.java (1)
1-1: 패키지 명명 규칙: camelCase 대신 소문자 사용 권장Java 패키지는 관례적으로 모두 소문자를 사용합니다.
futureCongestion는 동작상 문제는 없지만, 케이스 민감하지 않은 파일시스템에서 혼동을 줄이고 일관성을 위해 소문자로 맞추는 것을 권장합니다.아래와 같이 변경을 제안합니다:
-package com.opendata.domain.tourspot.repository.custom.futureCongestion; +package com.opendata.domain.tourspot.repository.custom.futurecongestion;다른 파일에 필요한 연쇄 변경(참고):
- FutureCongestionRepository.java의 import 경로
-import com.opendata.domain.tourspot.repository.custom.futureCongestion.CustomFutureCongestionRepository; +import com.opendata.domain.tourspot.repository.custom.futurecongestion.CustomFutureCongestionRepository;
- CustomFutureCongestionRepositoryImpl.java의 package 경로
-package com.opendata.domain.tourspot.repository.custom.futureCongestion; +package com.opendata.domain.tourspot.repository.custom.futurecongestion;src/main/java/com/opendata/domain/tourspot/repository/custom/futureCongestion/CustomFutureCongestionRepositoryImpl.java (3)
1-1: 패키지 명명 규칙: camelCase 대신 소문자 사용 권장패키지 세그먼트는 소문자 사용이 관례입니다. 가능하면
futurecongestion로 통일을 권장합니다.-package com.opendata.domain.tourspot.repository.custom.futureCongestion; +package com.opendata.domain.tourspot.repository.custom.futurecongestion;
12-14: Custom Repository 구현체에는 @repository 제거 권장 (중복 빈 생성 방지)Spring Data가 리포지토리 프래그먼트를 합성해 프록시 빈을 생성하므로 구현체(@repository)까지 빈으로 등록하면 동일 타입 빈 2개가 생겨 주입 충돌 위험이 있습니다. 구현체의 @repository는 제거하는 것을 권장합니다.
-@Repository @RequiredArgsConstructor public class CustomFutureCongestionRepositoryImpl implements CustomFutureCongestionRepository {참고: @repository 제거 후 불필요한 import(
org.springframework.stereotype.Repository)도 정리 가능합니다.
21-26: QueryDSL 벌크 업데이트 후 1차 캐시 불일치 가능성 — flush/clear 처리 권장JPA 벌크 업데이트는 영속성 컨텍스트를 우회합니다. 동일 트랜잭션 내 후속 조회에서 스테일 데이터가 나올 수 있으니, 필요 시 flush/clear를 고려해 주세요.
변경 예시(해당 범위 내 수정):
- return queryFactory.update(qTourSpotFutureCongestion) + long affected = queryFactory.update(qTourSpotFutureCongestion) .set(qTourSpotFutureCongestion.congestionLvl, newLevel) .where(qTourSpotFutureCongestion.tourspot.tourspotId.eq(tourspotId), qTourSpotFutureCongestion.fcstTime.eq(fcstTime)) - .execute(); + .execute(); + // 영속성 컨텍스트 동기화 (EntityManager 주입 필요) + entityManager.flush(); + entityManager.clear(); + return affected;선행 추가(파일 상단 import 및 필드 주입 필요):
// import 추가 import jakarta.persistence.EntityManager; // 필드 추가 (생성자 주입) private final EntityManager entityManager;혹은 서비스 계층에서 해당 메서드 호출 후 명시적으로 clear를 수행하는 운영 방안도 가능합니다.
src/main/java/com/opendata/domain/tourspot/repository/custom/montlyCongestion/CustomMonthlyCongestionRepositoryImpl.java (1)
16-19: 구현 미완료 메서드가 상수 1L을 반환 — 오동작 가능성
updateCongestionLevel(...)이 현재1L을 반환하는 스텁 상태입니다. 호출 시 정상 동작으로 오인될 수 있어 위험합니다. 최소한 명시적으로 미구현임을 드러내거나, 실제 로직을 구현해주세요.안전장치(선택) diff:
- public long updateCongestionLevel(List<TourSpotMonthlyCongestion> monthlyCongestions) { - - return 1L; - } + public long updateCongestionLevel(List<TourSpotMonthlyCongestion> monthlyCongestions) { + // TODO: Querydsl bulk update 로직 구현 + throw new UnsupportedOperationException("updateCongestionLevel is not implemented yet"); + }
- 실제 구현 시 대량 업데이트는 Querydsl bulk 연산 또는 batch 처리로 구현하고, 반환값은 영향받은 row 수로 맞추는 것을 권장합니다.
- 사용 예정이 없다면 인터페이스/구현에서 일시 제거하거나, 명확한 TODO 주석을 남겨주세요.
src/main/java/com/opendata/global/jwt/JwtFilter.java (1)
50-51: 이미 설정된 인증을 덮어쓰지 않도록 가드 추가 제안SecurityContext에 기존 인증이 있는 경우 새 인증으로 덮어쓰지 않도록 하는 것이 일반적인 패턴입니다.
- Authentication authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); - SecurityContextHolder.getContext().setAuthentication(authToken); + if (SecurityContextHolder.getContext().getAuthentication() == null) { + Authentication authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authToken); + }src/main/java/com/opendata/global/config/SwaggerConfig.java (1)
25-37: 스키마 키 이름을 'BearerAuth'로 변경 권장 (가독성/관례 일치)스키마 키 이름을 HTTP 헤더명(Authorization)과 동일하게 두면 혼동될 수 있습니다. 관례적으로 'BearerAuth'를 사용합니다. 전역 SecurityRequirement 및 Components 등록 키도 함께 변경해 주세요.
- SecurityRequirement securityRequirement = new SecurityRequirement() - .addList("Authorization"); + SecurityRequirement securityRequirement = new SecurityRequirement() + .addList("BearerAuth"); - Components components = new Components() - .addSecuritySchemes("Authorization", apiKey); + Components components = new Components() + .addSecuritySchemes("BearerAuth", apiKey);src/main/java/com/opendata/global/response/status/ErrorStatus.java (1)
29-32: 관광지 관련 에러 코드 추가 — HTTP 상태 코드 정합성 재검토 제안상태 자체는 적절하나, 다음과 같은 HTTP 상태 코드를 고려해볼 수 있습니다.
- 존재하지 않음: NOT_FOUND(404)
- 이미 존재: CONFLICT(409)
- 용량 초과(비즈니스 제약 위반): CONFLICT(409) 또는 BAD_REQUEST(400)
API 소비자 입장에서 상태 코드의 의미가 명확해집니다.
src/main/java/com/opendata/domain/tourspot/entity/TourSpotComponent.java (1)
10-11: JPA 엔티티에 @DaTa 사용은 지양 권장 (equals/hashCode/Setter 과다 생성 이슈)@DaTa는 equals/hashCode/toString/Setter를 모두 생성해 지연 로딩/식별자 기반 비교에서 문제를 유발할 수 있습니다. 엔티티에는 보통 @Getter만 두고, 필요한 경우에 한해 메서드를 추가합니다.
-@Data +@Getter필요 시 개별 필드에 한정하여 Setter를 추가하거나, 생성/팩토리 메서드로만 상태를 변경하도록 유지해 주세요.
Also applies to: 17-20
src/main/java/com/opendata/global/security/CustomUserDetails.java (1)
50-53: getUserEmail()는 getEmail()과 중복 — 하나로 통일 권장이미
getEmail()이 동일 기능을 제공합니다. 현재 코드 전반에서getEmail()과getUserEmail()이 혼용되고 있어 API 일관성이 떨어집니다. 우선 하위 호환을 위해 메서드에 @deprecated를 달고, 점진적으로 호출부를getEmail()로 교체하는 것을 제안합니다.다음 변경을 권장합니다(호출부 정리는 별도 PR로 진행 권장):
- public String getUserEmail() { - return user.getEmail(); - } + /** + * @deprecated getEmail()과 동일합니다. 새 코드는 getEmail()을 사용하세요. + */ + @Deprecated + public String getUserEmail() { + return getEmail(); + }호출부 점검 포인트:
- UserService.findUserByEmailOrThrow는 이미
getEmail()을 사용- Course/마이페이지 서비스 등에서
getUserEmail()사용 중이면getEmail()로 교체 검토src/main/java/com/opendata/domain/course/dto/response/CourseHistoryResponse.java (1)
6-12: ID 타입을 도메인과 맞추고 날짜 포맷 일관성 확보 제안
Course.id가Long이라면 DTO의courseId도Long으로 맞추는 편이 변환 비용/오류를 줄입니다.- 다른 DTO들(
CourseComponentResponse)과 동일하게LocalDateTime에@JsonFormat을 부여해 응답 포맷을 고정하는 것을 권장합니다.package com.opendata.domain.course.dto.response; +import com.fasterxml.jackson.annotation.JsonFormat; import java.time.LocalDateTime; import java.util.List; public record CourseHistoryResponse( - String courseId, - LocalDateTime startDtm, - LocalDateTime endDtm, + Long courseId, + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + LocalDateTime startDtm, + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + LocalDateTime endDtm, List<CourseComponentHistoryDto> history ) { }주의:
- Mapper에서
course.getId()→Long매핑으로 맞추어야 합니다.- 기존 API 스펙이
String을 강제한다면 이 변경은 API 호환성 영향이 있으므로 문서/프론트 확인이 필요합니다.src/main/java/com/opendata/domain/course/repository/custom/CustomCourseComponentRepository.java (1)
7-10: 정렬 기준의 명시성 보강(주석 또는 시그니처에 반영) 권장구현체에서
tourspotTm ASC로 정렬한다면, 인터페이스에도 의도를 남겨두면 협업/유지보수에 유리합니다(메서드명으로 드러내거나 Javadoc으로 명시).public interface CustomCourseComponentRepository { - List<CourseComponent> findAllByCourseId(Long courseId); + /** + * 해당 코스의 컴포넌트를 조회합니다. + * 정렬: tourspotTm ASC + */ + List<CourseComponent> findAllByCourseId(Long courseId); }src/main/java/com/opendata/domain/user/repository/UserRepository.java (1)
4-7: 의도 재확인 요청: Specification 사용 계획 여부
JpaSpecificationExecutor는 import만 있고 확장은 하지 않습니다.
- 스펙 쿼리를 사용할 계획이 없다면 import를 정리하고,
- 사용할 계획이라면
extends JpaSpecificationExecutor<User>추가가 필요합니다.스펙 사용 의도가 있는지 확인 후 다음 중 하나를 적용해 주세요.
옵션 A) 확장 추가:
-public interface UserRepository extends JpaRepository<User, Long>, CustomUserRepository { +public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User>, CustomUserRepository {옵션 B) import 정리:
-import org.springframework.data.jpa.repository.JpaSpecificationExecutor;src/main/java/com/opendata/domain/tourspot/service/TourSpotService.java (1)
70-74: NPE 방어 로직 보강 제안
tourSpotCurrentCongestion가 null이 아닌 경우에도getCongestionLvl()이 null이면 NPE가 발생할 수 있습니다. 레벨 null도 안전하게 처리하도록 방어 로직을 한 단계 보강하세요.다음과 같이 수정 제안드립니다:
- String congestionLabel = null; - if (tourSpotCurrentCongestion != null) { - congestionLabel = tourSpotCurrentCongestion.getCongestionLvl().getCongestionLabel(); - } + String congestionLabel = null; + if (tourSpotCurrentCongestion != null) { + var lvl = tourSpotCurrentCongestion.getCongestionLvl(); + congestionLabel = (lvl != null) ? lvl.getCongestionLabel() : null; + }Also applies to: 79-79
src/main/java/com/opendata/domain/user/dto/UserResponse.java (1)
1-25: DTO 팩토리 메서드 보강 제안(엔티티 중심 변환 추가)현재 정적 팩토리는 필드 나열식입니다. 서비스/컨트롤러에서 중복 매핑을 줄이기 위해
User엔티티를 직접 받아 생성하는 팩토리(또는 Mapper)를 제공하면 재사용성이 높아집니다.다음 메서드 추가를 제안드립니다(필요시
Userimport 추가):public class UserResponse { private String email; private MemberShip membership; private String name; public static UserResponse toUserResponse(String email, MemberShip membership, String name) { return UserResponse.builder() .email(email) .membership(membership) .name(name) .build(); } + + // 필요 시 엔티티 기반 팩토리 추가 + public static UserResponse from(com.opendata.domain.user.entity.User user) { + if (user == null) return null; + return UserResponse.builder() + .email(user.getEmail()) + .membership(user.getMembership()) + .name(user.getName()) + .build(); + } }추가로, Swagger 문서화를 사용 중이라면 각 필드에
@Schema설명을 부여하면 API 명세 가독성이 향상됩니다.src/main/java/com/opendata/domain/course/dto/response/CourseComponentHistoryDto.java (2)
3-10: String 필드에 @jsonformat 적용 비효율 및 시간 포맷 불일치 가능성
- @jsonformat은 보통 날짜/시간 타입(LocalDateTime 등)에 적용할 때 직렬화 포맷에 효과가 있습니다. 현재 String에 붙어 있어 실효성이 떨어집니다.
- 본 PR 내 다른 곳에서는 "yyyy-MM-dd HH:mm" 포맷을 사용합니다(CourseService). 여기서는 초까지 포함("yyyy-MM-dd HH:mm:ss"). 포맷 불일치로 파싱/매핑 이슈 가능성이 있습니다.
권장 수정(예시):
import com.fasterxml.jackson.annotation.JsonFormat; +import java.time.LocalDateTime; public record CourseComponentHistoryDto( - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - String tourSpotTime, + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + LocalDateTime tourSpotTime, String tourSpotName, Long tourspotId, Double lat, Double lon) {
- LocalDateTime으로 변경 시, 관련 Mapper(CourseHistoryMapper)의 dateFormat도 일관되게 맞춰주세요. 프로젝트 전반에서 "초 포함 여부(…:ss)"를 하나로 통일 확인 부탁드립니다.
8-10: 네이밍 일관성 제안: tourSpotName vs tourspotId
- 동일 엔티티 기준으로 필드 네이밍이 혼재되어 있습니다. tourSpotName(카멜) vs tourspotId(소문자 spot). 레코드/DTO 전반에 걸쳐 일관된 스타일을 권장합니다(예: tourspotName, tourspotId).
권장 수정(예시):
- String tourSpotName, + String tourspotName,주의: 변경 시 Mapper 및 프론트/문서(Swagger)까지 영향이 있으므로 팀 합의 후 일괄 반영을 권장합니다.
src/main/java/com/opendata/domain/course/repository/custom/CustomCourseRepositoryImpl.java (1)
27-31: fetch join + orderBy 사용 시 컬렉션 정렬 보장되지 않음
- 컬렉션 fetch join에 대한 orderBy(cc.tourspotTm.asc())는 영속성 컨텍스트 내 컬렉션(List)의 순서를 보장하지 않습니다. 결과 Course의 컬렉션 정렬이 필요하다면:
- 엔티티 매핑에 @orderby("tourspotTm ASC") 추가, 또는
- 조회 후 애플리케이션 단에서 컴포넌트 정렬
엔티티 측 권장(예시):
- Course 엔티티의 courseComponents 매핑에 @orderby("tourspotTm ASC") 추가
src/main/java/com/opendata/domain/tourspot/repository/custom/currentCongestion/CustomCurrentCongestionRepositoryImpl.java (1)
16-21: 파라미터 표기 오탈자(fsctTime) 및 읽기 전용 트랜잭션 권장
- 메서드명/필드가 fcstTime인데 파라미터명이 fsctTime으로 오탈자입니다. 혼란을 줄이기 위해 통일을 권장합니다.
- 조회성 쿼리는 @transactional(readOnly = true) 적용을 권장합니다.
권장 수정(예시):
import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; @Repository @RequiredArgsConstructor public class CustomCurrentCongestionRepositoryImpl implements CustomCurrentCongestionRepository { private final JPAQueryFactory queryFactory; @Override - public TourSpotCurrentCongestion findByTourSpotAndCurTime(TourSpot tourSpot, String fsctTime) { + @Transactional(readOnly = true) + public TourSpotCurrentCongestion findByTourSpotAndCurTime(TourSpot tourSpot, String fcstTime) { QTourSpotCurrentCongestion qTourSpotCurrentCongestion = QTourSpotCurrentCongestion.tourSpotCurrentCongestion; return queryFactory.selectFrom(qTourSpotCurrentCongestion) - .where(qTourSpotCurrentCongestion.tourspot.eq(tourSpot), qTourSpotCurrentCongestion.fcstTime.eq(fsctTime)) + .where(qTourSpotCurrentCongestion.tourspot.eq(tourSpot), + qTourSpotCurrentCongestion.fcstTime.eq(fcstTime)) .fetchFirst(); } }src/main/java/com/opendata/domain/course/service/CourseService.java (1)
59-105: Optional.get() 무방비 사용(참고 사항)
- 본 구간은 이번 변경 범위는 아니지만, congestion.get() 호출은 빈 Optional일 경우 예외가 발생합니다. 안전 처리 고려 바랍니다.
권장 수정(아이디어): orElse, orElseThrow, 또는 기본 CongestionLevel 부여 등 도메인 정책에 맞게 처리
src/main/java/com/opendata/docs/CourseControllerDocs.java (2)
63-69: Swagger에 인증 Principal 파라미터 숨김 권장 (@parameter(hidden = true))OpenAPI 문서에
customUserDetails가 노출되면 혼선을 줄 수 있습니다. 인증 컨텍스트에서 주입되는 파라미터는 숨기는 것이 일반적입니다.아래처럼 파라미터에
@Parameter(hidden = true)를 추가하세요.- @AuthenticationPrincipal CustomUserDetails customUserDetails, + @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails customUserDetails, @RequestParam double lat, @RequestParam double lon, @RequestParam String startTime, @RequestParam String endTime, @RequestParam String tourspot추가로 필요한 import (선택):
// 파일 상단 import 섹션에 추가 import io.swagger.v3.oas.annotations.Parameter;또한, 본 API가 인증이 필요한 경우라면
@Operation에 SecurityRequirement를 명시하면 Swagger UI에서 토큰 연동이 보다 명확해집니다(전역 설정이 이미 있다면 생략 가능).import io.swagger.v3.oas.annotations.security.SecurityRequirement; @Operation( summary = "...", description = "...", security = @SecurityRequirement(name = "bearerAuth") )
94-96: PathVariable 이름 명시 및 Principal 숨김
@PathVariable은 명시적으로 변수명을 지정하면 구현부/문서 상의 혼선을 줄일 수 있습니다.- 마찬가지로
@AuthenticationPrincipal은 문서에서 숨기는 편이 좋습니다.- @AuthenticationPrincipal CustomUserDetails customUserDetails, - @PathVariable String courseId + @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails customUserDetails, + @PathVariable("courseId") String courseId필요 시 상단에 다음 import 추가:
import io.swagger.v3.oas.annotations.Parameter;src/main/java/com/opendata/domain/tourspot/repository/CurrentCongestionRepository.java (1)
4-4: 패키지명에 CamelCase 사용: 자바 패키지 컨벤션과 불일치
custom.currentCongestion처럼 CamelCase가 섞인 패키지명은 자바 컨벤션(소문자, 점 구분)과 맞지 않습니다. OS/툴체인에 따라 경로 대소문자 이슈를 유발할 수 있어 추후 유지보수성에 불리합니다.권장:
com.opendata.domain.tourspot.repository.custom.currentcongestion- 또는 의미를 더 간결히 한
custom.current등패키지명을 일괄 변경 시, 구현체/사용처 import도 함께 정리해야 합니다.
src/main/java/com/opendata/domain/tourspot/repository/custom/currentCongestion/CustomCurrentCongestionRepository.java (1)
1-8: 오타 및 네이밍 일관성: fsctTime → fcstTime, CurTime → FcstTime
- 파라미터명이
fsctTime로 오타입니다. 엔티티 필드(fcstTime) 및 다른 레포(findByTourSpotIdAndFcstTime)와 일관되게 맞추세요.- 메서드명도
CurTime대신FcstTime로 통일하면 가독성과 검색성이 좋아집니다.-package com.opendata.domain.tourspot.repository.custom.currentCongestion; +package com.opendata.domain.tourspot.repository.custom.currentCongestion; @@ public interface CustomCurrentCongestionRepository { - TourSpotCurrentCongestion findByTourSpotAndCurTime(TourSpot tourSpot, String fsctTime); + TourSpotCurrentCongestion findByTourSpotAndFcstTime(TourSpot tourSpot, String fcstTime); }추가 제안(선택):
- 다른 커스텀 레포지토리(
CustomFutureCongestionRepository)와 동일하게 반환 타입을Optional<TourSpotCurrentCongestion>로 맞추면 널 처리/오류 전파가 명확해집니다.- TourSpotCurrentCongestion findByTourSpotAndFcstTime(TourSpot tourSpot, String fcstTime); + Optional<TourSpotCurrentCongestion> findByTourSpotAndFcstTime(TourSpot tourSpot, String fcstTime);이 경우 구현체와 사용처도 함께 수정해야 합니다.
src/main/java/com/opendata/domain/course/mapper/CourseHistoryMapper.java (2)
28-33: dateFormat 제거 권장: 대상 타입이 String이 아닐 경우 무의미하거나 혼동 유발
tourspotTm→tourSpotTime매핑에서dateFormat은 보통 String↔Date/Time 타입 변환 시에만 의미가 있습니다.CourseComponentHistoryDto.tourSpotTime이LocalDateTime(예:@JsonFormat)이라면dateFormat은 제거하는 편이 명확합니다.- @Mapping(target = "tourSpotTime", source = "tourspotTm", dateFormat = "yyyy-MM-dd HH:mm:ss") + @Mapping(target = "tourSpotTime", source = "tourspotTm") @Mapping(target = "tourSpotName", source = "tourSpot.tourspotNm") @Mapping(target = "tourspotId", source = "tourSpot.tourspotId") @Mapping(target = "lat", source = "tourSpot.address.latitude") @Mapping(target = "lon", source = "tourSpot.address.longitude") CourseComponentHistoryDto toHistoryDto(CourseComponent component);만약 반환을 문자열로 원하신다면 DTO의 필드 타입을
String으로 변경하거나, 별도의@Named포매팅 메서드를 제공해주시는 방식을 고려할 수 있습니다.
15-18: 중첩 널 안전성 확보(MapStruct null-check 전략 추가) 권장
tourSpot.address.latitude/longitude접근 시 중간 객체가 null이면 NPE가 발생할 수 있습니다. 도메인 제약으로 null이 절대 아니라면 그대로 유지해도 되지만, 안전하게 가려면 null-check 전략을 켜는 방법이 있습니다.@Mapper( - componentModel = SPRING, - unmappedTargetPolicy = ReportingPolicy.IGNORE + componentModel = SPRING, + unmappedTargetPolicy = ReportingPolicy.IGNORE, + nullValueCheckStrategy = org.mapstruct.NullValueCheckStrategy.ALWAYS )필요 시 import 추가:
import org.mapstruct.NullValueCheckStrategy;src/main/java/com/opendata/domain/tourspot/repository/custom/component/CustomTourSpotComponentRepository.java (1)
13-14: 삭제 결과 반환값 제공 고려
deleteByUserIdAndTourSpotId가void를 반환하면 실제 삭제 건수를 상위 계층에서 검증하기 어렵습니다. 삭제된 행 수를 반환하면 서비스 계층에서 결과 검증/로깅이 용이합니다.- void deleteByUserIdAndTourSpotId(Long userId, Long tourSpotId); + long deleteByUserIdAndTourSpotId(Long userId, Long tourSpotId);추가로, 계정 탈퇴 시 일괄정리를 고려해
long deleteByUserId(Long userId);같은 메서드도 유용할 수 있습니다.src/main/java/com/opendata/domain/course/repository/custom/CustomCourseComponentRepositoryImpl.java (2)
4-4: 미사용 import 제거 제안(QCourse)QCourse는 사용되지 않습니다. 정리해 두면 경고 제거 및 가독성에 좋습니다.
적용 diff:
-import com.opendata.domain.course.entity.QCourse;
20-25: 중복 방지용 distinct() 추가를 고려현재는 ManyToOne(fetch join)이라 중복 가능성이 낮지만, 조인 변경 시 중복이 발생할 수 있습니다. 방어적으로 distinct()를 추가하는 것을 고려해 주세요.
적용 diff:
return queryFactory .selectFrom(cc) .leftJoin(cc.tourSpot, ts).fetchJoin() .where(cc.course.id.eq(courseId)) + .distinct() .orderBy(cc.tourspotTm.asc()) .fetch();src/main/java/com/opendata/domain/tourspot/repository/custom/component/CustomTourSpotComponentRepositoryImpl.java (2)
4-4: 미사용 import 제거 제안(TourSpot)TourSpot 타입은 사용되지 않습니다. 제거를 권장합니다.
적용 diff:
-import com.opendata.domain.tourspot.entity.TourSpot;
40-48: count 쿼리 null 안전성 보강(선택사항)COUNT는 일반적으로 0을 반환해 null 가능성이 낮지만, 방어적으로 null 대비를 넣어두면 안전합니다.
적용 diff:
public long countByUserId(Long userId) { QTourSpotComponent q = QTourSpotComponent.tourSpotComponent; - return queryFactory - .select(q.count()) - .from(q) - .where(q.userId.eq(userId)) - .fetchOne(); + Long count = queryFactory + .select(q.count()) + .from(q) + .where(q.userId.eq(userId)) + .fetchOne(); + return count != null ? count : 0L; }src/main/java/com/opendata/domain/course/controller/CourseController.java (2)
25-35: findCourses의 @AuthenticationPrincipal 파라미터가 사용되지 않음 — 의도 확인 필요인증이 필수인 엔드포인트라면 파라미터를 실제 로직에 활용하거나, 보안 어노테이션 추가(@PreAuthorize 등)로 의도를 명확히 해주세요. 공개 엔드포인트라면 파라미터 제거를 고려해도 됩니다.
37-43: 좋아요 API 인증 강제 여부와 Null Principal 대응 확인postCourseLike는 인증 사용자를 요구하는 흐름으로 보입니다. Security 설정에서 해당 경로를 인증 필수로 강제하고, 혹시나 null principal이 주입될 수 있는 상황에 대한 방어가 서비스 레이어에 있는지 확인 부탁드립니다. 필요 시 아래처럼 선언적 권한 체크를 추가할 수 있습니다.
예시:
+import org.springframework.security.access.prepost.PreAuthorize; ... -@PostMapping("/like/{courseId}") +@PreAuthorize("isAuthenticated()") +@PostMapping("/like/{courseId}") public ResponseEntity<ApiResponse<Void>> postCourseLike( @AuthenticationPrincipal CustomUserDetails customUserDetails, @PathVariable String courseId){src/main/java/com/opendata/docs/MyPageControllerDocs.java (2)
8-22: Swagger에 보안 요구사항을 클래스 레벨로 명시하면 문서 일관성이 좋아집니다전역 설정이 있더라도, 문서 상에서 잠금 아이콘/Authorize 버튼 연계를 위해 명시적 @securityrequirement 추가를 권장합니다. 스키마 이름은 프로젝트 설정과 일치해야 합니다(예: "bearerAuth").
적용 diff:
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.RequestParam; import java.util.List; @Tag(name = "마이페이지 API") +@SecurityRequirement(name = "bearerAuth") public interface MyPageControllerDocs {
106-110: 메서드명(updateTourSpot)과 설명(“추가”)이 불일치합니다동작이 “추가”라면 add/save 계열 네이밍으로 맞추는 것이 더 명확합니다. 컨트롤러/서비스 시그니처와 함께 일관성 확보를 권장합니다.
src/main/java/com/opendata/domain/mypage/controller/MypageController.java (4)
31-38: POST /preferences는 201 Created가 더 적합하며, 메서드명도 추가 동작을 반영하는 것이 좋습니다리소스 생성(“선호 관광지 추가”) 성격의 엔드포인트이므로 201을 반환하는 것이 REST 관례에 더 맞습니다. 또한 메서드명을 update→add로 바꾸면 의도가 명확해집니다.
적용 예시:
@PostMapping("/preferences") - public ResponseEntity<ApiResponse<Void>> updateTourSpot( + public ResponseEntity<ApiResponse<Void>> addTourSpot( @AuthenticationPrincipal CustomUserDetails customUserDetails, @RequestParam Long tourSpotId ){ mypageService.saveUserTourSpot(customUserDetails,tourSpotId); - return ResponseEntity.ok(ApiResponse.onSuccessVoid()); + return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.onSuccessVoid()); }추가 import (파일 상단):
import org.springframework.http.HttpStatus;
33-35: tourSpotId 파라미터에 유효성 검증 어노테이션을 추가하세요음수/널 입력을 방지하기 위해 @NotNull, @positive를 권장합니다. 클래스에 @validated도 함께 적용해야 동작합니다.
- @RequestParam Long tourSpotId + @RequestParam @NotNull @Positive Long tourSpotId추가 import:
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive;
18-21: 컨트롤러에 @validated 추가로 메서드 파라미터 검증 활성화메서드 파라미터의 Bean Validation을 활성화하려면 클래스에 @validated가 필요합니다.
@RestController @RequiredArgsConstructor +@Validated @RequestMapping("/mypage") public class MypageController implements MyPageControllerDocs추가 import:
import org.springframework.validation.annotation.Validated;
45-52: DELETE는 PathVariable 사용을 권장합니다 (일관성 및 REST 관례 측면)리소스 식별자는 URL 경로로 표현하는 것이 일반적입니다. 기존 클라이언트 영향 범위를 고려해 선택 적용을 권장합니다. 문서(MyPageControllerDocs)와의 동기화도 함께 확인 바랍니다.
- @DeleteMapping("/preferences") + @DeleteMapping("/preferences/{tourSpotId}") public ResponseEntity<ApiResponse<Void>> deleteTourSpot( @AuthenticationPrincipal CustomUserDetails customUserDetails, - @RequestParam Long tourSpotId + @PathVariable Long tourSpotId ){ mypageService.deleteTourSpot(customUserDetails,tourSpotId); return ResponseEntity.ok(ApiResponse.onSuccessVoid()); }src/main/java/com/opendata/domain/mypage/service/MypageService.java (2)
87-101: JsonProcessingException을 도메인 예외로 변환하고, 리스트 생성 일관성 유지현재 RuntimeException으로 래핑하면 글로벌 에러 핸들러에서 일관된 에러 응답을 만들기 어렵습니다. GlobalException + ErrorStatus로 변환하고, 컬렉션 수집은 toList()로 맞추세요.
return tourSpotComponentRepository.findAllByUserId(userId).stream() .map(tourSpotComponent -> { try { return tourSpotService.combineTourSpotDetail(tourSpotComponent.getTourSpotId()); } catch (JsonProcessingException e) { - throw new RuntimeException(e); + throw new GlobalException(ErrorStatus.INTERNAL_SERVER_ERROR); } }) - .collect(Collectors.toList()); + .toList();ErrorStatus.INTERNAL_SERVER_ERROR가 없다면 공통 서버 오류 상태를 하나 정의해 사용하세요. 필요 시 정의/매핑도 도와드릴 수 있습니다.
44-53: 매직 넘버(5) 상수화로 가독성과 변경 용이성 개선선호 관광지 최대 개수 5는 상수로 추출해 의미를 부여하면 유지보수성이 좋아집니다.
public class MypageService { + private static final int MAX_PREFERENCES = 5; @@ - if (tourSpotComponentRepository.countByUserId(userId) >= 5) { + if (tourSpotComponentRepository.countByUserId(userId) >= MAX_PREFERENCES) { throw new GlobalException(ErrorStatus.TOURSPOT_EXCEEDS); }Also applies to: 80-81
| @RequiredArgsConstructor | ||
| public class CustomCourseComponentRepositoryImpl implements CustomCourseComponentRepository { | ||
| private final JPAQueryFactory queryFactory; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@repository 누락으로 커스텀 구현체가 빈으로 등록되지 않습니다 (런타임 실패 가능)
현재 클래스는 JPAQueryFactory를 생성자 주입(@requiredargsconstructor)으로 사용하지만, 스프링 빈이 아니면 주입되지 않습니다. Spring Data가 커스텀 프래그먼트 구현을 찾더라도 EntityManager 기반 생성자가 없으면 인스턴스화에 실패할 수 있습니다. @repository를 추가해 주세요.
적용 diff:
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
import java.util.List;
-@RequiredArgsConstructor
+@Repository
+@RequiredArgsConstructor
public class CustomCourseComponentRepositoryImpl implements CustomCourseComponentRepository {
private final JPAQueryFactory queryFactory;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @RequiredArgsConstructor | |
| public class CustomCourseComponentRepositoryImpl implements CustomCourseComponentRepository { | |
| private final JPAQueryFactory queryFactory; | |
| import com.querydsl.jpa.impl.JPAQueryFactory; | |
| import lombok.RequiredArgsConstructor; | |
| import org.springframework.stereotype.Repository; | |
| import java.util.List; | |
| @Repository | |
| @RequiredArgsConstructor | |
| public class CustomCourseComponentRepositoryImpl implements CustomCourseComponentRepository { | |
| private final JPAQueryFactory queryFactory; |
🤖 Prompt for AI Agents
In
src/main/java/com/opendata/domain/course/repository/custom/CustomCourseComponentRepositoryImpl.java
around lines 12-14, the class is not annotated as a Spring bean so constructor
injection can fail at runtime; add the @Repository annotation to the class
declaration and ensure Spring can instantiate it by providing a constructor that
accepts an EntityManager and creates the JPAQueryFactory (or alternatively
autowire an existing JPAQueryFactory bean) so the custom repository
implementation is registered and injectable at runtime.
| public interface CustomCourseRepository { | ||
| List<Course> findAllByUserId(Long userId); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
findAllByUserId 추가 OK — 다만 Course-User 매핑 존재 여부 확인 필요 + 페이징 확장 제안
- 구현부(QueryDSL)에서
c.user.id.eq(userId)를 사용할 가능성이 높습니다.Course엔티티에User user연관관계가 실제로 선언되어 있는지 확인이 필요합니다(제공된 스니펫에는 주석 처리 흔적이 보임). - 사용량이 많을 수 있는 마이페이지 히스토리 특성상 페이징/슬라이싱 시그니처도 함께 제공하면 확장성이 좋아집니다.
확인용 스크립트(엔티티에 User 연관관계가 있는지 탐색):
선택적 확장(페이징 시그니처 추가 제안):
+import org.springframework.data.domain.Pageable;
+
public interface CustomCourseRepository {
List<Course> findAllByUserId(Long userId);
+ List<Course> findAllByUserId(Long userId, Pageable pageable);
}🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Course 엔티티 파일 찾기
course_file="$(fd -a 'Course.java' | rg 'domain/course/entity/Course\.java' || true)"
if [ -z "$course_file" ]; then
echo "Course.java 파일을 찾지 못했습니다."
exit 1
fi
echo "검사 대상: $course_file"
# User 연관 필드/어노테이션 유무 확인
rg -n -C3 -e 'class\s+Course\b' -e '@ManyToOne' -e '@JoinColumn' -e 'private\s+User\b' "$course_file"Length of output: 1097
findAllByUserId 추가 OK — Course-User 매핑 확인됨; 페이징 오버로드 권장
검증 결과: Course 엔티티에 User 연관관계(@manytoone, @joincolumn, private User user)가 존재하므로 현재 인터페이스 시그니처는 적절합니다. 다만 마이페이지 사용량을 고려해 페이징/슬라이스 오버로드 추가를 권장합니다.
- 확인된 위치
- src/main/java/com/opendata/domain/course/entity/Course.java — @manytoone(fetch = FetchType.LAZY) / @joincolumn(name = "user_id") / private User user (라인 약 24–26)
- src/main/java/com/opendata/domain/course/repository/custom/CustomCourseRepository.java — findAllByUserId(Long userId) (PR 스니펫: 라인 8–10)
권장 변경(선택):
+import org.springframework.data.domain.Pageable;
+
public interface CustomCourseRepository {
List<Course> findAllByUserId(Long userId);
+ List<Course> findAllByUserId(Long userId, Pageable pageable);
}선택적으로 반환형을 Page 또는 Slice로 변경하는 것도 고려하세요.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public interface CustomCourseRepository { | |
| List<Course> findAllByUserId(Long userId); | |
| } | |
| import org.springframework.data.domain.Pageable; | |
| public interface CustomCourseRepository { | |
| List<Course> findAllByUserId(Long userId); | |
| List<Course> findAllByUserId(Long userId, Pageable pageable); | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/com/opendata/domain/course/repository/custom/CustomCourseRepository.java
around lines 8 to 10, add paginated overloads for findAllByUserId so callers can
request results with Pageable; specifically keep the existing List<Course>
findAllByUserId(Long userId) and add signatures like Page<Course>
findAllByUserId(Long userId, Pageable pageable) and/or Slice<Course>
findAllByUserId(Long userId, Pageable pageable), and ensure corresponding
imports (org.springframework.data.domain.Page, Slice, Pageable) and any custom
implementation are updated to support the new methods.
| import com.opendata.domain.course.entity.Course; | ||
| import com.opendata.domain.course.entity.QCourse; | ||
| import com.opendata.domain.course.entity.QCourseComponent; | ||
| import com.opendata.domain.tourspot.entity.QTourSpot; | ||
| import com.querydsl.jpa.impl.JPAQueryFactory; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @RequiredArgsConstructor | ||
| public class CustomCourseRepositoryImpl implements CustomCourseRepository{ | ||
|
|
||
| private final JPAQueryFactory queryFactory; | ||
|
|
||
| @Override |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Custom Repository에서 JPAQueryFactory 주입 방식 오류 가능성
- Spring Data 커스텀 리포지토리 구현은 보통 ‘Impl’ 클래스를 리포지토리 인프라가 직접 인스턴스화합니다. 이때 EntityManager 인자를 받는 생성자를 통해 내부에서 JPAQueryFactory를 생성하는 패턴이 안전합니다.
- 현재 @requiredargsconstructor + JPAQueryFactory 필드 주입 방식은 빈 주입이 보장되지 않아 런타임 주입 실패(NoSuchBeanDefinition 등) 가능성이 큽니다.
권장 수정(예시):
import com.querydsl.jpa.impl.JPAQueryFactory;
+import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
-@RequiredArgsConstructor
public class CustomCourseRepositoryImpl implements CustomCourseRepository{
-
- private final JPAQueryFactory queryFactory;
+ private final JPAQueryFactory queryFactory;
+
+ public CustomCourseRepositoryImpl(EntityManager em) {
+ this.queryFactory = new JPAQueryFactory(em);
+ }설명: 이 패턴은 Spring Data가 Impl 클래스를 생성할 때 EntityManager를 전달해주므로, 추가 빈 주입 없이 안정적으로 동작합니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import com.opendata.domain.course.entity.Course; | |
| import com.opendata.domain.course.entity.QCourse; | |
| import com.opendata.domain.course.entity.QCourseComponent; | |
| import com.opendata.domain.tourspot.entity.QTourSpot; | |
| import com.querydsl.jpa.impl.JPAQueryFactory; | |
| import lombok.RequiredArgsConstructor; | |
| import java.util.List; | |
| @RequiredArgsConstructor | |
| public class CustomCourseRepositoryImpl implements CustomCourseRepository{ | |
| private final JPAQueryFactory queryFactory; | |
| @Override | |
| import com.opendata.domain.course.entity.Course; | |
| import com.opendata.domain.course.entity.QCourse; | |
| import com.opendata.domain.course.entity.QCourseComponent; | |
| import com.opendata.domain.tourspot.entity.QTourSpot; | |
| import com.querydsl.jpa.impl.JPAQueryFactory; | |
| import jakarta.persistence.EntityManager; | |
| import lombok.RequiredArgsConstructor; | |
| import java.util.List; | |
| public class CustomCourseRepositoryImpl implements CustomCourseRepository{ | |
| private final JPAQueryFactory queryFactory; | |
| public CustomCourseRepositoryImpl(EntityManager em) { | |
| this.queryFactory = new JPAQueryFactory(em); | |
| } | |
| @Override |
🤖 Prompt for AI Agents
In
src/main/java/com/opendata/domain/course/repository/custom/CustomCourseRepositoryImpl.java
around lines 4 to 18, the class currently relies on a JPAQueryFactory being
injected via @RequiredArgsConstructor which can fail when Spring Data
instantiates the custom repository; change the implementation to accept an
EntityManager in the constructor (remove @RequiredArgsConstructor and the
injected JPAQueryFactory field), and inside that constructor instantiate a new
JPAQueryFactory(entityManager) to use for queries so the repository is created
reliably by Spring Data without requiring an external JPAQueryFactory bean.
| public void likeCourse(String courseId, CustomUserDetails customUserDetails) { | ||
| String email = customUserDetails.getUserEmail(); | ||
| User user = userRepository.findUserByEmail(email); | ||
| ObjectMapper objectMapper = new ObjectMapper(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
인증 사용자 조회 시 NPE 가능성 — NotFound 처리 추가 권장
- userRepository.findUserByEmail(email) 결과가 null일 수 있어 이후 빌더(user 세팅)에서 NPE/제약 위반 위험이 있습니다. 도메인 표준 예외로 명시적으로 처리하세요.
권장 수정(예시):
@Transactional
public void likeCourse(String courseId, CustomUserDetails customUserDetails) {
- String email = customUserDetails.getUserEmail();
- User user = userRepository.findUserByEmail(email);
+ String email = customUserDetails.getUserEmail();
+ User user = Optional.ofNullable(userRepository.findUserByEmail(email))
+ .orElseThrow(() -> new UserNotFoundException(UserMessages.USER_NOT_FOUND));참고: UserNotFoundException/UserMessages 네임스페이스는 프로젝트의 기존 사용 위치와 동일하게 맞춰주세요.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public void likeCourse(String courseId, CustomUserDetails customUserDetails) { | |
| String email = customUserDetails.getUserEmail(); | |
| User user = userRepository.findUserByEmail(email); | |
| ObjectMapper objectMapper = new ObjectMapper(); | |
| public void likeCourse(String courseId, CustomUserDetails customUserDetails) { | |
| String email = customUserDetails.getUserEmail(); | |
| User user = Optional.ofNullable(userRepository.findUserByEmail(email)) | |
| .orElseThrow(() -> new UserNotFoundException(UserMessages.USER_NOT_FOUND)); | |
| ObjectMapper objectMapper = new ObjectMapper(); |
🤖 Prompt for AI Agents
In src/main/java/com/opendata/domain/course/service/CourseService.java around
lines 213 to 216, the call userRepository.findUserByEmail(email) can return null
which may cause an NPE later; update the code to explicitly handle the not-found
case by replacing the raw null-return usage with a safe lookup that throws the
project’s standard domain exception (e.g. UserNotFoundException or equivalent
from UserMessages) when the user is absent (use Optional/orElseThrow or an
if-null throw), and ensure the thrown exception includes the email for context.
| public List<CourseHistoryResponse> getCourses(CustomUserDetails customUserDetails) | ||
| { | ||
| Long userId = customUserDetails.getUserId(); | ||
|
|
||
| return courseRepository.findAllByUserId(userId).stream() | ||
| .map(course -> { | ||
| var components = Optional.ofNullable(course.getCourseComponents()) | ||
| .orElseGet(Collections::emptyList); | ||
|
|
||
| var history = components.stream() | ||
| .map(courseHistoryMapper::toHistoryDto) | ||
| .toList(); | ||
|
|
||
| return courseHistoryMapper.toHistoryResponse(course, history); | ||
| }) | ||
| .toList(); | ||
|
|
||
|
|
||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
getCourses: N+1 위험과 페이징 필요성 점검
- courseRepository.findAllByUserId(userId)가 코스-컴포넌트 연관 로딩 시 fetch join(또는 batch fetch)을 사용하지 않으면 N+1이 발생할 수 있습니다.
- 사용자 코스 기록이 많을 수 있으므로 페이징 도입도 고려해 주세요.
다음 스크립트로 해당 메서드 구현을 확인해 주세요(페치 조인 여부 포함):
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# findAllByUserId 정의/사용부 확인
rg -nP --type=java -C3 '\bfindAllByUserId\s*\('
# fetch join 사용 흔적 탐색
rg -nP --type=java -C2 '(join\s+fetch|fetchJoin)'Length of output: 7009
getCourses: fetch join으로 N+1는 해소되었으나 — 페이징 도입 필요
확인 요약:
- 확인: src/main/java/com/opendata/domain/course/repository/custom/CustomCourseRepositoryImpl.java 의 findAllByUserId(...)에서 QueryDSL로
.leftJoin(c.courseComponents, cc).fetchJoin() 및 .leftJoin(cc.tourSpot, ts).fetchJoin()를 사용하고 있어 연관 엔티티를 미리 로드합니다(따라서 N+1 문제는 해소됨). - 참고: selectDistinct(c)로 중복 코스도 제거하고 있습니다.
권장 조치(필수 권고):
- 페이징 도입: 사용자의 코스 수가 많을 수 있으므로 페이징 또는 제한(limit) 도입 검토
- 구현 방식 예시:
- repository에 Pageable/limit/offset 파라미터 추가(또는 별도 페이징 메서드)하고 QueryDSL에 .limit/.offset 적용, 총건수 카운트 쿼리 제공 → Page 형태로 반환.
- 또는 service에서 page/size 파라미터를 받아 부분 로드 후 반환 형식 변경.
- 구현 방식 예시:
- 변경이 필요한 위치:
- src/main/java/com/opendata/domain/mypage/service/MypageService.java — getCourses(...) (현재 54–72행)
- src/main/java/com/opendata/domain/course/repository/custom/CustomCourseRepositoryImpl.java — findAllByUserId(...): 페이징 파라미터/쿼리 추가
🤖 Prompt for AI Agents
In src/main/java/com/opendata/domain/mypage/service/MypageService.java around
lines 54–72 and in
src/main/java/com/opendata/domain/course/repository/custom/CustomCourseRepositoryImpl.java,
introduce paging to avoid unbounded result sets: change the service
getCourses(...) signature to accept page and size (or a Pageable) and call a new
repository method that accepts limit/offset or Pageable; update
CustomCourseRepositoryImpl.findAllByUserId(...) to apply QueryDSL .limit(...)
and .offset(...) (and implement a matching count query) so you can return a
Page-like result, then adapt the service to map the page content to
List<CourseHistoryResponse> and return a Page<CourseHistoryResponse> (or a DTO
containing items + total/pages) accordingly.
| @@ -1,4 +1,4 @@ | |||
| package com.opendata.domain.tourspot.repository.custom; | |||
| package com.opendata.domain.tourspot.repository.custom.montlyCongestion; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
패키지명 오탈자(montly → monthly) 수정 필요
인터페이스와 동일하게 패키지 오탈자를 수정해주세요. 구현체와 인터페이스의 패키지가 항상 일치해야 합니다.
-package com.opendata.domain.tourspot.repository.custom.montlyCongestion;
+package com.opendata.domain.tourspot.repository.custom.monthlyCongestion;폴더명도 함께 montlyCongestion → monthlyCongestion로 변경되어야 합니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| package com.opendata.domain.tourspot.repository.custom.montlyCongestion; | |
| package com.opendata.domain.tourspot.repository.custom.monthlyCongestion; |
🤖 Prompt for AI Agents
In
src/main/java/com/opendata/domain/tourspot/repository/custom/montlyCongestion/CustomMonthlyCongestionRepositoryImpl.java
around line 1, the package name has a typo ("montlyCongestion" should be
"monthlyCongestion"); update the package declaration to
com.opendata.domain.tourspot.repository.custom.monthlyCongestion, rename the
directory from montlyCongestion to monthlyCongestion to match, update any
imports/usages and the interface package to the corrected package name so the
implementation and interface packages match, and run a compile/IDE refactor to
fix any remaining references.
|
|
||
| import com.opendata.domain.tourspot.entity.TourSpotMonthlyCongestion; | ||
| import com.opendata.domain.tourspot.repository.custom.CustomMonthlyCongestionRepository; | ||
| import com.opendata.domain.tourspot.repository.custom.montlyCongestion.CustomMonthlyCongestionRepository; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
import 경로 오탈자(montly → monthly)로 인한 컴파일 오류 가능
패키지 오탈자 수정에 맞춰 import 경로도 함께 변경되어야 합니다.
-import com.opendata.domain.tourspot.repository.custom.montlyCongestion.CustomMonthlyCongestionRepository;
+import com.opendata.domain.tourspot.repository.custom.monthlyCongestion.CustomMonthlyCongestionRepository;추가 확인:
- Spring Data JPA 커스텀 프래그먼트 결합 규칙상
CustomMonthlyCongestionRepositoryImpl명명은 적절합니다. 패키지 이동 후에도 base package 스캔 범위 내에 있으면 자동 결합됩니다.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "→ Searching repo for occurrences of 'montly' (common typo)..."
rg -n --hidden -S "montly" || true
echo
echo "→ Searching repo for occurrences of 'monthlyCongestion' (expected correct package)..."
rg -n --hidden -S "monthlyCongestion" || true
echo
echo "→ Searching for types/filenames referenced in the review (CustomMonthlyCongestionRepository, MonthlyCongestionRepository)..."
rg -n --hidden -S "CustomMonthlyCongestionRepository|MonthlyCongestionRepository" || true
echo
TARGET="src/main/java/com/opendata/domain/tourspot/repository/MonthlyCongestionRepository.java"
if [ -f "$TARGET" ]; then
echo "→ Showing top of $TARGET"
sed -n '1,200p' "$TARGET"
else
echo "→ File $TARGET not found in workspace."
fiLength of output: 5704
패키지 오타(montly → monthly) 수정 필요
검증 결과 코드베이스에 montlyCongestion 패키지명이 실제로 사용되고 있습니다. 단순 import 수정만으로는 불일치가 발생하니 패키지 선언, 디렉터리명, 관련 import를 함께 변경하세요.
수정 대상(예시)
- src/main/java/com/opendata/domain/tourspot/repository/MonthlyCongestionRepository.java — import 수정
- src/main/java/com/opendata/domain/tourspot/repository/custom/montlyCongestion/CustomMonthlyCongestionRepository.java — package 선언 수정
- src/main/java/com/opendata/domain/tourspot/repository/custom/montlyCongestion/CustomMonthlyCongestionRepositoryImpl.java — package 선언 수정
- 디렉터리명 변경: src/main/java/com/opendata/domain/tourspot/repository/custom/montlyCongestion → .../monthlyCongestion
권장 diff 예시:
- import com.opendata.domain.tourspot.repository.custom.montlyCongestion.CustomMonthlyCongestionRepository;
+ import com.opendata.domain.tourspot.repository.custom.monthlyCongestion.CustomMonthlyCongestionRepository;패키지 선언(두 파일 모두)
- package com.opendata.domain.tourspot.repository.custom.montlyCongestion;
+ package com.opendata.domain.tourspot.repository.custom.monthlyCongestion;주의 및 확인사항:
- CustomMonthlyCongestionRepositoryImpl 네이밍은 적절합니다. 구현 클래스가 base package 스캔 범위 내에 있으면 Spring Data JPA가 커스텀 프래그먼트를 자동으로 결합합니다.
- 변경 후 프로젝트 전체 검색으로 남은 참조(import/패키지)를 모두 갱신하고 빌드 확인하세요.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import com.opendata.domain.tourspot.repository.custom.montlyCongestion.CustomMonthlyCongestionRepository; | |
| import com.opendata.domain.tourspot.repository.custom.monthlyCongestion.CustomMonthlyCongestionRepository; |
🤖 Prompt for AI Agents
In
src/main/java/com/opendata/domain/tourspot/repository/MonthlyCongestionRepository.java
around line 4, the import references the misspelled package "montlyCongestion";
update import to use "monthlyCongestion" and then rename the package declaration
in
src/main/java/com/opendata/domain/tourspot/repository/custom/montlyCongestion/CustomMonthlyCongestionRepository.java
and CustomMonthlyCongestionRepositoryImpl.java to
"com.opendata.domain.tourspot.repository.custom.monthlyCongestion", move/rename
the directory
src/main/java/com/opendata/domain/tourspot/repository/custom/montlyCongestion →
.../monthlyCongestion, and run a project-wide search to update all
imports/references and verify the project builds successfully.
| User findUserById(Long userId); | ||
| void deleteUserById(Long userId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
JpaRepository가 이미 제공하는 기능과 중복됩니다
findUserById(Long)는JpaRepository#findById(Optional)와 기능상 유사합니다.deleteUserById(Long)는JpaRepository#deleteById와 중복됩니다.
QueryDSL이 필요한 특별한 사유(복잡한 조인/성능 최적화 등)가 없다면, 중복 메서드는 제거하고 서비스 레이어에서 Optional 처리 및 예외 변환을 권장합니다. 중복은 API 표면적을 넓히고 혼동을 유발합니다.
다음과 같이 인터페이스에서 제거하는 방향을 제안합니다(구현체에서도 함께 제거 필요):
public interface CustomUserRepository {
User findUserByEmail(String email);
- User findUserById(Long userId);
- void deleteUserById(Long userId);
}또는, 만약 실제 사용 중인지 확인이 필요하다면 다음 스크립트로 호출처를 점검해 주세요.
🏁 Script executed:
#!/bin/bash
# findUserById / deleteUserById 호출부 검색
rg -nP '\bfindUserById\s*\(' -C3
rg -nP '\bdeleteUserById\s*\(' -C3Length of output: 2886
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "---- CustomUserRepository.java ----"
sed -n '1,240p' src/main/java/com/opendata/domain/user/repository/custom/CustomUserRepository.java || true
echo
echo "---- CustomUserRepositoryImpl.java ----"
sed -n '1,240p' src/main/java/com/opendata/domain/user/repository/custom/CustomUserRepositoryImpl.java || true
echo
echo "---- 찾기: UserRepository 인터페이스(또는 JpaRepository 확장) ----"
rg -nP --hidden --no-ignore 'interface\s+\w*UserRepository\b' -S || true
echo
echo "---- 프로젝트 내 JpaRepository 확장 목록(간단히) ----"
rg -nP --hidden --no-ignore 'extends\s+JpaRepository<|extends\s+PagingAndSortingRepository<' -S || trueLength of output: 4825
CustomUserRepository의 중복된 메서드 제거 권고
JpaRepository(UserRepository가 JpaRepository<User, Long>를 확장)가 이미 findById/deleteById를 제공하고 있으며, 현재 Custom 구현은 QueryDSL로 단순 id 조회/삭제만 수행합니다. 전체 검색 결과 해당 메서드들의 선언/구현 외에 다른 호출처는 발견되지 않았습니다. 특별한 이유(예: 복잡한 조인, 성능 튜닝, Optional 미사용 의도)가 없다면 인터페이스와 구현체에서 제거하세요. 서비스 레이어에서 Optional 처리 및 예외 변환을 권장합니다.
수정 대상:
- src/main/java/com/opendata/domain/user/repository/custom/CustomUserRepository.java
- src/main/java/com/opendata/domain/user/repository/custom/CustomUserRepositoryImpl.java
제안 diff:
public interface CustomUserRepository {
User findUserByEmail(String email);
- User findUserById(Long userId);
- void deleteUserById(Long userId);
} public class CustomUserRepositoryImpl implements CustomUserRepository{
private final JPAQueryFactory queryFactory;
@Override
public User findUserByEmail(String email) {
QUser u = QUser.user;
return queryFactory
.selectFrom(u)
.where(u.email.eq(email))
.fetchFirst();
}
-
- @Override
- public User findUserById(Long userId) {
- QUser u = QUser.user;
- return queryFactory
- .selectFrom(u)
- .where(u.id.eq(userId))
- .fetchFirst();
- }
-
- @Override
- public void deleteUserById(Long userId) {
- QUser u = QUser.user;
- queryFactory
- .delete(u)
- .where(u.id.eq(userId))
- .execute();
- }
}서비스에서 Optional 반환을 원치 않아 메서드를 유지하려면 의도를 명시해 주세요.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| User findUserById(Long userId); | |
| void deleteUserById(Long userId); | |
| public interface CustomUserRepository { | |
| User findUserByEmail(String email); | |
| } |
| User findUserById(Long userId); | |
| void deleteUserById(Long userId); | |
| public class CustomUserRepositoryImpl implements CustomUserRepository{ | |
| private final JPAQueryFactory queryFactory; | |
| @Override | |
| public User findUserByEmail(String email) { | |
| QUser u = QUser.user; | |
| return queryFactory | |
| .selectFrom(u) | |
| .where(u.email.eq(email)) | |
| .fetchFirst(); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/com/opendata/domain/user/repository/custom/CustomUserRepository.java
around lines 8-9 and the corresponding
src/main/java/com/opendata/domain/user/repository/custom/CustomUserRepositoryImpl.java,
remove the duplicate method declarations and implementations for
findUserById(Long) and deleteUserById(Long) because JpaRepository already
provides findById/deleteById; update any callers in service layer to use
repository.findById(id) and handle the returned Optional (or convert to
exception there), and delete the QueryDSL-based implementations from the Impl
class; if you intentionally want non-Optional semantics, instead of duplicating
methods keep them but rename (e.g., getUserByIdOrThrow) and implement explicit
behavior with Javadoc describing the intent.
| @Override | ||
| public void deleteUserById(Long userId) { | ||
| QUser u = QUser.user; | ||
| queryFactory | ||
| .delete(u) | ||
| .where(u.id.eq(userId)) | ||
| .execute(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
삭제 쿼리에 트랜잭션 명시 필요
- QueryDSL delete는 트랜잭션 경계 내에서 실행되어야 합니다. 현재 메서드에 @transactional이 없어 호출 컨텍스트에 따라 실패할 수 있습니다.
권장 수정(예시):
import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
...
@Override
- public void deleteUserById(Long userId) {
+ @Transactional
+ public void deleteUserById(Long userId) {
QUser u = QUser.user;
queryFactory
.delete(u)
.where(u.id.eq(userId))
.execute();
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @Override | |
| public void deleteUserById(Long userId) { | |
| QUser u = QUser.user; | |
| queryFactory | |
| .delete(u) | |
| .where(u.id.eq(userId)) | |
| .execute(); | |
| } | |
| import org.springframework.stereotype.Repository; | |
| import org.springframework.transaction.annotation.Transactional; | |
| @Override | |
| @Transactional | |
| public void deleteUserById(Long userId) { | |
| QUser u = QUser.user; | |
| queryFactory | |
| .delete(u) | |
| .where(u.id.eq(userId)) | |
| .execute(); | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/com/opendata/domain/user/repository/custom/CustomUserRepositoryImpl.java
around lines 33 to 40, the QueryDSL delete is executed outside an explicit
transaction which can fail depending on the caller; annotate the method with
Spring's @Transactional (or annotate the class) to ensure a transactional
boundary, import org.springframework.transaction.annotation.Transactional, and
ensure the transaction is writable (i.e., no readOnly=true) so the delete
executes reliably.
| return new OpenAPI() | ||
| .info(info); | ||
| .addServersItem(new Server().url("/")) // 리버스 프록시 등일 때 베이스 경로 | ||
| .components(components) | ||
| .addSecurityItem(securityRequirement) | ||
| .info(apiInfo()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Server URL("/") 전역 설정은 배포 환경에 따라 경로 해석에 영향 — 프로필별 검증 권장
리버스 프록시/게이트웨이/서브패스 배포 시 Swagger UI의 "Try it out"가 잘못된 베이스 경로를 조합할 수 있습니다. 환경별 프로필에서 server URL을 분기하거나 제거하는 방안을 검토해 주세요.
실제 배포 환경에서 경로가 올바르게 계산되는지 확인하는 간단한 점검 스크립트입니다(문자열 체크 수준):
🏁 Script executed:
#!/bin/bash
# openapi.yaml/json 산출물을 찾아 servers 섹션과 url 값을 살펴봅니다.
fd -t f -a --hidden 'openapi.*\.(ya?ml|json)$' | while read -r f; do
echo "== $f =="
rg -n -A2 -B2 -i 'servers:|\"servers\"|url:|\"url\"' "$f"
doneLength of output: 60
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "추가 검증: Swagger/OpenAPI 관련 코드 및 설정(searching for OpenAPI/Swagger usages)..."
rg -n --hidden -S -g '!**/target/**' -g '!**/.git/**' \
'addServersItem\(|new\s+Server\(\)\.url\(|OpenAPI\(|class\s+SwaggerConfig\b|springdoc|v3/api-docs|swagger-ui|servers:' -C3 || true
# SwaggerConfig 파일이 있으면 내용 출력
files=$(rg -n --hidden -S -g '!**/target/**' -g '!**/.git/**' 'class\s+SwaggerConfig\b' -l || true)
if [ -n "$files" ]; then
echo
for f in $files; do
echo "== $f =="
nl -ba "$f" | sed -n '1,240p'
echo
done
fi
# 리포지토리 내 정적 OpenAPI 산출물 검색
echo "== searching for openapi.*.(yml|yaml|json) artifacts =="
rg -n --hidden -S -g '!**/target/**' -g '!**/.git/**' 'openapi.*\.(ya?ml|json)$' || true
# application.*(yml|properties)에서 springdoc 또는 server 관련 설정 검색
echo "== searching application.* for springdoc/server settings =="
rg -n --hidden -S -g '!**/target/**' -g '!**/.git/**' 'springdoc|server\.servlet\.context-path|server\.forward-headers-strategy|server\.base-path|spring\.web\.mvc\.servlet\.path' -C2 || trueLength of output: 3973
Server URL("/") 전역 설정 — 프로필/설정 분기 또는 제거 필요
SwaggerConfig에서 new Server().url("/")가 하드코딩되어 있어 리버스 프록시/게이트웨이/서브패스 배포 시 Swagger UI(특히 "Try it out")의 요청 URL이 잘못 계산될 수 있습니다. 검증 결과와 권고는 아래와 같습니다.
-
확인된 위치
- src/main/java/com/opendata/global/config/SwaggerConfig.java (약 라인 33–37)
return new OpenAPI() .addServersItem(new Server().url("/")) // 리버스 프록시 등일 때 베이스 경로 .components(components) .addSecurityItem(securityRequirement) .info(apiInfo()); - src/main/java/com/opendata/global/config/SecurityConfig.java (약 라인 94–95) — Swagger UI 및 /v3/api-docs 경로는 permitAll로 접근 허용되어 있음.
- 리포지토리에서 정적 openapi.*.(yml|yaml|json) 산출물 또는 application.*에서의 springdoc/server 관련 설정은 검색 결과로 발견되지 않았습니다.
- src/main/java/com/opendata/global/config/SwaggerConfig.java (약 라인 33–37)
-
권장 조치 (우선순위)
- 가장 간단: .addServersItem(new Server().url("/")) 제거(또는 주석 처리) — 다양한 배포 환경에서 잘못된 기본경로 적용 방지.
- 권장: 프로퍼티/프로파일로 분기 — 배포 환경별로 server URL을 주입하도록 변경.
예시:@Value("${openapi.server-url:}") private String openapiServerUrl; public OpenAPI openAPI() { OpenAPI openAPI = new OpenAPI(); if (!openapiServerUrl.isBlank()) { openAPI.addServersItem(new Server().url(openapiServerUrl)); } return openAPI .components(components) .addSecurityItem(securityRequirement) .info(apiInfo()); }
- (옵션) CI/배포 문서에 각 환경의 base path 처리 방식 명시.
위 사항을 반영해 코드/설정(또는 프로파일)을 분기하거나 서버 URL을 제거해 주세요.
🤖 Prompt for AI Agents
In src/main/java/com/opendata/global/config/SwaggerConfig.java around lines
33–37, remove the hardcoded .addServersItem(new Server().url("/")) or replace it
with a property-driven check: add a String field injected from a property (e.g.
openapi.server-url) and only call addServersItem when that value is non-blank;
update application.properties/application-<profile>.properties with
environment-specific server URLs as needed (leave property empty by default), so
Swagger UI won’t assume a root base path in reverse-proxy/subpath deployments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🔭 Outside diff range comments (1)
src/main/java/com/opendata/domain/tourspot/repository/custom/monthlyCongestion/CustomMonthlyCongestionRepositoryImpl.java (1)
16-19: 구현 누락: updateCongestionLevel이 항상 1L을 반환합니다현재 메서드가 고정값을 반환하여 실제 업데이트가 수행되지 않습니다. 호출부는 업데이트된 행 수를 기대할 가능성이 높아 기능 오작동으로 이어질 수 있습니다. QueryDSL 업데이트 구현과 트랜잭션 보장을 추가해 주세요.
아래 diff로 메서드 본문을 구현하세요(선택적으로 @transactional 추가):
- public long updateCongestionLevel(List<TourSpotMonthlyCongestion> monthlyCongestions) { - return 1L; - } + @Transactional + public long updateCongestionLevel(List<TourSpotMonthlyCongestion> monthlyCongestions) { + if (monthlyCongestions == null || monthlyCongestions.isEmpty()) { + return 0L; + } + long affected = 0L; + for (TourSpotMonthlyCongestion mc : monthlyCongestions) { + if (mc.getMonthlyCongestionId() == null || mc.getCongestionLvl() == null) { + continue; + } + affected += queryFactory + .update(tourSpotMonthlyCongestion) + .set(tourSpotMonthlyCongestion.congestionLvl, mc.getCongestionLvl()) + .where(tourSpotMonthlyCongestion.monthlyCongestionId.eq(mc.getMonthlyCongestionId())) + .execute(); + } + return affected; + }메서드 외부에 다음 import를 추가하세요:
import org.springframework.transaction.annotation.Transactional; import static com.opendata.domain.tourspot.entity.QTourSpotMonthlyCongestion.tourSpotMonthlyCongestion;참고:
- 대량 업데이트(벌크 업데이트)를 사용하므로 동일 영속성 컨텍스트에 로드된 엔티티가 있다면, 이후 읽기 전에 clear/refresh를 고려하세요(서비스 계층에서 처리 권장).
🧹 Nitpick comments (1)
src/main/java/com/opendata/domain/tourspot/repository/custom/monthlyCongestion/CustomMonthlyCongestionRepositoryImpl.java (1)
1-1: 패키지 세그먼트에 카멜케이스 사용은 비권장입니다Java 패키지명은 관례적으로 전부 소문자를 사용합니다.
custom.monthlyCongestion→custom.monthlycongestion또는custom.monthly등으로 통일을 검토해 주세요. 팀 컨벤션과도 일치하는지 확인 바랍니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
src/main/java/com/opendata/domain/tourspot/repository/MonthlyCongestionRepository.java(1 hunks)src/main/java/com/opendata/domain/tourspot/repository/custom/monthlyCongestion/CustomMonthlyCongestionRepository.java(1 hunks)src/main/java/com/opendata/domain/tourspot/repository/custom/monthlyCongestion/CustomMonthlyCongestionRepositoryImpl.java(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/com/opendata/domain/tourspot/repository/MonthlyCongestionRepository.java
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/main/java/com/opendata/domain/tourspot/repository/custom/monthlyCongestion/CustomMonthlyCongestionRepository.java (2)
src/main/java/com/opendata/domain/tourspot/repository/custom/CustomMonthlyCongestionRepositoryImpl.java (2)
CustomMonthlyCongestionRepositoryImpl(10-20)updateCongestionLevel(16-19)src/main/java/com/opendata/domain/tourspot/repository/custom/CustomMonthlyCongestionRepository.java (2)
CustomMonthlyCongestionRepository(7-10)updateCongestionLevel(9-9)
src/main/java/com/opendata/domain/tourspot/repository/custom/monthlyCongestion/CustomMonthlyCongestionRepositoryImpl.java (5)
src/main/java/com/opendata/domain/tourspot/repository/custom/CustomMonthlyCongestionRepositoryImpl.java (2)
CustomMonthlyCongestionRepositoryImpl(10-20)updateCongestionLevel(16-19)src/main/java/com/opendata/domain/tourspot/repository/custom/CustomMonthlyCongestionRepository.java (2)
CustomMonthlyCongestionRepository(7-10)updateCongestionLevel(9-9)src/main/java/com/opendata/domain/tourspot/repository/custom/CustomFutureCongestionRepositoryImpl.java (1)
CustomFutureCongestionRepositoryImpl(12-39)src/main/java/com/opendata/domain/tourspot/entity/TourSpotMonthlyCongestion.java (1)
TourSpotMonthlyCongestion(8-24)src/main/java/com/opendata/domain/tourspot/repository/custom/CustomFutureCongestionRepository.java (1)
CustomFutureCongestionRepository(9-12)
🔇 Additional comments (2)
src/main/java/com/opendata/domain/tourspot/repository/custom/monthlyCongestion/CustomMonthlyCongestionRepositoryImpl.java (1)
1-12: 패키지 이동 검증 완료 — 레거시 참조/중복 없음검색 결과: CustomMonthlyCongestionRepository는 새 경로에만 존재하며(아래 참조), 기존 패키지 경로를 가리키는 import는 발견되지 않았습니다. 중복 정의도 없습니다.
- src/main/java/com/opendata/domain/tourspot/repository/custom/monthlyCongestion/CustomMonthlyCongestionRepository.java
- src/main/java/com/opendata/domain/tourspot/repository/custom/monthlyCongestion/CustomMonthlyCongestionRepositoryImpl.java
문제 없음.
src/main/java/com/opendata/domain/tourspot/repository/custom/monthlyCongestion/CustomMonthlyCongestionRepository.java (1)
1-10: 패키지 이동과 인터페이스 시그니처 유지, 문제 없습니다마이그레이션된 패키지 경로와 시그니처가 일관적입니다. 구현체와의 네이밍 규약(Spring Data JPA 커스텀 레포지토리 바인딩)도 적합해 보입니다.
📌 PR 개요
✅ 변경사항
🔍 체크리스트
📎 관련 이슈
Closes #50
💬 기타 참고사항
Summary by CodeRabbit
새로운 기능
버그 수정
문서