-
Notifications
You must be signed in to change notification settings - Fork 0
[feat] 코스 상세 정보 반환 #54
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
Conversation
Walkthrough코스 상세 조회 API를 추가하고, 기존 코스 사양(spec) 엔드포인트와 DTO를 제거했습니다. 코스 엔티티에 uuid 필드를 도입하고, uuid 기반 조회 리포지토리 메서드를 추가했습니다. 서비스는 Redis 선조회 후 DB 조회 흐름으로 CourseResponse를 생성합니다. DateUtil 파싱 로직과 애플리케이션 비밀번호 환경변수를 수정했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Controller as CourseController
participant Service as CourseService
participant Redis as RedisCache
participant Repo as CourseRepository
participant Ext as Congestion API
Client->>Controller: GET /course/{courseId}
Controller->>Service: fetchCourseDetail(courseId)
Service->>Redis: GET tempCourse:{courseId}
alt Cache hit
Redis-->>Service: List<CourseComponentDto>
Service-->>Controller: CourseResponse(from cache)
else Cache miss
Service->>Repo: findByUuid(courseId)
Repo-->>Service: Course
loop for each component
Service->>Ext: get congestion forecast
Ext-->>Service: CongestionLevel
Service->>Service: map to CourseComponentDto
end
Service-->>Controller: CourseResponse(from DB)
end
Controller-->>Client: 200 ApiResponse<CourseResponse>
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 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: 2
🔭 Outside diff range comments (2)
src/main/resources/application.yml (2)
38-42: JWT 시크릿 하드코딩은 보안 사고로 직결됩니다 — 환경변수로 전환하세요레포지토리에 비밀키가 노출되어 있습니다. 최소 prod에서는 반드시 환경변수로 분리하고, 이 커밋 이전 히스토리를 포함해 키 교체(폐기)도 권장합니다.
다음과 같이 환경변수로 치환하는 것을 제안합니다:
jwt: issuer: [email protected] - secret: "jfsgkhdsjkweoiruwqejkrfbhnsadkjlfghbwerqewrsdfgdsfagbdf" + secret: ${JWT_SECRET} access-expiration: 3600 refresh-expiration: 8640
63-66: 외부 API 키 하드코딩 제거 필요API 키가 코드에 남아 있습니다. 키 노출은 회수/재발급이 필요한 사고입니다. 환경변수로 치환하세요.
api: seoul_city_data_key: ${SEOUL_CITY_DATA_KEY} tour_api_tourspot_key: ${TOUR_API_TOURSPOT_KEY} - tour_api_congestion_key: B4FraEdNAEHerMG6ZQUi5OXCzio%2FQJ4IRx9rOOz7%2BeiPBh4L8pX4XAygutNaYnOoL%2BD8vS%2F3qZ53efN6daHZ%2Fg%3D%3D - zz: wabF%2F2ep3dqrmXQcTNupVTpQXrL3wBuOK1TSRhAo8mwZ9Wqop4GCNUXkqSdf4%2Bwxa1%2FdOCkfmbBXYQL13wlwKQ%3D%3D - xx: J28I%2B2X%2FLQ8vHJaw2Yorr492RFOq2%2FRBGCkl1hVMJCu65fNG5xDvDi7GazJpx3ZKa9xb4fYhq14vtXjWSaHrSw%3D%3D + tour_api_congestion_key: ${TOUR_API_CONGESTION_KEY} + zz: ${API_ZZ} + xx: ${API_XX}
🧹 Nitpick comments (8)
src/main/resources/application.yml (1)
56-59: 프로덕션 로그 레벨 점검 권장Hibernate SQL/파라미터 로그가 전역으로 debug/trace입니다. 운영 환경에서는 성능/보안(민감 파라미터 노출) 이슈가 있습니다. profile(local)로 한정하거나 prod에서는 info 이하로 낮추는 것을 권장합니다.
src/main/java/com/opendata/domain/course/entity/Course.java (1)
24-25: uuid 칼럼에 유니크 제약 추가 권장
findByUuid조회가 추가되었으므로, 데이터 정합성과 성능(인덱스)을 위해 유니크 제약(및 길이)을 명시하는 것이 좋습니다. 중복 UUID가 생기면 현재 구현(fetchFirst)은 비결정적 결과를 반환할 수 있습니다.아래와 같이 칼럼 제약을 추가하는 것을 제안합니다(기존 데이터에
uuid가 비어있을 수 있으므로 nullable은 상황에 맞게 결정):- private String uuid; + @Column(name = "uuid", length = 36, unique = true) + private String uuid;운영 DB에 제약이 생성되는지(DDL-auto 전략/마이그레이션) 확인 부탁드립니다. 필요 시 명시적인 마이그레이션 스크립트로 배포하는 것이 안전합니다.
src/main/java/com/opendata/global/util/DateUtil.java (1)
35-40: 시간 파싱 로직을 포맷러로 일반화하면 견고성이 올라갑니다현재는 길이 비교와 문자 치환으로 분기합니다.
T또는 공백 구분자, 초 존재 유무를 모두 포괄하는 포맷터를 사용하면 예외 경우를 줄일 수 있습니다. null/blank 입력에 대한 방어도 추가해 주세요.아래와 같이 개선을 제안합니다(추가 import 없이 FQCN 사용):
- String normalized = time.replace("T", " "); - if (normalized.length() == 16) { // yyyy-MM-dd HH:mm - normalized += ":00"; - } - return LocalDateTime.parse(normalized, FORMATTER); + if (time == null || time.isBlank()) { + throw new IllegalArgumentException("time must not be null/blank"); + } + String normalized = time.trim(); + java.time.format.DateTimeFormatter flexible = + new java.time.format.DateTimeFormatterBuilder() + .parseCaseInsensitive() + .appendPattern("yyyy-MM-dd") + .optionalStart().appendLiteral('T').optionalEnd() + .optionalStart().appendLiteral(' ').optionalEnd() + .appendPattern("HH:mm") + .optionalStart().appendLiteral(':').appendPattern("ss").optionalEnd() + .toFormatter(); + return LocalDateTime.parse(normalized, flexible);참고: 타임존이 포함된 입력(e.g.
Z,+09:00)을 지원해야 한다면OffsetDateTime파싱으로 별도 분기하는 것을 권장합니다.src/main/java/com/opendata/domain/course/repository/custom/CustomCourseRepository.java (1)
7-12: 파라미터 null 계약을 명시하면 사용성이 좋아집니다
uuid에 대한 null/blank 허용 여부를 인터페이스 차원에서 명확히 해두면 호출부 오류를 줄일 수 있습니다. 스프링 표준 어노테이션으로 명시를 제안합니다.import java.util.List; import java.util.Optional; +import org.springframework.lang.NonNull; public interface CustomCourseRepository { List<Course> findAllByUserId(Long userId); - Optional<Course> findByUuid(String uuid); + Optional<Course> findByUuid(@NonNull String uuid); }src/main/java/com/opendata/domain/course/repository/custom/CustomCourseRepositoryImpl.java (1)
35-43: findByUuid 메서드: Null/Blank 검증 및 fetchOne 사용, fetchCourseDetail 시 연관 엔티티 즉시 로딩 검토
findByUuid(String uuid)에서 입력값이null또는 공백일 경우 빠르게Optional.empty()를 반환해 불필요한 쿼리 실행 방지- UUID가 유니크하다면
fetchOne()을 사용해 중복 발생 시 예외를 던져 조기에 이상 징후 탐지fetchCourseDetail내부에서course.getCourseComponents()를 바로 순회하므로, 트랜잭션 범위 바깥에서 호출될 경우LazyInitializationException또는 N+1 문제가 발생할 수 있음
- 서비스 메서드에
@Transactional(readOnly = true)추가- 또는 CustomCourseRepositoryImpl.findByUuid에
fetchJoin()으로 연관 엔티티 미리 로딩예시 변경안:
@Override public Optional<Course> findByUuid(String uuid) { - QCourse qCourse = QCourse.course; - - return Optional.ofNullable(queryFactory.selectFrom(qCourse) - .where(qCourse.uuid.eq(uuid)) - .fetchFirst()); + if (uuid == null || uuid.isBlank()) { + return Optional.empty(); + } + QCourse c = QCourse.course; + return Optional.ofNullable( + queryFactory.selectFrom(c) + .where(c.uuid.eq(uuid)) + .fetchOne() + ); }또는 상세 조회용 메서드에 아래와 같이 fetch join 적용:
return Optional.ofNullable( queryFactory.selectFrom(c) .leftJoin(c.courseComponents, cc).fetchJoin() .leftJoin(cc.tourSpot, ts).fetchJoin() .where(c.uuid.eq(uuid)) .fetchOne() );src/main/java/com/opendata/docs/CourseControllerDocs.java (1)
71-93: 500 응답 스펙도 문서화해 주세요 (다른 문서와의 일관성).TourSpot 상세 문서처럼 500 응답 예시를 추가하면 에러 응답 포맷 일관성이 좋아집니다.
아래와 같이 500 응답을 추가하는 것을 제안합니다:
@ApiResponses(value = { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "코스 상세 정보 조회 성공"), @io.swagger.v3.oas.annotations.responses.ApiResponse( responseCode = "404", description = "코스 정보 없음", content = @Content( mediaType = "application/json", examples = @ExampleObject( name = "코스 없음", summary = "존재하지 않는 코스 ID", value = """ { "success": false, "code": "COURSE_404", "message": "해당 코스를 찾을 수 없습니다." } """ ) ) ) + , + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "500", + description = "서버 에러", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + name = "서버 에러 응답", + summary = "예상치 못한 서버 에러", + value = """ + { + "success": false, + "code": "COMMON_500", + "message": "서버 에러, 관리자에게 문의 바랍니다." + } + """ + ) + ) + ) })src/main/java/com/opendata/domain/course/service/CourseService.java (2)
249-251: ObjectMapper 매번 생성 대신 빈 주입 사용 권장.서비스 전역에서 동일 직렬화 정책을 유지하도록 빈 주입(ObjectMapper)을 사용하세요. 성능과 유지보수에 유리합니다.
적용 예(클래스 상단 필드 추가):
private final ObjectMapper objectMapper;그리고 메서드 내
- ObjectMapper objectMapper = new ObjectMapper();삭제 후 주입된 필드를 사용해 주세요.
likeCourse(...)의 동일 패턴도 함께 정리 권장.
249-276: 코스 ID 표현 일관성 강화Redis-Key, 외부 API, DB UUID가 혼용되어 있어 사용성과 유지보수에 혼란을 줄 수 있습니다. 아래 세 위치를 중심으로 “외부 API ↔ Redis ↔ DB” 간 코스 ID를 일관되게 관리할 것을 권장드립니다.
주요 위치
- src/main/java/com/opendata/domain/course/service/CourseService.java:97–99
- src/main/java/com/opendata/domain/course/service/CourseService.java:218–219
- src/main/java/com/opendata/domain/course/service/CourseService.java:252–253
제안 리팩터링 예시
- 추천코스 생성부 (recommendCourses)
- String tempCourseId = "tempCourse:" + UUID.randomUUID(); - courseResponses.add(courseResponseMapper.toResponse(tempCourseId, singleCourse)); - redisTemplate.opsForValue().set(tempCourseId, singleCourse, Duration.ofMinutes(30)); + // ① 외부에는 UUID만 노출 + String uuid = UUID.randomUUID().toString(); + String redisKey = "tempCourse:" + uuid; + courseResponses.add(courseResponseMapper.toResponse(uuid, singleCourse)); + redisTemplate.opsForValue().set(redisKey, singleCourse, Duration.ofMinutes(30));
- 조회부 (likeCourse, fetchCourseDetail 등)
final String PREFIX = "tempCourse:"; String normalized = courseId.startsWith(PREFIX) ? courseId.substring(PREFIX.length()) : courseId; String redisKey = PREFIX + normalized; List<?> rawList = (List<?>) redisTemplate.opsForValue().get(redisKey);이렇게 처리하면
- 외부 API 요청/응답에서는 순수 UUID만 주고받고
- Redis 키 조립·조회 로직은 항상 PREFIX+UUID 형태로 일관화
- DB 조회도 UUID 기반으로 통일
코드 반영 후, CourseResponseMapper.toResponse(UUID) 시그니처 변경 여부도 함께 검토해 주세요.
📜 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 (9)
src/main/java/com/opendata/docs/CourseControllerDocs.java(2 hunks)src/main/java/com/opendata/domain/course/controller/CourseController.java(3 hunks)src/main/java/com/opendata/domain/course/dto/response/CourseSpecResponse.java(0 hunks)src/main/java/com/opendata/domain/course/entity/Course.java(1 hunks)src/main/java/com/opendata/domain/course/repository/custom/CustomCourseRepository.java(1 hunks)src/main/java/com/opendata/domain/course/repository/custom/CustomCourseRepositoryImpl.java(2 hunks)src/main/java/com/opendata/domain/course/service/CourseService.java(2 hunks)src/main/java/com/opendata/global/util/DateUtil.java(1 hunks)src/main/resources/application.yml(1 hunks)
💤 Files with no reviewable changes (1)
- src/main/java/com/opendata/domain/course/dto/response/CourseSpecResponse.java
🧰 Additional context used
🧬 Code Graph Analysis (6)
src/main/java/com/opendata/domain/course/repository/custom/CustomCourseRepositoryImpl.java (3)
src/main/java/com/opendata/domain/course/repository/custom/CustomCourseComponentRepositoryImpl.java (2)
RequiredArgsConstructor(12-27)Override(15-26)src/main/java/com/opendata/domain/course/repository/CourseRepository.java (1)
CourseRepository(12-15)src/main/java/com/opendata/domain/course/repository/custom/CustomCourseComponentRepository.java (2)
CustomCourseComponentRepository(7-10)findAllByCourseId(9-9)
src/main/java/com/opendata/domain/course/repository/custom/CustomCourseRepository.java (1)
src/main/java/com/opendata/domain/course/repository/CourseRepository.java (1)
CourseRepository(12-15)
src/main/java/com/opendata/domain/course/entity/Course.java (4)
src/main/java/com/opendata/domain/course/mapper/CourseMapper.java (2)
toEntity(16-16)Mapper(10-17)src/main/java/com/opendata/domain/course/dto/response/CourseComponentHistoryDto.java (1)
CourseComponentHistoryDto(5-13)src/main/java/com/opendata/domain/course/repository/CourseRepository.java (1)
CourseRepository(12-15)src/main/java/com/opendata/domain/course/dto/response/CourseResponse.java (1)
CourseResponse(5-8)
src/main/java/com/opendata/domain/course/service/CourseService.java (2)
src/main/java/com/opendata/domain/course/dto/response/CourseComponentHistoryDto.java (1)
CourseComponentHistoryDto(5-13)src/main/java/com/opendata/domain/course/dto/response/CourseComponentDto.java (1)
CourseComponentDto(12-23)
src/main/java/com/opendata/domain/course/controller/CourseController.java (3)
src/main/java/com/opendata/domain/mypage/service/MypageService.java (1)
course(59-68)src/main/java/com/opendata/docs/MyPageControllerDocs.java (1)
Operation(22-64)src/main/java/com/opendata/domain/mypage/controller/MypageController.java (1)
RestController(18-65)
src/main/java/com/opendata/docs/CourseControllerDocs.java (2)
src/main/java/com/opendata/domain/course/dto/response/CourseResponse.java (1)
CourseResponse(5-8)src/main/java/com/opendata/docs/TourSpotControllerDocs.java (1)
Operation(42-82)
🔇 Additional comments (4)
src/main/resources/application.yml (1)
75-75: 로컬 DB 비밀번호 변수 정합성 확인 필요prod와 동일하게
${MYSQL_PASSWORD}로 통일된 점은 좋습니다. 로컬 실행 환경(IDE, docker-compose,.env)에MYSQL_PASSWORD가 실제로 주입되는지 확인 부탁드립니다. 기존에MYSQL_USER로 오해하고 설정해둔 케이스가 있다면 로컬 부팅 실패 가능성이 있습니다.src/main/java/com/opendata/domain/course/repository/custom/CustomCourseRepositoryImpl.java (1)
12-12: Optional import 추가는 타당합니다
findByUuid가Optional을 반환하므로 해당 import 추가는 적절합니다.src/main/java/com/opendata/docs/CourseControllerDocs.java (1)
93-93: courseId 형식(임시/영구) 명시 검토 요청.현재 API는 Redis 임시 코스와 DB 영구 코스를 모두 지원합니다. path 변수 courseId가 "UUID만"인지, "tempCourse:UUID" 같은 prefix를 포함하는지 명확히 문서에 표기되면 통신 오류를 줄일 수 있습니다. 서비스 구현과 합의된 포맷을 문서에 반영해 주세요.
src/main/java/com/opendata/domain/course/controller/CourseController.java (1)
38-41: 신규 상세 조회 엔드포인트 위임 로직 깔끔합니다.서비스 위임과 ApiResponse 래핑이 기존 컨벤션과 일치합니다.
| public CourseResponse fetchCourseDetail(String courseId){ | ||
| ObjectMapper objectMapper = new ObjectMapper(); | ||
| DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); | ||
| List<?> rawList = (List<?>) redisTemplate.opsForValue().get("tempCourse:" + courseId); | ||
| if (rawList == null || rawList.isEmpty()){ | ||
| List<CourseComponentDto> courseComponentDtoList = new ArrayList<>(); | ||
| Course course = courseRepository.findByUuid(courseId).orElseThrow(); | ||
| course.getCourseComponents().forEach( | ||
| courseComponent -> { | ||
| Long spotId = courseComponent.getTourSpot().getTourspotId(); | ||
| LocalDateTime time = courseComponent.getTourspotTm(); | ||
|
|
||
| Optional<TourSpotFutureCongestion> congestion = | ||
| futureCongestionRepository.findByTourSpotIdAndFcstTime(spotId, time.format(formatter)); | ||
|
|
||
| CongestionLevel level = congestion.get().getCongestionLvl(); | ||
| courseComponentDtoList.add(CourseComponentDto.from(courseComponent, level)); | ||
|
|
||
| } | ||
| ); | ||
| return new CourseResponse(courseId, courseComponentDtoList); | ||
| } | ||
| List<CourseComponentDto> tempCourse = objectMapper.convertValue( | ||
| rawList, | ||
| new TypeReference<List<CourseComponentDto>>() {} | ||
| ); | ||
| return new CourseResponse(courseId, tempCourse); | ||
| } |
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.
Redis 키 prefix 중복 가능성(tempCourse:tempCourse:...) → 캐시 미스 및 조회 실패 유발.
추천 응답에서 반환하는 ID에 이미 "tempCourse:" prefix가 포함될 여지가 있는 반면, 여기서는 다시 "tempCourse:"를 덧붙여 조회합니다. 이 경우 실제 Redis 키와 불일치하여 캐시 미스가 발생할 수 있습니다. 또한 캐시 미스 후 DB 조회 시에도 prefix가 포함된 ID를 그대로 사용하면 영구 코스 UUID 조회가 실패할 수 있습니다.
Minimal-impact 수정: 전달받은 courseId가 prefix를 포함해도/포함하지 않아도 동작하도록 키와 조회용 ID를 분리하세요.
- public CourseResponse fetchCourseDetail(String courseId){
- ObjectMapper objectMapper = new ObjectMapper();
- DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
- List<?> rawList = (List<?>) redisTemplate.opsForValue().get("tempCourse:" + courseId);
+ public CourseResponse fetchCourseDetail(String courseId){
+ ObjectMapper objectMapper = new ObjectMapper();
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
+ final String PREFIX = "tempCourse:";
+ final boolean hasPrefix = courseId != null && courseId.startsWith(PREFIX);
+ final String redisKey = hasPrefix ? courseId : PREFIX + courseId;
+ final String normalizedId = hasPrefix ? courseId.substring(PREFIX.length()) : courseId;
+ List<?> rawList = (List<?>) redisTemplate.opsForValue().get(redisKey);
if (rawList == null || rawList.isEmpty()){
List<CourseComponentDto> courseComponentDtoList = new ArrayList<>();
- Course course = courseRepository.findByUuid(courseId).orElseThrow();
+ Course course = courseRepository.findByUuid(normalizedId)
+ .orElseThrow(() -> new CourseNotFoundException(CourseMessages.COURSE_NOT_FOUND));
course.getCourseComponents().forEach(
courseComponent -> {
Long spotId = courseComponent.getTourSpot().getTourspotId();
LocalDateTime time = courseComponent.getTourspotTm();
- Optional<TourSpotFutureCongestion> congestion =
- futureCongestionRepository.findByTourSpotIdAndFcstTime(spotId, time.format(formatter));
-
- CongestionLevel level = congestion.get().getCongestionLvl();
+ CongestionLevel level = futureCongestionRepository
+ .findByTourSpotIdAndFcstTime(spotId, time.format(formatter))
+ .map(TourSpotFutureCongestion::getCongestionLvl)
+ .orElseThrow(() -> new IllegalStateException("Future congestion not found: spotId="
+ + spotId + ", time=" + time.format(formatter)));
courseComponentDtoList.add(CourseComponentDto.from(courseComponent, level));
}
);
- return new CourseResponse(courseId, courseComponentDtoList);
+ return new CourseResponse(normalizedId, courseComponentDtoList);
}
List<CourseComponentDto> tempCourse = objectMapper.convertValue(
rawList,
new TypeReference<List<CourseComponentDto>>() {}
);
- return new CourseResponse(courseId, tempCourse);
+ return new CourseResponse(normalizedId, tempCourse);
}추가 권장(일관성 확립):
- recommendCourses에서 응답에 담는 courseId는 "UUID만"을 사용하고, Redis 키만 "tempCourse:" + UUID로 관리하면 혼선을 근본적으로 제거할 수 있습니다. 아래 별도 코멘트에 보강 제안을 첨부합니다.
- 메시지 상수(CourseMessages.COURSE_NOT_FOUND)는 실제 사용중인 상수명에 맞춰 조정해 주세요.
📝 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 CourseResponse fetchCourseDetail(String courseId){ | |
| ObjectMapper objectMapper = new ObjectMapper(); | |
| DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); | |
| List<?> rawList = (List<?>) redisTemplate.opsForValue().get("tempCourse:" + courseId); | |
| if (rawList == null || rawList.isEmpty()){ | |
| List<CourseComponentDto> courseComponentDtoList = new ArrayList<>(); | |
| Course course = courseRepository.findByUuid(courseId).orElseThrow(); | |
| course.getCourseComponents().forEach( | |
| courseComponent -> { | |
| Long spotId = courseComponent.getTourSpot().getTourspotId(); | |
| LocalDateTime time = courseComponent.getTourspotTm(); | |
| Optional<TourSpotFutureCongestion> congestion = | |
| futureCongestionRepository.findByTourSpotIdAndFcstTime(spotId, time.format(formatter)); | |
| CongestionLevel level = congestion.get().getCongestionLvl(); | |
| courseComponentDtoList.add(CourseComponentDto.from(courseComponent, level)); | |
| } | |
| ); | |
| return new CourseResponse(courseId, courseComponentDtoList); | |
| } | |
| List<CourseComponentDto> tempCourse = objectMapper.convertValue( | |
| rawList, | |
| new TypeReference<List<CourseComponentDto>>() {} | |
| ); | |
| return new CourseResponse(courseId, tempCourse); | |
| } | |
| public CourseResponse fetchCourseDetail(String courseId){ | |
| ObjectMapper objectMapper = new ObjectMapper(); | |
| DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); | |
| final String PREFIX = "tempCourse:"; | |
| final boolean hasPrefix = courseId != null && courseId.startsWith(PREFIX); | |
| final String redisKey = hasPrefix ? courseId : PREFIX + courseId; | |
| final String normalizedId = hasPrefix ? courseId.substring(PREFIX.length()) : courseId; | |
| List<?> rawList = (List<?>) redisTemplate.opsForValue().get(redisKey); | |
| if (rawList == null || rawList.isEmpty()){ | |
| List<CourseComponentDto> courseComponentDtoList = new ArrayList<>(); | |
| Course course = courseRepository.findByUuid(normalizedId) | |
| .orElseThrow(() -> new CourseNotFoundException(CourseMessages.COURSE_NOT_FOUND)); | |
| course.getCourseComponents().forEach( | |
| courseComponent -> { | |
| Long spotId = courseComponent.getTourSpot().getTourspotId(); | |
| LocalDateTime time = courseComponent.getTourspotTm(); | |
| CongestionLevel level = futureCongestionRepository | |
| .findByTourSpotIdAndFcstTime(spotId, time.format(formatter)) | |
| .map(TourSpotFutureCongestion::getCongestionLvl) | |
| .orElseThrow(() -> new IllegalStateException("Future congestion not found: spotId=" | |
| spotId + ", time=" + time.format(formatter))); | |
| courseComponentDtoList.add(CourseComponentDto.from(courseComponent, level)); | |
| } | |
| ); | |
| return new CourseResponse(normalizedId, courseComponentDtoList); | |
| } | |
| List<CourseComponentDto> tempCourse = objectMapper.convertValue( | |
| rawList, | |
| new TypeReference<List<CourseComponentDto>>() {} | |
| ); | |
| return new CourseResponse(normalizedId, tempCourse); | |
| } |
🤖 Prompt for AI Agents
In src/main/java/com/opendata/domain/course/service/CourseService.java around
lines 249-276, the method builds the Redis key by blindly prefixing
"tempCourse:" to the incoming courseId which may already include that prefix,
causing key duplication and DB lookup failures; fix by normalizing inputs: if
courseId startsWith "tempCourse:" remove that prefix to produce a pure UUID
variable (e.g., uuid), then use redisKey = "tempCourse:" + uuid for Redis
get/convert and use uuid (without prefix) for DB repository.findByUuid(...)
calls; also handle the Optional from futureCongestionRepository safely and
ensure error/exception messages use the correct message constant name.
| Optional<TourSpotFutureCongestion> congestion = | ||
| futureCongestionRepository.findByTourSpotIdAndFcstTime(spotId, time.format(formatter)); | ||
|
|
||
| CongestionLevel level = congestion.get().getCongestionLvl(); | ||
| courseComponentDtoList.add(CourseComponentDto.from(courseComponent, level)); | ||
|
|
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
Optional.get() 사용으로 인한 NoSuchElementException 위험.
혼잡도 조회가 실패할 경우 NSEE로 500이 발생합니다. 명시적으로 orElseThrow로 전환하거나 기본값을 정의하세요.
위 상위 코멘트의 diff에 포함된 형태처럼 처리하거나, 기본값이 가능하다면 다음과 같이도 가능합니다:
- CongestionLevel level = congestion.get().getCongestionLvl();
+ CongestionLevel level = congestion
+ .map(TourSpotFutureCongestion::getCongestionLvl)
+ .orElse(CongestionLevel.UNKNOWN); // 프로젝트 도메인에 맞는 기본값으로 교체📝 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.
| Optional<TourSpotFutureCongestion> congestion = | |
| futureCongestionRepository.findByTourSpotIdAndFcstTime(spotId, time.format(formatter)); | |
| CongestionLevel level = congestion.get().getCongestionLvl(); | |
| courseComponentDtoList.add(CourseComponentDto.from(courseComponent, level)); | |
| Optional<TourSpotFutureCongestion> congestion = | |
| futureCongestionRepository.findByTourSpotIdAndFcstTime(spotId, time.format(formatter)); | |
| CongestionLevel level = congestion | |
| .map(TourSpotFutureCongestion::getCongestionLvl) | |
| .orElse(CongestionLevel.UNKNOWN); // 프로젝트 도메인에 맞는 기본값으로 교체 | |
| courseComponentDtoList.add(CourseComponentDto.from(courseComponent, level)); |
🤖 Prompt for AI Agents
In src/main/java/com/opendata/domain/course/service/CourseService.java around
lines 261 to 266, avoid calling Optional.get() which can throw
NoSuchElementException; instead either use orElseThrow with a clear, specific
exception (e.g., EntityNotFoundException with a helpful message including
spotId/time) or supply a sensible default via orElse (and handle default
congestion level appropriately), then pass the resolved CongestionLevel into
CourseComponentDto.from; update any method signatures or error handling to
reflect the chosen approach.
📌 PR 개요
✅ 변경사항
🔍 체크리스트
📎 관련 이슈
Closes #53
💬 기타 참고사항
Summary by CodeRabbit