Skip to content

Conversation

@kamillcream
Copy link
Contributor

@kamillcream kamillcream commented Aug 4, 2025

📌 PR 개요

  • 생성된 코스 중 하나를 선택하여 DB에 어장

✅ 변경사항

  • Course, CourseComponent(TourSpot과 Course의 중간 엔티티) 연관 관계 활성화
  • Redis에서 저장된 객체를 DB에 저장

🔍 체크리스트

  • PR 제목은 명확한가요?
  • 관련 이슈가 있다면 연결했나요?
  • 로컬 테스트는 통과했나요?
  • 코드에 불필요한 부분은 없나요?

📎 관련 이슈

Closes #45


💬 기타 참고사항

Summary by CodeRabbit

  • 신규 기능

    • 코스 좋아요 등록 기능이 추가되어, 코스 ID를 이용해 좋아요를 등록할 수 있습니다.
    • CourseComponentMapper, CourseComponentRepository 등 코스 컴포넌트 관련 매퍼 및 저장소가 도입되었습니다.
  • 버그 수정

    • 날짜 및 시간 포맷이 "yyyy-MM-dd HH:mm:ss"로 통일되어 일관된 시간 정보 제공이 가능합니다.
  • 개선 및 변경

    • 코스 및 코스 컴포넌트 엔터티에 사용자 및 컴포넌트 연관관계가 명확히 추가되었습니다.
    • CourseComponentDto의 필드명이 직관적으로 변경되고, tourspotId 등 추가 정보가 제공됩니다.
    • Redis를 활용한 임시 코스 데이터 저장 및 불러오기가 개선되었습니다.
    • "/course/**" 경로에 대한 접근 권한이 확장되어 다양한 코스 관련 요청이 허용됩니다.
    • API 응답에 성공 시 결과가 없는 경우를 위한 응답 메서드가 추가되었습니다.
    • 날짜-시간 처리 유틸리티가 초 단위 포맷과 파싱 기능을 지원하도록 개선되었습니다.
    • Redis 설정에서 ObjectMapper가 JavaTimeModule을 명시적으로 등록하도록 변경되었습니다.
  • 문서화

    • Swagger/OpenAPI 문서에 코스 좋아요 등록 관련 엔드포인트가 활성화되었습니다.
  • 기타

    • 코스 저장 기한 만료 메시지가 추가되었습니다.

@kamillcream kamillcream linked an issue Aug 4, 2025 that may be closed by this pull request
2 tasks
@coderabbitai
Copy link

coderabbitai bot commented Aug 4, 2025

Walkthrough

코스 즐겨찾기(Like) 기능이 구현되었습니다. 코스와 코스 컴포넌트 엔티티, 매퍼, 레포지토리, 서비스 계층이 확장 및 추가되었고, Redis 캐시에서 임시 코스 데이터를 불러와 영속화하는 로직이 포함되었습니다. 날짜 포맷 일관성 및 JSON 직렬화 설정도 변경되었습니다.

Changes

Cohort / File(s) Change Summary
코스 즐겨찾기 API 활성화 및 엔드포인트 수정
src/main/java/com/opendata/docs/CourseControllerDocs.java, src/main/java/com/opendata/domain/course/controller/CourseController.java
코스 즐겨찾기(Like) API를 활성화하고, 파라미터를 PathVariable로 단순화, 반환 타입을 Void로 변경. Swagger 문서화도 활성화.
Course, CourseComponent 엔티티 확장
src/main/java/com/opendata/domain/course/entity/Course.java, src/main/java/com/opendata/domain/course/entity/CourseComponent.java, src/main/java/com/opendata/domain/user/entity/User.java
Course-User, Course-CourseComponent 관계 매핑 추가. CourseComponent에 course 필드 및 할당 메서드 추가. User에 courseList 필드 추가.
DTO 및 매퍼 리팩토링/신규 추가
src/main/java/com/opendata/domain/course/dto/response/CourseComponentDto.java, src/main/java/com/opendata/domain/course/mapper/CourseComponentMapper.java, src/main/java/com/opendata/domain/course/mapper/CourseMapper.java
CourseComponentDto 필드 및 타입 변경, 매퍼 분리 및 신설(CourseComponentMapper), CourseMapper 간소화.
레포지토리 신설
src/main/java/com/opendata/domain/course/repository/CourseComponentRepository.java
CourseComponent 전용 JPA 레포지토리 신규 생성.
코스 서비스 기능 확장
src/main/java/com/opendata/domain/course/service/CourseService.java
likeCourse(String courseId) 메서드 신설: Redis에서 임시 코스 불러와 Course 및 CourseComponent 영속화, 기존 관련 코드 정리.
날짜/시간 포맷 일관화 및 유틸 추가
src/main/java/com/opendata/global/util/DateUtil.java, src/main/resources/application.yml, src/main/java/com/opendata/global/config/RedisConfig.java
날짜 포맷 seconds 단위로 변경, 파싱 메서드 추가, Jackson/RedisTemplate 날짜 직렬화 설정 일치화.
API 응답 유틸 추가
src/main/java/com/opendata/global/response/ApiResponse.java
결과값 없는 성공 응답 팩토리 메서드 onSuccessVoid() 추가.
시큐리티 설정 변경
src/main/java/com/opendata/global/config/SecurityConfig.java
"/course"에서 "/course/**"로 허용 경로 확장.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller
    participant Service
    participant Redis
    participant Repository
    participant DB

    Client->>Controller: POST /course/like/{courseId}
    Controller->>Service: likeCourse(courseId)
    Service->>Redis: Get "tempCourse:{courseId}"
    Redis-->>Service: 임시 코스 데이터(JSON)
    Service->>Service: JSON → CourseComponentDto 리스트 변환
    Service->>Repository: User, TourSpot, Course, CourseComponent 조회/생성
    Service->>DB: Course/Component 저장
    Service->>Controller: 완료 응답(ApiResponse<Void>)
    Controller-->>Client: 200 OK
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Assessment against linked issues

Objective Addressed Explanation
코스 엔티티 검토 및 확장 (#45)
코스 서비스 구현 (#45)

Assessment against linked issues: Out-of-scope changes

해당 변경사항 중 요구사항과 무관한 기능적 변경은 발견되지 않았습니다.

Possibly related PRs

  • [docs] swagger 설정 #44: Swagger 문서화 및 코스 즐겨찾기 API의 초기 주석 처리 버전을 도입한 PR로, 본 PR에서 해당 기능이 실제로 활성화 및 구현됨.
  • [feat] 공통 코드 -> Enum #21: 코스 컨트롤러의 초기 활성화 PR로 본 PR의 메서드 시그니처 및 구현 변경과 관련됨.

Poem

코스 즐겨찾기, 토끼의 꿈
Redis에서 불러온 추억 한 줌
엔티티 손잡고 DB로 점프!
날짜 포맷도 초 단위로 척척
🐇 오늘도 코드를 뛰노는 중
즐겨찾기, 이제 우리 모두의 것!

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e3985b1 and 2b4209b.

📒 Files selected for processing (3)
  • src/main/java/com/opendata/domain/course/message/CourseMessages.java (1 hunks)
  • src/main/java/com/opendata/domain/course/service/CourseService.java (6 hunks)
  • src/main/java/com/opendata/domain/user/entity/User.java (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/main/java/com/opendata/domain/course/message/CourseMessages.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/com/opendata/domain/user/entity/User.java
  • src/main/java/com/opendata/domain/course/service/CourseService.java
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#45-select-course

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (5)
src/main/java/com/opendata/domain/course/controller/CourseController.java (1)

35-41: 코스 좋아요 엔드포인트 구현이 적절합니다.

RESTful 설계 원칙을 잘 따르고 있으며, 경로 변수 사용과 응답 타입이 적절합니다. 코스 선택 기능의 핵심 요구사항을 잘 구현했습니다.

다만 courseId 파라미터에 대한 검증 로직(예: null/empty 체크, 형식 검증) 추가를 고려해보세요.

src/main/java/com/opendata/domain/course/dto/response/CourseComponentDto.java (1)

13-17: 필드 명명 규칙 일관성 문제

tourSpotName은 camelCase를 사용하는 반면 tourspotId는 모두 소문자로 되어 있습니다. 일관성을 위해 tourspotIdtourSpotId로 변경하는 것이 좋겠습니다.

-        Long tourspotId,
+        Long tourSpotId,
src/main/java/com/opendata/domain/course/entity/Course.java (2)

3-3: 사용하지 않는 import 제거 필요

JsonFormat import가 사용되지 않고 있습니다.

-import com.fasterxml.jackson.annotation.JsonFormat;

42-48: 날짜 할당 메서드에 유효성 검증 추가 권장

시작 시간과 종료 시간의 유효성을 검증하면 데이터 무결성을 향상시킬 수 있습니다.

 public void assignStartDtm(LocalDateTime startDtm){
+    if (startDtm == null) {
+        throw new IllegalArgumentException("시작 시간은 null일 수 없습니다.");
+    }
+    if (this.endDtm != null && startDtm.isAfter(this.endDtm)) {
+        throw new IllegalArgumentException("시작 시간은 종료 시간보다 이후일 수 없습니다.");
+    }
     this.startDtm = startDtm;
 }

 public void assignEndDtm(LocalDateTime endDtm){
+    if (endDtm == null) {
+        throw new IllegalArgumentException("종료 시간은 null일 수 없습니다.");
+    }
+    if (this.startDtm != null && endDtm.isBefore(this.startDtm)) {
+        throw new IllegalArgumentException("종료 시간은 시작 시간보다 이전일 수 없습니다.");
+    }
     this.endDtm = endDtm;
 }
src/main/java/com/opendata/domain/course/service/CourseService.java (1)

232-244: 코스 컴포넌트 저장 로직 개선 가능

반복문 내에서 매번 save를 호출하는 것보다 배치 저장을 사용하면 성능이 향상됩니다.

+        List<CourseComponent> courseComponents = new ArrayList<>();
         for(int i = 0; i < tempCourse.size(); i++){
             CourseComponentDto courseComponentDto = tempCourse.get(i);
             if (i == 0){
                 course.assignStartDtm(DateUtil.parseTime(courseComponentDto.time()));
             }
             if (i == tempCourse.size()-1){
                 course.assignEndDtm(DateUtil.parseTime(courseComponentDto.time()));
             }
             TourSpot tourSpot = tourSpotRepository.findById(courseComponentDto.tourspotId()).orElseThrow();
             CourseComponent courseComponent = courseComponentMapper.toEntity(tourSpot, DateUtil.parseTime(courseComponentDto.time()), course);
-
-            courseComponentRepository.save(courseComponent);
+            courseComponents.add(courseComponent);
         }
+        courseComponentRepository.saveAll(courseComponents);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between caf6d86 and e3985b1.

📒 Files selected for processing (15)
  • src/main/java/com/opendata/docs/CourseControllerDocs.java (2 hunks)
  • src/main/java/com/opendata/domain/course/controller/CourseController.java (1 hunks)
  • src/main/java/com/opendata/domain/course/dto/response/CourseComponentDto.java (1 hunks)
  • src/main/java/com/opendata/domain/course/entity/Course.java (2 hunks)
  • src/main/java/com/opendata/domain/course/entity/CourseComponent.java (2 hunks)
  • src/main/java/com/opendata/domain/course/mapper/CourseComponentMapper.java (1 hunks)
  • src/main/java/com/opendata/domain/course/mapper/CourseMapper.java (2 hunks)
  • src/main/java/com/opendata/domain/course/repository/CourseComponentRepository.java (1 hunks)
  • src/main/java/com/opendata/domain/course/service/CourseService.java (6 hunks)
  • src/main/java/com/opendata/domain/user/entity/User.java (2 hunks)
  • src/main/java/com/opendata/global/config/RedisConfig.java (2 hunks)
  • src/main/java/com/opendata/global/config/SecurityConfig.java (1 hunks)
  • src/main/java/com/opendata/global/response/ApiResponse.java (1 hunks)
  • src/main/java/com/opendata/global/util/DateUtil.java (2 hunks)
  • src/main/resources/application.yml (1 hunks)
🔇 Additional comments (14)
src/main/resources/application.yml (1)

19-22: 일관된 날짜 형식 설정이 잘 구현되었습니다.

Jackson 설정을 통해 전역적으로 날짜 형식을 yyyy-MM-dd HH:mm:ss로 통일하고 타임스탬프 직렬화를 비활성화한 것은 좋은 접근입니다. 이는 DateUtil의 변경사항과 일치하며 애플리케이션 전반에서 일관된 날짜 처리를 보장합니다.

src/main/java/com/opendata/global/util/DateUtil.java (1)

34-36: 새로운 parseTime 메서드가 잘 구현되었습니다.

날짜 문자열을 LocalDateTime으로 파싱하는 메서드가 추가되어 직렬화/역직렬화 과정이 완성되었습니다. 일관된 형식 사용으로 안정적인 날짜 처리가 가능할 것입니다.

src/main/java/com/opendata/global/response/ApiResponse.java (1)

26-28: Void 성공 응답을 위한 유틸리티 메서드가 잘 추가되었습니다.

데이터 없이 성공 상태만 반환해야 하는 엔드포인트를 위한 onSuccessVoid() 메서드가 적절히 구현되었습니다. 코스 좋아요 기능과 같이 단순한 성공/실패만 알려주면 되는 API에 유용할 것입니다.

src/main/java/com/opendata/domain/course/repository/CourseComponentRepository.java (1)

1-9: 표준 JPA 레포지토리 구현이 올바릅니다.

Spring Data JPA 규칙을 잘 따르고 있으며, 기본 CRUD 작업에 필요한 모든 설정이 적절히 구성되어 있습니다.

src/main/java/com/opendata/domain/course/entity/CourseComponent.java (4)

11-11: 테이블 명시적 매핑이 적절합니다.

@Table(name = "course_component")로 테이블 이름을 명시적으로 지정하여 데이터베이스 매핑이 명확해졌습니다.


22-24: Course와의 관계 설정이 올바릅니다.

@ManyToOne 관계가 적절히 구성되었으며, LAZY 로딩과 조인 컬럼 설정이 정확합니다. 이는 코스 선택 기능 구현에 필요한 핵심 관계입니다.


12-15: Lombok 어노테이션 변경이 적절합니다.

@Builder 제거 및 @Setter 추가로 엔티티 조작이 더 유연해졌습니다. 생성자 어노테이션도 적절히 설정되어 있습니다.


34-36: 명시적 관계 설정 메서드가 유용합니다.

assignCourse 메서드를 통해 Course 관계를 명시적으로 설정할 수 있어 코드의 가독성과 의도가 명확해졌습니다.

src/main/java/com/opendata/global/config/RedisConfig.java (2)

81-83: 매개변수 이름 변경이 일관성을 높입니다.

redisConnectionFactory에서 connectionFactory로 변경하여 메서드 매개변수 명명이 더 간결해졌습니다.


85-87: 날짜/시간 직렬화 설정이 개선되었습니다.

JavaTimeModule을 명시적으로 등록하고 WRITE_DATES_AS_TIMESTAMPS를 비활성화하여 날짜/시간 처리의 일관성이 향상되었습니다. findAndRegisterModules() 대신 명시적 등록을 사용하는 것이 더 예측 가능합니다.

src/main/java/com/opendata/domain/course/mapper/CourseMapper.java (1)

16-16: toEntity(CourseComponentDto) 사용 위치 확인 필요

코드베이스에서 CourseMapper.toEntity(CourseComponentDto) 호출을 찾을 수 없습니다. CourseComponentDtoCourse로 변환하는 것은 의미상 부자연스러워 보이므로, 아래 사항을 검토해 주세요:

  • 서비스·컨트롤러 레이어에서 해당 메서드가 실제로 사용되는지 확인
  • 사용하지 않는다면 CourseMapper에서 해당 메서드를 제거하거나
    CourseComponentDto → CourseComponent 매핑은 CourseComponentMapper로 이동
  • 설계 상 CourseComponentDtoCourse로 매핑해야 하는 특별한 이유가 있다면, 주석 또는 문서로 명확히 설명

대상 파일:

  • src/main/java/com/opendata/domain/course/mapper/CourseMapper.java
src/main/java/com/opendata/docs/CourseControllerDocs.java (1)

68-92: API 문서화가 적절히 구현되었습니다.

좋아요 등록 API의 문서화가 명확하고 RESTful 규칙을 잘 따르고 있습니다. PathVariable을 사용한 단순화된 엔드포인트 설계가 좋습니다.

src/main/java/com/opendata/domain/course/mapper/CourseComponentMapper.java (1)

1-36: MapStruct 매퍼 구현이 적절합니다.

양방향 관계 설정을 위한 @AfterMapping 사용과 @Context를 통한 Course 전달이 잘 구현되었습니다. Spring 컴포넌트 모델 사용도 적절합니다.

src/main/java/com/opendata/domain/course/service/CourseService.java (1)

150-150: CourseComponentMapper 사용이 적절합니다.

CourseMapper를 CourseComponentMapper로 교체한 것이 도메인 모델에 더 적합합니다.

Also applies to: 161-161

Comment on lines +20 to 21
return new CourseComponentDto(tourSpot.getTourspotNm(), tourSpot.getTourspotId(), level.getCongestionLabel() ,c.getTourspotTm().toString(),
tourSpot.getAddress().getLatitude(), tourSpot.getAddress().getLongitude());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

날짜 형식 변환 시 포맷터 사용 필요

c.getTourspotTm().toString()은 LocalDateTime의 기본 toString() 메서드를 사용하는데, 이는 ISO-8601 형식을 반환합니다. @JsonFormat 어노테이션에 지정된 형식과 일치하도록 명시적인 포맷터를 사용해야 합니다.

-        return new CourseComponentDto(tourSpot.getTourspotNm(), tourSpot.getTourspotId(), level.getCongestionLabel() ,c.getTourspotTm().toString(),
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+        return new CourseComponentDto(tourSpot.getTourspotNm(), tourSpot.getTourspotId(), level.getCongestionLabel(), c.getTourspotTm().format(formatter),
📝 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.

Suggested change
return new CourseComponentDto(tourSpot.getTourspotNm(), tourSpot.getTourspotId(), level.getCongestionLabel() ,c.getTourspotTm().toString(),
tourSpot.getAddress().getLatitude(), tourSpot.getAddress().getLongitude());
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
return new CourseComponentDto(tourSpot.getTourspotNm(), tourSpot.getTourspotId(), level.getCongestionLabel(), c.getTourspotTm().format(formatter),
tourSpot.getAddress().getLatitude(), tourSpot.getAddress().getLongitude());
🤖 Prompt for AI Agents
In src/main/java/com/opendata/domain/course/dto/response/CourseComponentDto.java
around lines 20 to 21, the code uses c.getTourspotTm().toString() which returns
the LocalDateTime in ISO-8601 format by default. To match the date format
specified by the @JsonFormat annotation, replace the toString() call with a
DateTimeFormatter that formats the LocalDateTime explicitly according to the
desired pattern before returning it.

Comment on lines 209 to 245
@Transactional
public void likeCourse(String courseId) {
User user = userRepository.findById(1L).orElseThrow();
ObjectMapper objectMapper = new ObjectMapper();

List<?> rawList = (List<?>) redisTemplate.opsForValue().get("tempCourse:" + courseId);

List<CourseComponentDto> tempCourse = objectMapper.convertValue(
rawList,
new TypeReference<List<CourseComponentDto>>() {}
);

tempCourse.forEach(
tc ->{
System.out.println(tc.tourspotId());
System.out.println(tc.tourSpotName());
}
);
Course course = Course.builder()
.user(user)
.build();

courseRepository.save(course);
for(int i = 0; i < tempCourse.size(); i++){
CourseComponentDto courseComponentDto = tempCourse.get(i);
if (i == 0){
course.assignStartDtm(DateUtil.parseTime(courseComponentDto.time()));
}
if (i == tempCourse.size()-1){
course.assignEndDtm(DateUtil.parseTime(courseComponentDto.time()));
}
TourSpot tourSpot = tourSpotRepository.findById(courseComponentDto.tourspotId()).orElseThrow();
CourseComponent courseComponent = courseComponentMapper.toEntity(tourSpot, DateUtil.parseTime(courseComponentDto.time()), course);

courseComponentRepository.save(courseComponent);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

트랜잭션 롤백 시나리오 고려 필요

@Transactional 어노테이션이 있지만, 예외 발생 시 Redis 데이터가 이미 삭제되었을 수 있습니다. Redis 데이터 삭제는 트랜잭션이 성공적으로 완료된 후에 수행하는 것이 안전합니다.

Redis에서 데이터를 가져온 후 바로 삭제하지 말고, 모든 데이터베이스 작업이 성공적으로 완료된 후에 삭제하는 것을 권장합니다.

+        // 트랜잭션 성공 후 Redis 데이터 삭제
+        redisTemplate.delete("tempCourse:" + courseId);
📝 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.

Suggested change
@Transactional
public void likeCourse(String courseId) {
User user = userRepository.findById(1L).orElseThrow();
ObjectMapper objectMapper = new ObjectMapper();
List<?> rawList = (List<?>) redisTemplate.opsForValue().get("tempCourse:" + courseId);
List<CourseComponentDto> tempCourse = objectMapper.convertValue(
rawList,
new TypeReference<List<CourseComponentDto>>() {}
);
tempCourse.forEach(
tc ->{
System.out.println(tc.tourspotId());
System.out.println(tc.tourSpotName());
}
);
Course course = Course.builder()
.user(user)
.build();
courseRepository.save(course);
for(int i = 0; i < tempCourse.size(); i++){
CourseComponentDto courseComponentDto = tempCourse.get(i);
if (i == 0){
course.assignStartDtm(DateUtil.parseTime(courseComponentDto.time()));
}
if (i == tempCourse.size()-1){
course.assignEndDtm(DateUtil.parseTime(courseComponentDto.time()));
}
TourSpot tourSpot = tourSpotRepository.findById(courseComponentDto.tourspotId()).orElseThrow();
CourseComponent courseComponent = courseComponentMapper.toEntity(tourSpot, DateUtil.parseTime(courseComponentDto.time()), course);
courseComponentRepository.save(courseComponent);
}
}
@Transactional
public void likeCourse(String courseId) {
User user = userRepository.findById(1L).orElseThrow();
ObjectMapper objectMapper = new ObjectMapper();
List<?> rawList = (List<?>) redisTemplate.opsForValue().get("tempCourse:" + courseId);
List<CourseComponentDto> tempCourse = objectMapper.convertValue(
rawList,
new TypeReference<List<CourseComponentDto>>() {}
);
tempCourse.forEach(
tc -> {
System.out.println(tc.tourspotId());
System.out.println(tc.tourSpotName());
}
);
Course course = Course.builder()
.user(user)
.build();
courseRepository.save(course);
for (int i = 0; i < tempCourse.size(); i++) {
CourseComponentDto courseComponentDto = tempCourse.get(i);
if (i == 0) {
course.assignStartDtm(DateUtil.parseTime(courseComponentDto.time()));
}
if (i == tempCourse.size() - 1) {
course.assignEndDtm(DateUtil.parseTime(courseComponentDto.time()));
}
TourSpot tourSpot = tourSpotRepository
.findById(courseComponentDto.tourspotId())
.orElseThrow();
CourseComponent courseComponent = courseComponentMapper.toEntity(
tourSpot,
DateUtil.parseTime(courseComponentDto.time()),
course
);
courseComponentRepository.save(courseComponent);
}
// 트랜잭션 성공 후 Redis 데이터 삭제
redisTemplate.delete("tempCourse:" + courseId);
}
🤖 Prompt for AI Agents
In src/main/java/com/opendata/domain/course/service/CourseService.java between
lines 209 and 245, the method likeCourse uses @Transactional but does not handle
Redis data deletion safely. To fix this, ensure that Redis data deletion happens
only after all database operations complete successfully. Move the Redis
deletion logic to after the course and course components are saved, so that if
any exception occurs, the Redis data remains intact and is only removed upon
successful transaction commit.

// }
@Transactional
public void likeCourse(String courseId) {
User user = userRepository.findById(1L).orElseThrow();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

하드코딩된 사용자 ID 문제

사용자 ID가 1L로 하드코딩되어 있습니다. 실제 인증된 사용자 정보를 사용해야 합니다.

-        User user = userRepository.findById(1L).orElseThrow();
+        // SecurityContext에서 현재 사용자 ID를 가져와야 합니다
+        Long userId = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+        User user = userRepository.findById(userId)
+            .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
🤖 Prompt for AI Agents
In src/main/java/com/opendata/domain/course/service/CourseService.java at line
211, the user ID is hardcoded as 1L when fetching the user. Replace this
hardcoded ID with the actual authenticated user's ID by retrieving it from the
security context or session, ensuring the method dynamically uses the current
user's information instead of a fixed value.

Comment on lines 214 to 219
List<?> rawList = (List<?>) redisTemplate.opsForValue().get("tempCourse:" + courseId);

List<CourseComponentDto> tempCourse = objectMapper.convertValue(
rawList,
new TypeReference<List<CourseComponentDto>>() {}
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Redis 데이터 조회 시 에러 처리 필요

Redis에서 데이터를 가져올 때 null 체크와 타입 검증이 필요합니다.

         List<?> rawList = (List<?>) redisTemplate.opsForValue().get("tempCourse:" + courseId);
+        if (rawList == null || rawList.isEmpty()) {
+            throw new IllegalArgumentException("유효하지 않은 코스 ID입니다: " + courseId);
+        }
 
         List<CourseComponentDto> tempCourse = objectMapper.convertValue(
                 rawList,
                 new TypeReference<List<CourseComponentDto>>() {}
         );
+        if (tempCourse == null || tempCourse.isEmpty()) {
+            throw new IllegalStateException("코스 데이터 변환에 실패했습니다.");
+        }
📝 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.

Suggested change
List<?> rawList = (List<?>) redisTemplate.opsForValue().get("tempCourse:" + courseId);
List<CourseComponentDto> tempCourse = objectMapper.convertValue(
rawList,
new TypeReference<List<CourseComponentDto>>() {}
);
List<?> rawList = (List<?>) redisTemplate.opsForValue().get("tempCourse:" + courseId);
if (rawList == null || rawList.isEmpty()) {
throw new IllegalArgumentException("유효하지 않은 코스 ID입니다: " + courseId);
}
List<CourseComponentDto> tempCourse = objectMapper.convertValue(
rawList,
new TypeReference<List<CourseComponentDto>>() {}
);
if (tempCourse == null || tempCourse.isEmpty()) {
throw new IllegalStateException("코스 데이터 변환에 실패했습니다.");
}
🤖 Prompt for AI Agents
In src/main/java/com/opendata/domain/course/service/CourseService.java around
lines 214 to 219, the code retrieves data from Redis without checking if the
result is null or verifying the type before conversion. Add a null check after
fetching the data from Redis to handle cases where the key does not exist, and
ensure the data is of the expected type before calling objectMapper.convertValue
to prevent runtime errors.

Comment on lines 221 to 226
tempCourse.forEach(
tc ->{
System.out.println(tc.tourspotId());
System.out.println(tc.tourSpotName());
}
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

디버깅용 System.out.println 제거 필요

프로덕션 코드에서 System.out.println을 사용하면 안 됩니다. 적절한 로깅을 사용해야 합니다.

-        tempCourse.forEach(
-                tc ->{
-                    System.out.println(tc.tourspotId());
-                    System.out.println(tc.tourSpotName());
-                }
-        );
+        tempCourse.forEach(tc -> 
+            log.debug("Processing course component - tourSpotId: {}, tourSpotName: {}", 
+                tc.tourspotId(), tc.tourSpotName())
+        );
📝 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.

Suggested change
tempCourse.forEach(
tc ->{
System.out.println(tc.tourspotId());
System.out.println(tc.tourSpotName());
}
);
tempCourse.forEach(tc ->
log.debug("Processing course component - tourSpotId: {}, tourSpotName: {}",
tc.tourspotId(), tc.tourSpotName())
);
🤖 Prompt for AI Agents
In src/main/java/com/opendata/domain/course/service/CourseService.java between
lines 221 and 226, replace the System.out.println statements used for debugging
with proper logging calls using the project's logging framework. Remove the
print statements and use a logger to output the tourspotId and tourSpotName,
ensuring the log level is appropriate (e.g., debug or info) for production code.

.authorizeHttpRequests(requests -> requests
.requestMatchers("/oauth2/**","/register/*","/login/oauth2/**", "/swagger-ui/**", // Swagger UI 관련 경로
"/v3/api-docs/**","/api/tourspot/**", "/course","/","/login").permitAll()
"/v3/api-docs/**","/api/tourspot/**", "/course/**","/","/login").permitAll()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

코스 관련 엔드포인트의 보안 설정을 세분화하세요.

"/course/**" 패턴으로 변경하면 모든 코스 하위 경로에 대한 접근이 허용됩니다. 하지만 POST /course/like/{courseId}와 같은 데이터 수정 작업은 인증된 사용자만 접근할 수 있어야 합니다.

다음과 같이 보안 설정을 세분화하는 것을 권장합니다:

-                        .requestMatchers("/oauth2/**","/register/*","/login/oauth2/**", "/swagger-ui/**",    // Swagger UI 관련 경로
-                                "/v3/api-docs/**","/api/tourspot/**", "/course/**","/","/login").permitAll()
+                        .requestMatchers("/oauth2/**","/register/*","/login/oauth2/**", "/swagger-ui/**",    // Swagger UI 관련 경로
+                                "/v3/api-docs/**","/api/tourspot/**", "/course/search/**","/","/login").permitAll()
+                        .requestMatchers(HttpMethod.POST, "/course/**").authenticated()
📝 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.

Suggested change
"/v3/api-docs/**","/api/tourspot/**", "/course/**","/","/login").permitAll()
.requestMatchers("/oauth2/**","/register/*","/login/oauth2/**", "/swagger-ui/**", // Swagger UI 관련 경로
"/v3/api-docs/**","/api/tourspot/**", "/course/search/**","/","/login").permitAll()
.requestMatchers(HttpMethod.POST, "/course/**").authenticated()
🤖 Prompt for AI Agents
In src/main/java/com/opendata/global/config/SecurityConfig.java at line 95, the
current security configuration permits all access to "/course/**", which allows
unauthenticated users to perform data modification actions like POST
"/course/like/{courseId}". To fix this, refine the security rules by permitting
only safe GET requests to "/course/**" for all users, and restrict POST or other
modifying requests under "/course/**" to authenticated users. Adjust the
antMatchers or equivalent method calls to differentiate between read-only and
modifying endpoints accordingly.


public class DateUtil {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

날짜 형식 변경에 대한 기존 사용처 확인이 필요합니다.

날짜 형식을 초 단위까지 포함하도록 변경한 것은 좋습니다. 하지만 기존 코드에서 이 포맷터를 사용하는 부분이 있다면 호환성 문제가 발생할 수 있습니다.

다음 스크립트로 기존 사용처를 확인해주세요:


🏁 Script executed:

#!/bin/bash
# DateUtil의 FORMATTER 사용처 확인
rg -A 3 -B 3 "DateUtil\." --type java

Length of output: 3560


🏁 Script executed:

#!/bin/bash
sed -n '1,200p' src/main/java/com/opendata/global/util/DateUtil.java

Length of output: 1320


DateUtil 초 단위 변경 적용 시 기존 사용처 호환성 점검 및 처리 필요

초 단위까지 포함하는 포맷으로 변경되면서, 아래 사용처에서 파싱 예외나 데이터 불일치가 발생할 수 있습니다. 대응 방안(DTO 포맷 수정, 파싱 로직 확장, 쿼리 수정 등)을 검토해주세요.

  • src/main/java/com/opendata/domain/course/service/CourseService.java
    • DateUtil.parseTime(courseComponentDto.time()) 호출
    – courseComponentDto.time()가 기존 “yyyy-MM-dd HH:mm” 포맷일 경우 DateTimeParseException 발생 가능
    ▶ DTO.time() 반환값에 초(:ss) 포함 또는 parseTime에 다중 포맷 분기 추가 필요

  • src/main/java/com/opendata/domain/tourspot/service/TourSpotService.java
    • currentCongestionRepository.findByTourSpotAndCurTime(..., DateUtil.getCurrentRoundedFormattedDateTime()) 호출
    – 쿼리 키에 초(“:00”) 포함 여부 점검 필요
    ▶ DB 스키마/쿼리 매핑 업데이트 또는 초 제외 로직 도입 검토

  • 선택 사항: DateUtil.parseTime 메서드에 “yyyy-MM-dd HH:mm” 포맷도 처리할 수 있는 fallback 로직 추가 고려

🤖 Prompt for AI Agents
In src/main/java/com/opendata/global/util/DateUtil.java at line 7, the
DateTimeFormatter pattern was changed to include seconds, which may cause
parsing errors in existing code expecting "yyyy-MM-dd HH:mm" format. Review all
usages, especially in CourseService.java and TourSpotService.java, and update
them accordingly. Fix this by either modifying DTO.time() to include seconds, or
enhance DateUtil.parseTime to support multiple formats with fallback logic. Also
verify and adjust any database queries or schema that rely on datetime strings
without seconds to ensure compatibility.

@kamillcream kamillcream merged commit 809efe6 into main Aug 6, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 코스 선택 기능 구현

2 participants