Skip to content

refactor : elasticsearch like filter 수정#15

Merged
doma17 merged 9 commits intoCodIN-INU:mainfrom
BHC-Chicken:refactor/like-filter
Aug 15, 2025
Merged

refactor : elasticsearch like filter 수정#15
doma17 merged 9 commits intoCodIN-INU:mainfrom
BHC-Chicken:refactor/like-filter

Conversation

@BHC-Chicken
Copy link
Contributor

@BHC-Chicken BHC-Chicken commented Aug 7, 2025

  • like로 필터를 걸었을 때 좋아요한 강의가 없다면 빈 리스트를 반환합니다.

Summary by CodeRabbit

  • 버그 수정

    • 좋아요 필터가 활성화된 상태에서 좋아요된 강의 목록이 비어 있으면 즉시 빈 결과를 반환하도록 개선되었습니다.
  • 신규 기능

    • 강의별 조회수(hits)를 원자적으로 증가시키는 카운트가 추가되었습니다.
    • 문서 기반 검색 결과로 강의 미리보기가 반환되어 즉시 노출됩니다.
  • 개선

    • 검색 쿼리 및 정렬 로직이 간소화되어 관련도와 정렬 품질이 향상되었습니다.
    • 강의 문서에 감정, 시간표, 학기정보, 강의계획서·AI요약 등 풍부한 필드가 추가되었습니다.
  • 리팩터링

    • 검색/색인 흐름과 파라미터 구조가 정리되어 내부 처리와 인덱싱이 단순화되었습니다.

- like로 필터를 걸었을 때 좋아요한 강의가 없다면 빈 리스트를 반환합니다.
@BHC-Chicken BHC-Chicken requested a review from doma17 August 7, 2025 09:18
@BHC-Chicken BHC-Chicken self-assigned this Aug 7, 2025
@BHC-Chicken BHC-Chicken added the enhancement New feature or request label Aug 7, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 7, 2025

📝 Walkthrough

Walkthrough

검색·미리보기 흐름이 Elasticsearch 문서(LectureDocument) 중심으로 전환되었고, 좋아요 필터(Boolean like)와 likedIds 기반 조기 반환이 도입되었으며 정렬이 Spring Data Sort로 변경되고 조회수 증분용 incrementHits가 추가되었습니다. 인덱싱 시작 흐름과 문서 모델/변환 로직이 확장되었습니다.

Changes

Cohort / File(s) Change Summary
Elastic service: API, 쿼리, 정렬, hits 증가
src/main/java/inu/codin/codin/domain/elasticsearch/service/LectureElasticService.java
searchIdssearchLectureDocument로 이름/시그니처 변경(리턴 Page<Long>Page<LectureDocument>, Boolean like 추가), like=true && likedIds 비어있음 시 빈 Page 조기 반환, 쿼리 재구성(multiMatch / matchAll), Spring Data Sort 적용으로 정렬 로직 리팩토링, incrementHits(Long) 추가(painless UpdateQuery).
Search repo 인터페이스: 시그니처 변경
src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryCustom.java
searchLecturesAtPreview 반환 타입을 Page<Lecture>Page<LectureDocument>로 변경하고 liked 파라미터명 변경 및 Boolean like 추가.
Search repo 구현: ES 문서 직접 반환
src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryImpl.java
JPA 엔티티 재정렬 로직 제거, ES에서 받은 LectureDocument를 직접 반환하도록 변경(시그니처·내부 호출 업데이트).
Service 계층: ES 통합 및 매핑 변경
src/main/java/inu/codin/codin/domain/lecture/service/LectureService.java
LectureElasticService, LikeService 주입 추가, preview 검색 경로에서 likeList와 Boolean like 전달, getLectureDetailsincrementHits 호출 추가, 페이지 응답 매핑을 LectureDocument 기반으로 변경.
Document 변환기: 필드 확장 및 null-safe 헬퍼 추가
src/main/java/inu/codin/codin/domain/elasticsearch/convertor/LectureDocumentConverter.java
credit, preCourses, schedule, semesters, tags, emotion, syllabus, aiSummary 등 필드 추가 및 toPreCourses/toSemesterInfos/toTagNames/toScheduleInfos 헬퍼 도입, null 안전성 강화.
Document 모델: 매핑·필드 추가
src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java
credit, preCourses, starRating, likes(int), hits(int), semesters, schedule, emotion, syllabus(multi-field), aiSummary, type, lectureType, evaluation, department 등 매핑 추가/조정.
DTOs: nested DTO 추가
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/EmotionInfo.java
.../ScheduleInfo.java
.../SemesterInfo.java
Elasticsearch 문서용 DTO 추가(EmotionInfo, ScheduleInfo, SemesterInfo) 및 static from(...) 팩토리 구현.
Indexer: 시작 인덱싱 흐름 변경
src/main/java/inu/codin/codin/domain/elasticsearch/indexer/LectureStartupIndexer.java
ApplicationReadyEvent 리스너 제거, 공개 lectureIndex() 추가, 기존 인덱스 무조건 삭제 후 재생성 및 페이징 인덱싱으로 흐름 변경.
Upload service: 업로드·인덱싱 트리거, 파일 저장 정리
src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java
업로드 메서드명들 교체(강의/강의실 처리 분리), 공통 saveFile 도입, 성공 시 LectureStartupIndexer.lectureIndex() 호출(강의 업로드 경로).
응답 DTO: preview 매핑 변경
src/main/java/inu/codin/codin/domain/lecture/dto/LecturePreviewResponseDto.java
입력 타입을 LectureLectureDocument로 변경, liked boolean→likes int로 변경 및 필드 매핑 조정.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant LectureService
    participant LectureSearchRepoImpl
    participant LectureElasticService
    participant LikeService

    Client->>LectureService: sortListOfLectures(...)
    LectureService->>LectureSearchRepoImpl: searchLecturesAtPreview(keyword, dept, sort, likeList, pageable, like)
    LectureSearchRepoImpl->>LectureElasticService: searchLectureDocument(keyword, dept, sort, likedIds, page, size, like)
    alt like == true and likedIds empty
        LectureElasticService-->>LectureSearchRepoImpl: return empty Page<LectureDocument>
    else
        LectureElasticService-->>LectureSearchRepoImpl: return Page<LectureDocument> (documents + total)
    end
    LectureSearchRepoImpl-->>LectureService: Page<LectureDocument>
    LectureService->>LikeService: count likes for document ids
    LikeService-->>LectureService: likeCounts
    LectureService-->>Client: LecturePageResponse (mapped from LectureDocument + likeCounts)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

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 Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

🪧 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.
    • 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.
  • 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 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/Issue comments)

Type @coderabbitai help to get the list of available commands.

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

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • 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: 2

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between e6e9e8f and a347c79.

📒 Files selected for processing (4)
  • src/main/java/inu/codin/codin/domain/elasticsearch/service/LectureElasticService.java (2 hunks)
  • src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryCustom.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryImpl.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/lecture/service/LectureService.java (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryCustom.java (1)
src/main/java/inu/codin/codin/domain/like/service/LikeService.java (1)
  • applyLikeToLecture (88-96)
🔇 Additional comments (5)
src/main/java/inu/codin/codin/domain/lecture/service/LectureService.java (2)

54-54: 변수명 개선이 좋습니다.

liked에서 likeList로 변경하여 변수의 타입과 용도를 더 명확하게 표현했습니다.


61-61: 메서드 시그니처 호출 일관성 확인 완료
searchLecturesAtPreview 메서드는 인터페이스(LectureSearchRepositoryCustom), 구현체(LectureSearchRepositoryImpl), 서비스 호출 지점(LectureService) 모두에 새로 추가된 Boolean like 파라미터가 일관되게 반영되어 있어 추가 조치가 필요 없습니다.

src/main/java/inu/codin/codin/domain/elasticsearch/service/LectureElasticService.java (2)

54-56: 좋아요 필터 최적화가 잘 구현되었습니다.

like가 true이고 좋아요한 강의 ID 목록이 비어있을 때 빈 결과를 즉시 반환하는 로직이 올바르게 구현되었습니다. 이는 불필요한 Elasticsearch 쿼리를 방지하여 성능을 개선합니다.


49-50: 메서드 시그니처 일관성 확인 완료

  • LectureElasticService.searchIds 메서드에 Boolean like가 맨 끝에 추가된 것을 확인했습니다.
  • LectureSearchRepositoryImpl(40행)에서 해당 메서드를 호출할 때도 page, size, like 순서로 올바르게 전달되고 있습니다.
  • 다른 호출 지점이 없어 파라미터 순서 변경으로 인한 영향은 없습니다.
src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryImpl.java (1)

39-40: 파라미터 전달 로직 확인

로그 메시지와 Elasticsearch 서비스 호출에서 파라미터가 올바르게 전달되고 있습니다. 파라미터명만 일관성 있게 수정하면 됩니다.


public interface LectureSearchRepositoryCustom {
Page<Lecture> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> liked, Pageable pageable);
Page<Lecture> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> listList, Pageable pageable, Boolean like);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

인터페이스 파라미터 명명 일관성 문제

인터페이스의 List<Long> listList 파라미터명이 서비스 레이어의 likeList와 일치하지 않습니다. 전체 코드베이스에서 일관된 명명을 사용해야 합니다.

다음과 같이 수정을 권장합니다:

-Page<Lecture> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> listList, Pageable pageable, Boolean like);
+Page<Lecture> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> likeList, Pageable pageable, Boolean like);
📝 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
Page<Lecture> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> listList, Pageable pageable, Boolean like);
Page<Lecture> searchLecturesAtPreview(
String keyword,
Department department,
SortingOption sortingOption,
List<Long> likeList,
Pageable pageable,
Boolean like
);
🤖 Prompt for AI Agents
In
src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryCustom.java
at line 13, the parameter name listList in the method searchLecturesAtPreview is
inconsistent with the service layer's naming likeList. Rename the parameter from
listList to likeList to maintain consistent naming conventions across the
codebase.

public Page<Lecture> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> liked, Pageable pageable) {
log.debug("[searchLecturesAtPreview] 강의 조회, keyword={}, department={}, sortingOption={}, liked={}", keyword, department, sortingOption, liked);
Page<Long> idPage = lectureElasticService.searchIds(keyword, department, sortingOption, liked, pageable.getPageNumber(), pageable.getPageSize());
public Page<Lecture> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> listList, Pageable pageable, Boolean like) {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

파라미터 명명 일관성 문제

List<Long> listList 파라미터명이 서비스 레이어의 likeList와 일치하지 않습니다. 코드 가독성을 위해 동일한 명명 규칙을 사용하는 것이 좋겠습니다.

다음과 같이 수정을 권장합니다:

-public Page<Lecture> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> listList, Pageable pageable, Boolean like) {
+public Page<Lecture> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> likeList, Pageable pageable, Boolean like) {
🤖 Prompt for AI Agents
In
src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryImpl.java
at line 38, the parameter name List<Long> listList is inconsistent with the
service layer's likeList. Rename listList to likeList to maintain naming
consistency and improve code readability.

- 스케출 필드 추가
- 평점 필드 추가
- 학점 필드 추가
- 서버 재시작 시 -> 문서 업로드 시로 변경
- 검색 메소드가 ID 대신 LectureDocument 페이지를 직접 반환하도 리팩토링하여 조회 효율 개선
- Spring Data의 Sort를 사용하여 동적 정렬 로직 구현
- 복잡한 검색 쿼리를 multiMatch를 사용하여 단순화
- Painless 스크립트를 이용한 강의 조회수(hits) 증가 기능 추가
- Lecture -> LectureDocument 반환에 따른 DTO 수정
LectureElasticService가 LectureDocument를 직접 반환하도록 변경됨에 따라, LectureSearchRepositoryImpl의 searchLecturesAtPreview 메소드 로직을 개선했습니다.

기존 방식의 문제점:
- Elasticsearch에서 강의 ID 목록을 가져온 후, 다시 JPA를 통해 RDBMS에서 Lecture 엔티티를 조회하는 2단계 프로세스로 인해 불필요한 DB I/O가 발생했습니다.

- Elasticsearch의 정렬 순서를 유지하기 위해 조회된 엔티티를 수동으로 재정렬하는 복잡한 코드가 필요했습니다.

개선된 내용:

- 성능 향상 및 로직 단순화
   - LectureElasticService로부터 Page<LectureDocument>를 직접 받아 반환하도록 변경하여, 추가적인 DB 조회 및 수동 정렬 로직을 모두 제거했습니다. 이를 통해 응답 성능과 코드의 가독성, 유지보수성을 크게 향상시켰습니다.

- 반환 타입 변경
   - 메소드의 반환 타입을 Page<Lecture>에서 Page<LectureDocument>로 변경하여, 데이터 소스(Elasticsearch)와 API 응답의 일관성을 맞췄습니다.

- 불필요한 코드 제거
   - 과거 QueryDSL 검색 방식에 사용되던 @deprecated 메소드들과 더 이상 사용하지 않는 LectureRepository 의존성을 완전히 제거하여 코드를 정리했습니다.
리포지토리 계층이 Elasticsearch 검색 결과를 직접(Page<LectureDocument>) 반환하도록 리팩토링됨에 따라, LectureService의 관련 로직을 다음과 같이 수정하고 최적화했습니다.

주요 변경 사항:
- 강의 검색 로직 수정 (sortListOfLectures)
   - LectureSearchRepository로부터 Page<Lecture> 대신 Page<LectureDocument>를 받도록 수정했습니다.

   - DTO 변환 로직(getLecturePageResponse)도 LectureDocument를 직접 사용하도록 변경하여, 서비스 계층에서 불필요한 데이터 변환 과정을 제거했습니다.

- 리스트 DTO 생성 로직 최적화 (getLecturePageResponse)

응답 데이터 변경: 각 강의의 총 좋아요 수(likeCount)를 조회하여 LecturePreviewResponseDto에 포함하도록 변경했습니다.

- 조회수 동기화 로직 추가 (getLectureDetails)
   - 강의 상세 정보 조회 시, RDBMS의 조회수를 증가시키는 것과 더불어 Elasticsearch 문서의 hits 필드도 함께 증가시키도록 lectureElasticService.incrementHits() 호출을 추가했습니다. 이를 통해 두 데이터 소스(RDBMS, Elasticsearch) 간의 데이터 정합성을 유지합니다.

의존성 변경:
- LectureElasticService에 대한 의존성을 추가했습니다.
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: 13

♻️ Duplicate comments (2)
src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryCustom.java (1)

13-15: 파라미터 명명(listList) 일관성 문제: likeList로 수정 권장

서비스 레이어와의 명명 일관성 및 가독성을 위해 listListlikeList로 변경하세요. 이전 코멘트에서도 동일 이슈가 지적되었습니다.

-    Page<LectureDocument> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> listList, Pageable pageable, Boolean like);
+    Page<LectureDocument> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> likeList, Pageable pageable, Boolean like);

또한 구현체/호출부 전역 반영 확인을 권장합니다:

#!/bin/bash
# 인터페이스 구현/호출부 시그니처 일치 여부 확인
rg -n "searchLecturesAtPreview\(" -A 2
src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryImpl.java (1)

36-36: 파라미터 명명 일관성 문제

List<Long> listList 파라미터명이 서비스 레이어의 likeList와 일치하지 않습니다. 코드 가독성을 위해 동일한 명명 규칙을 사용하는 것이 좋겠습니다.

다음과 같이 수정을 권장합니다:

-public Page<LectureDocument> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> listList, Pageable pageable, Boolean like) {
+public Page<LectureDocument> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> likeList, Pageable pageable, Boolean like) {
🧹 Nitpick comments (15)
src/main/java/inu/codin/codin/domain/elasticsearch/indexer/LectureStartupIndexer.java (3)

29-29: 상수는 static final로 선언하세요.

클래스 상수 성격이므로 static final로 선언하는 편이 관례적이며, 메모리/가독성 측면에서도 이점이 있습니다.

-    private final int CHUNK_SIZE = 100;
+    private static final int CHUNK_SIZE = 100;

47-58: 운영 중 인덱스 즉시 삭제-재생성은 다운타임/권한 리스크가 큽니다. Alias 기반 블루-그린 전략을 고려하세요.

현재 로직은 항상 기존 인덱스를 삭제 후 재생성합니다. 색인 중에는 검색이 완전히 비거나 오류가 날 수 있고(다운타임), 삭제 권한/리스크가 큽니다. 권장 전략:

  • 신규 인덱스(예: lectures_v2_YYYYMMDDHHmm) 생성 및 매핑/설정 적용
  • 신규 인덱스로 전체 색인 수행
  • 완료 후 alias(예: lectures_read)를 신규 인덱스로 atomically 전환
  • (선택) 이전 인덱스는 보관/삭제

이렇게 하면 무중단 전환과 롤백이 용이합니다. Spring Data Elasticsearch에서도 IndexOperations와 alias 전환을 조합해 구현 가능합니다.


79-79: 로그 태그 일관성

"[indexAllLectures]"는 이전 구현의 흔적으로 보입니다. 현재 메서드명에 맞추어 태그를 정리하세요.

-                log.info("[indexAllLectures] Indexed Lecture: {}, Total: {}", documents.size(), totalProcessed);
+                log.info("[performIndexing] Indexed Lecture: {}, Total: {}", documents.size(), totalProcessed);
src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java (3)

42-43: 로그 파일명은 getOriginalFilename() 사용 권장

MultipartFile#getName()은 파라미터명으로, 실제 업로드 파일명이 아닙니다. 운영 로그 가독성과 추적성을 위해 getOriginalFilename()을 사용하세요.

-            log.info("[uploadNewSemesterLectures] {} 학기 강의 정보 업로드 완료", file.getName());
+            log.info("[uploadNewSemesterLectures] {} 학기 강의 정보 업로드 완료", file.getOriginalFilename());
-            log.info("[uploadNewSemesterRooms] {} 강의실 현황 업데이트 완료", file.getName());
+            log.info("[uploadNewSemesterRooms] {} 강의실 현황 업데이트 완료", file.getOriginalFilename());

Also applies to: 63-63


71-83: 경로 결합 시 문자열 더하기 대신 File/Path API 사용 권장

플랫폼 독립성과 경로 안전성을 위해 경로 결합은 File/Path API를 사용하세요.

-        File savedFile = new File(UPLOAD_DIR + file.getOriginalFilename());
+        File savedFile = new File(UPLOAD_DIR, file.getOriginalFilename());

86-91: ProcessBuilder 인자 경로 결합 개선

스크립트/파일 경로를 문자열 덧셈으로 결합하면 경계 슬래시 문제를 유발할 수 있습니다. File API를 사용하세요.

-        ProcessBuilder processBuilder = new ProcessBuilder(
-                PYTHON_DIR, UPLOAD_DIR + pythonNm, UPLOAD_DIR+ file.getOriginalFilename()
-        );
+        ProcessBuilder processBuilder = new ProcessBuilder(
+                PYTHON_DIR,
+                new java.io.File(UPLOAD_DIR, pythonNm).getPath(),
+                new java.io.File(UPLOAD_DIR, file.getOriginalFilename()).getPath()
+        );
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/SemesterInfo.java (1)

19-26: NPE 방어 로직 여부 확인

lectureSemester 혹은 그 내부 semester가 null일 가능성이 전혀 없다면 현재 구현으로 충분합니다. 다만, 데이터 정합성이 100% 보장되지 않는다면 NPE가 색인 전체를 중단시킬 수 있습니다. 필요 시 null 가드를 추가하고, 호출 측에서 null을 필터링하도록 합의하세요.

예시:

-    public static SemesterInfo from(LectureSemester lectureSemester) {
-        Semester semester = lectureSemester.getSemester();
+    public static SemesterInfo from(LectureSemester lectureSemester) {
+        if (lectureSemester == null || lectureSemester.getSemester() == null) {
+            return null;
+        }
+        Semester semester = lectureSemester.getSemester();
src/main/java/inu/codin/codin/domain/lecture/dto/LecturePreviewResponseDto.java (1)

81-83: 사용되지 않는 getTags 메서드 제거 권장

getTags 정적 메서드가 더 이상 사용되지 않는 것으로 보입니다. 코드 정리를 위해 제거하는 것을 고려해보세요.

-    public static List<String> getTags(Set<LectureTag> tags) {
-        return tags.stream().map(tag -> tag.getTag().getTagName()).toList();
-    }
src/main/java/inu/codin/codin/domain/lecture/service/LectureService.java (2)

58-64: 좋아요 필터링 로직의 조기 반환 처리

좋아요가 체크되었지만 사용자가 좋아요한 강의가 없는 경우, 빈 리스트가 Elasticsearch 쿼리에 전달됩니다. 이는 PR 목표와 일치하지만, 조기 반환으로 성능을 개선할 수 있습니다.

다음과 같이 조기 반환을 추가하는 것을 고려해보세요:

 if (like != null && like) {
     likeList = likeService.getLiked(LikeType.LECTURE).stream()
             .map(likedResponseDto -> Long.valueOf(likedResponseDto.getLikeTypeId()))
             .toList();
+    
+    // 좋아요한 강의가 없으면 빈 페이지 반환
+    if (likeList.isEmpty()) {
+        return LecturePageResponse.of(List.of(), 0, -1);
+    }
 }

95-100: 좋아요 수 조회 성능 최적화

각 강의마다 개별적으로 좋아요 수를 조회하는 것은 N+1 성능 문제를 야기할 수 있습니다.

배치로 좋아요 수를 조회하도록 개선해보세요:

 private LecturePageResponse getLecturePageResponse(Page<LectureDocument> lecturePage) {
+    // 모든 강의 ID를 한 번에 조회
+    List<Long> lectureIds = lecturePage.getContent().stream()
+            .map(LectureDocument::getId)
+            .toList();
+    
+    Map<Long, Integer> likeCountMap = likeService.getLikeCounts(LikeType.LECTURE, lectureIds);
     
     return LecturePageResponse.of(lecturePage.stream()
-                    .map(lectureDocument -> {
-                                int likeCount = likeService.getLikeCount(LikeType.LECTURE, String.valueOf(lectureDocument.getId()));
-
-                                return LecturePreviewResponseDto.of(lectureDocument, likeCount);
-                            }
-                    )
+                    .map(lectureDocument -> {
+                        int likeCount = likeCountMap.getOrDefault(lectureDocument.getId(), 0);
+                        return LecturePreviewResponseDto.of(lectureDocument, likeCount);
+                    })
                     .toList(),

LikeService에 배치 조회 메서드 getLikeCounts(LikeType type, List<Long> ids)를 추가해야 합니다.

src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryImpl.java (1)

37-37: 로그 메시지의 파라미터명 불일치

로그 메시지에서 liked={}로 표시되지만 실제로는 listList 값을 출력하고 있습니다. 혼란을 방지하기 위해 일관성을 맞춰야 합니다.

-        log.debug("[searchLecturesAtPreview] 강의 조회, keyword={}, department={}, sortingOption={}, liked={}", keyword, department, sortingOption, listList);
+        log.debug("[searchLecturesAtPreview] 강의 조회, keyword={}, department={}, sortingOption={}, likeList={}, like={}", keyword, department, sortingOption, listList, like);
src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java (1)

16-20: 엘라스틱 문서 역직렬화 호환성 보완(@NoArgsConstructor 권장)

현재 @AllArgsConstructor(protected), @builder만 있습니다. Spring Data ES는 기본 생성자 없이도 빌더를 활용할 수 있으나, 역직렬화/프록시 등 호환성 측면에서 기본 생성자를 두는 편이 안전합니다.

아래 추가를 고려해 주세요.

 @Getter
 @Builder
 @AllArgsConstructor(access = AccessLevel.PROTECTED)
+@lombok.NoArgsConstructor(access = AccessLevel.PROTECTED)
 public class LectureDocument {
src/main/java/inu/codin/codin/domain/elasticsearch/service/LectureElasticService.java (3)

97-97: 하드코딩된 인덱스명 제거: 도큐먼트 기반 좌표 사용 권장

"lectures" 문자열 하드코딩은 인덱스명 변경 시 누락 위험이 있습니다. Annotation 기반 인덱스 좌표를 사용하세요.

아래처럼 교체:

-        SearchHits<LectureDocument> hits = elasticsearchOperations.search(query, LectureDocument.class, IndexCoordinates.of("lectures"));
+        SearchHits<LectureDocument> hits = elasticsearchOperations.search(
+            query,
+            LectureDocument.class,
+            elasticsearchOperations.getIndexCoordinatesFor(LectureDocument.class)
+        );

116-116: incrementHits에서도 인덱스명 하드코딩 제거

동일한 맥락으로 Update에서도 인덱스 좌표를 도큐먼트로부터 가져오도록 변경하세요.

-        elasticsearchOperations.update(uq, IndexCoordinates.of("lectures"));
+        elasticsearchOperations.update(uq, elasticsearchOperations.getIndexCoordinatesFor(LectureDocument.class));

45-46: JavaDoc 반환 타입 주석 불일치

메서드 반환 타입이 Page로 변경되었으나 주석은 Page 입니다. 주석을 최신화하세요.

-     * @return Page<Long>
+     * @return Page<LectureDocument>
📜 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 settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a347c79 and d6a00a8.

📒 Files selected for processing (12)
  • src/main/java/inu/codin/codin/domain/elasticsearch/convertor/LectureDocumentConverter.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java (3 hunks)
  • src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/EmotionInfo.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/ScheduleInfo.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/SemesterInfo.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/elasticsearch/indexer/LectureStartupIndexer.java (2 hunks)
  • src/main/java/inu/codin/codin/domain/elasticsearch/service/LectureElasticService.java (3 hunks)
  • src/main/java/inu/codin/codin/domain/lecture/dto/LecturePreviewResponseDto.java (2 hunks)
  • src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryCustom.java (2 hunks)
  • src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryImpl.java (2 hunks)
  • src/main/java/inu/codin/codin/domain/lecture/service/LectureService.java (4 hunks)
  • src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java (4 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/EmotionInfo.java (2)
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/ScheduleInfo.java (1)
  • Getter (9-33)
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/SemesterInfo.java (1)
  • Getter (10-27)
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/ScheduleInfo.java (2)
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/EmotionInfo.java (1)
  • Getter (9-32)
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/SemesterInfo.java (1)
  • Getter (10-27)
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/SemesterInfo.java (2)
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/EmotionInfo.java (1)
  • Getter (9-32)
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/ScheduleInfo.java (1)
  • Getter (9-33)
src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java (2)
src/main/java/inu/codin/codin/global/auth/filter/TokenValidationFilter.java (1)
  • RequiredArgsConstructor (21-65)
src/main/java/inu/codin/codin/domain/lecture/service/LectureService.java (1)
  • Service (32-128)
src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java (4)
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/EmotionInfo.java (1)
  • Getter (9-32)
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/ScheduleInfo.java (1)
  • Getter (9-33)
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/SemesterInfo.java (1)
  • Getter (10-27)
src/main/java/inu/codin/codin/domain/lecture/entity/Lecture.java (1)
  • Getter (16-89)
🔇 Additional comments (9)
src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryCustom.java (2)

13-15: ES 문서 기반 반환 타입 전환은 타당합니다

미리보기 검색을 Elasticsearch Document 중심으로 전환한 점 일관됩니다. 서비스 레이어(LectureService.sortListOfLectures)와도 시그니처가 부합합니다.


13-15: like 필터 조기 반환 전략 확인

PR 목적(좋아요 필터가 활성화이고 좋아요 목록이 비어있을 때 빈 목록 반환)에 따라, 구현체에서 Boolean.TRUE.equals(like) && (likeList == null || likeList.isEmpty()) 조건 시 즉시 빈 Page를 반환하는지 확인 바랍니다. 불필요한 ES 쿼리 수행을 줄일 수 있습니다.

src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/EmotionInfo.java (1)

1-32: 구조 깔끔합니다.

null 입력 처리와 필드 매핑이 명확합니다. 기존 DTO(ScheduleInfo, SemesterInfo)와도 일관된 스타일입니다.

src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/ScheduleInfo.java (1)

1-33: LGTM! 일관된 DTO 구조

다른 DTO 클래스들(SemesterInfo, EmotionInfo)과 일관된 구조를 가지고 있으며, null 안전성도 적절하게 처리되었습니다. Elasticsearch 필드 타입도 적절히 설정되었습니다.

src/main/java/inu/codin/codin/domain/lecture/service/LectureService.java (1)

77-85: 트랜잭션과 Elasticsearch 동기화 이슈

데이터베이스 트랜잭션 내에서 Elasticsearch 인덱스를 업데이트하는 것은 데이터 일관성 문제를 야기할 수 있습니다. 트랜잭션이 롤백되어도 Elasticsearch 업데이트는 롤백되지 않습니다.

다음 중 하나의 방법을 고려해보세요:

옵션 1: 트랜잭션 후 업데이트

-@Transactional
 public LectureDetailResponseDto getLectureDetails(Long lectureId) {
     Lecture lecture = lectureRepository.findLectureWithScheduleAndTagsById(lectureId)
             .orElseThrow(() -> new LectureException(LectureErrorCode.LECTURE_NOT_FOUND));
-    lecture.increaseHits();
+    
+    // 트랜잭션 분리
+    updateHits(lecture);
     Emotion emotion = emotionService.getOrMakeEmotion(lecture);
-    
-    lectureElasticService.incrementHits(lectureId);
     
     return LectureDetailResponseDto.of(lecture, emotion, userReviewStatsService.isOpenKeyword());
 }

+@Transactional
+private void updateHits(Lecture lecture) {
+    lecture.increaseHits();
+    // 트랜잭션 커밋 후 ES 업데이트
+    lectureElasticService.incrementHits(lecture.getId());
+}

옵션 2: 이벤트 기반 처리
Spring의 @TransactionalEventListener를 사용하여 트랜잭션 커밋 후 ES 업데이트를 수행하세요.

src/main/java/inu/codin/codin/domain/elasticsearch/convertor/LectureDocumentConverter.java (2)

52-80: LGTM! 일관된 null 안전성과 헬퍼 메서드 구조

모든 헬퍼 메서드들이 일관된 null 안전성 검사를 수행하고, 적절한 변환 로직을 구현했습니다. 코드의 가독성과 유지보수성이 크게 향상되었습니다.


44-50: 선수과목 문자열 파싱 포맷 확인 필요

  • toPreCourses(String preCourse)에서 사용 중인 preCourse.split(",\\s*")가 실제 DB에 저장된 preCourse 값 포맷(구분자, 공백 유무 등)과 일치하는지 확인해주세요.
  • DB에 저장된 preCourse 샘플 문자열을 직접 조회해,
    • 콤마 외에 세미콜론(;), 슬래시(/) 등 다른 구분자가 사용되는지
    • 연속 콤마(",,")나 선행·후행 공백 처리가 필요한 경우가 있는지
    • 빈 문자열, 특수문자 포함 케이스가 있는지
  • 다양한 케이스를 검증할 수 있는 단위 테스트 추가도 함께 고려 바랍니다.
src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryImpl.java (1)

38-49: LGTM! 깔끔한 Elasticsearch 통합

JPA 기반의 복잡한 쿼리 로직을 Elasticsearch로 위임하여 코드가 훨씬 간결해졌습니다. 빈 결과에 대한 처리도 적절합니다.

src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java (1)

73-85: ES 8에서 indexOptions = offsets는 허용됩니다 — 변경 불필요
ES 8 공식 문서에 따르면 Text 필드의 index_optionsoffsets 값을 지정해도 매핑 오류가 발생하지 않습니다.
따라서 아래 설정을 그대로 유지하셔도 무방합니다.

offsets는 문서 ID, 용어 빈도, 위치 정보, 시작/끝 오프셋을 모두 색인해 하이라이팅 기능을 지원합니다.
• 하이라이팅이 필요 없다면 기본값인 positions(문서 ID, 빈도, 위치)로 두어 저장 공간을 절약할 수 있습니다.

Likely an incorrect or invalid review comment.

Comment on lines 64 to 66
@Field(type = FieldType.Keyword)
private List<String> tags;

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

tags를 Keyword로만 매핑했는데 검색에서 multiMatch 대상으로 사용 → 검색 품질 저하 가능

현재 검색 쿼리에서 tags를 multiMatch로 조회합니다(서비스 파일 참조). Keyword 타입은 토큰화가 되지 않아 부분/형태소 매칭이 어려워 recall이 떨어질 수 있습니다. Text+Keyword 멀티 필드로 전환을 권장합니다.

아래와 같이 멀티필드로 전환해 주세요(한국어 형태소 분석을 원하는 경우).

-    @Field(type = FieldType.Keyword)
-    private List<String> tags;
+    @MultiField(
+        mainField = @Field(type = FieldType.Text, analyzer = "nori", searchAnalyzer = "nori"),
+        otherFields = { @InnerField(suffix = "keyword", type = FieldType.Keyword) }
+    )
+    private List<String> tags;

멀티필드를 도입하기 어렵다면, 검색 쿼리에서 tags를 multiMatch 대상에서 제외하고 정확 일치 기반(term/terms)으로만 사용해 주세요.

📝 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
@Field(type = FieldType.Keyword)
private List<String> tags;
@MultiField(
mainField = @Field(type = FieldType.Text, analyzer = "nori", searchAnalyzer = "nori"),
otherFields = { @InnerField(suffix = "keyword", type = FieldType.Keyword) }
)
private List<String> tags;
🤖 Prompt for AI Agents
In
src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java
around lines 64-66, the tags field is currently mapped only as Keyword which
prevents tokenization and hurts multiMatch recall; change tags to a multi-field
mapping with a text subfield (e.g., "tags": { "type":"keyword", "fields": {
"text": { "type":"text", "analyzer":"nori" } } }) so multiMatch queries can
target tags.text (use the appropriate Korean analyzer configured in your ES
setup), or if you cannot change the mapping, remove tags from multiMatch queries
and use term/terms exact-match queries for tags instead.

Comment on lines 62 to 69
Query boolQuery = QueryBuilders.bool(b -> {
if (keyword != null && !keyword.isBlank()) {
b.should(s -> s.multiMatch(mm -> mm
.fields("lectureNm^3", "tags^2", "professor", "type", "lectureType", "evaluation", "preCourses")
.query(keyword)
.operator(Operator.Or)
))
.should(s -> s.wildcard(w -> w
.field("lectureNm")
.value("*" + keyword + "*")
))
.should(s -> s.match(m -> m
.field("tags")
.query(keyword)
))
.minimumShouldMatch("1"); // 최소 매칭 갯수
b.must(m -> m.multiMatch(mm -> mm
.fields("lectureNm^3", "tags^2", "professor", "type", "lectureType", "evaluation")
.query(keyword)
.operator(Operator.Or)
));
} else {
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

multiMatch 대상 필드의 매핑 확인 필요(tags/type/lectureType/evaluation)

현재 multiMatch 대상에 "tags^2", "type", "lectureType", "evaluation"이 포함되어 있습니다. 이들 필드가 Keyword로 매핑되어 있으면 형태소/부분 매칭이 어려워 검색 품질이 저하됩니다. 최소한 tags는 Text(또는 Text+Keyword)로 매핑 전환이 필요합니다(문서 파일 코멘트 참조). 나머지 필드도 검색 요구사항에 따라 Text 멀티필드 도입을 검토해 주세요.


🏁 Script executed:

#!/bin/bash
# Locate Elasticsearch @Document definitions related to Lecture
doc_files=$(rg -l "@Document" --ignore-case | grep -i "lecture")
echo "Found document classes:"
echo "$doc_files"

# Inspect mapping annotations and field types in those files
for f in $doc_files; do
  echo "=== $f ==="
  rg -n "@Field" -A1 "$f"
  rg -n "private .*tags" -C1 "$f"
  rg -n "private .*type" -C1 "$f"
  rg -n "private .*lectureType" -C1 "$f"
  rg -n "private .*evaluation" -C1 "$f"
done

Length of output: 2914


LectureDocument 필드 매핑 개선 필요 (tags/type/lectureType/evaluation)

현재 LectureDocument의 아래 필드가 모두 FieldType.Keyword로 매핑되어 있어 멀티매치 검색 시 형태소 분석이나 부분 매칭이 불가능합니다.

  • tags (@Field(type = FieldType.Keyword) 라인 64–65)
  • type (@Field(type = FieldType.Keyword) 라인 40–41)
  • lectureType (@Field(type = FieldType.Keyword) 라인 43–44)
  • evaluation (@Field(type = FieldType.Keyword) 라인 46–47)

조치 사항:

  • tags 최소한 FieldType.Text(또는 Text+Keyword 멀티필드)로 매핑 전환
  • 검색 UX 요구사항에 따라 type, lectureType, evaluation에도 Text 멀티필드 도입 검토

대상 파일

  • src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java

Comment on lines 27 to 28
private LectureStartupIndexer indexer;

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

NPE 위험: indexer 필드가 주입되지 않습니다. final로 선언해 @requiredargsconstructor 주입을 활성화하세요.

현재 필드는 non-final이며 @Autowired도 없어 null 상태가 됩니다. line 44의 indexer.lectureIndex()에서 NPE가 발생합니다.

-    private LectureStartupIndexer indexer;
+    private final LectureStartupIndexer indexer;
📝 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
private LectureStartupIndexer indexer;
private final LectureStartupIndexer indexer;
🤖 Prompt for AI Agents
In
src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java
around lines 27-28, the indexer field is non-final and not injected (causing NPE
at line 44 when calling indexer.lectureIndex()); make the field final and enable
constructor injection by annotating the class with Lombok's
@RequiredArgsConstructor (or manually add a constructor that accepts the
indexer) so Spring injects it properly; alternatively, if you prefer field
injection, add @Autowired to the field, but preferred fix is to mark the field
final and use @RequiredArgsConstructor for safe injection.

Comment on lines 86 to 88
ProcessBuilder processBuilder = new ProcessBuilder(
PYTHON_DIR, UPLOAD_DIR + pythonNm, UPLOAD_DIR+ file.getOriginalFilename()
);
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

스크립트 경로 설정 확인 필요: 업로드 디렉토리(UPLOAD_DIR)에 스크립트가 존재합니까?

현재 UPLOAD_DIR + pythonNm로 스크립트 경로를 구성하고 있습니다. 일반적으로 스크립트는 업로드 폴더가 아닌 별도의 스크립트 디렉토리에 위치합니다. 설정 의도와 맞는지 점검 바랍니다. 의도된 구성이 아니라면 python.script.dir 등 별도 프로퍼티로 분리하는 것을 권장합니다.

다음 스크립트로 설정/사용 위치를 확인해 주세요:


🏁 Script executed:

#!/bin/bash
# 설정 키 사용/선언 여부 점검
rg -n "lecture\.file\.path|lecture\.python\.path" -A 3

# 스크립트 파일 존재 가능성 확인(레포지토리 내에 있는지)
fd -a "infoOfLecture.py" || true
fd -a "dayTimeOfRoom.py" || true

Length of output: 859


🏁 Script executed:

#!/bin/bash
# Display the LectureUploadService to understand context
sed -n '1,200p' src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java

Length of output: 3876


스크립트 경로 설정 검토 필요
현재 UPLOAD_DIR + pythonNm으로 스크립트를 찾도록 되어 있으나, 코드베이스 내에서 .py 파일(infoOfLecture.py, dayTimeOfRoom.py)이 발견되지 않습니다.
실제 런타임 환경에 업로드 디렉터리에 스크립트를 배치할 예정인지 확인해 주세요. 의도된 설정이 아니라면 스크립트 전용 디렉터리(예: lecture.script.path 또는 python.script.dir)를 별도 프로퍼티로 분리하는 것을 권장합니다.

점검 위치:

  • 설정 프로퍼티:
    • @Value("${lecture.file.path}") UPLOAD_DIR (LectureUploadService.java:21)
    • @Value("${lecture.python.path}") PYTHON_DIR (LectureUploadService.java:24)
  • 스크립트 호출부:
    • executeGetExitCode()new ProcessBuilder(PYTHON_DIR, UPLOAD_DIR + pythonNm, …) (LectureUploadService.java:86–88)
  • 코드베이스 내 스크립트 파일 미발견:
    • infoOfLecture.py, dayTimeOfRoom.py 검색 결과 없음
🤖 Prompt for AI Agents
In
src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java
around lines 86 to 88, the ProcessBuilder is assembling the python script path
as UPLOAD_DIR + pythonNm which assumes the .py scripts live in the upload
directory; confirm whether scripts will actually be deployed there and if not,
change the implementation to use a dedicated script directory property (e.g.
add/use @Value("${lecture.python.script.dir}") or similar) for PYTHON_SCRIPT_DIR
and build the command as new ProcessBuilder(PYTHON_DIR, PYTHON_SCRIPT_DIR + "/"
+ pythonNm, UPLOAD_DIR + file.getOriginalFilename()), update related
configuration keys and docs, and/or ensure the expected .py files are
committed/deployed to the chosen script directory so the runtime process can
locate them.

- LectureDocument likes, hits 필드 long -> Integer로 변경
- LectureElasticService searchLectureDocument() like NPE 방지
- LecturePreviewResponseDto Type 필드 NPE 방지
- LectureStartupIndexer
   - totalProcessed 오버플로우 방지
   - index refresh를 통한 가시성 확보
- LectureUploadService 에러 코드 혼재 사용 수정
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: 3

♻️ Duplicate comments (4)
src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java (1)

64-66: tags를 Keyword 단일 매핑 → 검색 multiMatch 대상이면 텍스트 서브필드 필요

검색에서 tags를 부분/형태소 매칭하려면 tokenization 가능한 text 서브필드가 필요합니다. 아래와 같이 멀티필드 적용을 고려해 주세요(필터/정렬은 tags.keyword 사용, 풀텍스트 검색은 tags 사용).

-    @Field(type = FieldType.Keyword)
-    private List<String> tags;
+    @MultiField(
+        mainField = @Field(type = FieldType.Text, analyzer = "nori", searchAnalyzer = "nori"),
+        otherFields = { @InnerField(suffix = "keyword", type = FieldType.Keyword) }
+    )
+    private List<String> tags;

변경 시 재색인이 필요합니다. 당장 멀티필드 도입이 어렵다면, 쿼리에서 tags를 multi_match 대상에서 제외하고 term/terms 기반의 정확 일치 필터만 사용해 주세요.

src/main/java/inu/codin/codin/domain/elasticsearch/indexer/LectureStartupIndexer.java (1)

64-64: 이전 리뷰 의견이 부분적으로만 반영됨

totalProcessedlong으로 변경한 것은 좋습니다. 하지만 refresh() 호출의 오류 처리가 여전히 누락되어 있습니다.

refresh 실패 시 로깅 추가:

-        elasticsearchOperations.indexOps(LectureDocument.class).refresh();
+        try {
+            elasticsearchOperations.indexOps(LectureDocument.class).refresh();
+        } catch (Exception e) {
+            log.warn("Elasticsearch refresh 실패, 검색 가시성이 지연될 수 있습니다: {}", e.getMessage());
+        }
src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java (2)

85-88: Python 스크립트 경로 설정 검증 필요

이전 리뷰에서 지적된 대로, 스크립트 파일이 업로드 디렉토리에 위치하는 것이 의도된 설계인지 확인이 필요합니다.

스크립트 파일 존재 여부와 경로 설정을 확인하겠습니다:

#!/bin/bash
# Python 스크립트 파일 검색
echo "=== Python 스크립트 파일 검색 ==="
fd -e py "infoOfLecture|dayTimeOfRoom"

# 설정 파일에서 경로 설정 확인
echo -e "\n=== application 설정 파일 검색 ==="
fd -e yml -e yaml -e properties | xargs rg -l "lecture\.(file|python)\.path" | head -5

# 설정 값 확인
echo -e "\n=== 경로 설정 값 확인 ==="
fd -e yml -e yaml -e properties | xargs rg "lecture\.(file|python)\.path" -A 1 -B 1

46-46: 로깅 방식 개선됨

이전 리뷰에서 지적된 대로 log.error(e.getMessage(), e)로 수정되어 전체 스택 트레이스가 로그에 기록됩니다. 단, 65번 라인은 여전히 수정이 필요합니다.

-            log.error(e.getMessage(), e.getStackTrace()[0]);
+            log.error(e.getMessage(), e);

Also applies to: 65-65

🧹 Nitpick comments (5)
src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java (4)

86-93: aiSummary store=true는 저장소 비용 증가 — 필요 시에만 활성화 권장

store=true는 _source와 별개로 필드를 저장해 디스크 사용량을 늘립니다. 조회에서 필드 저장(store)을 직접 사용하지 않는다면 제거해도 됩니다(일반적으로 _source에서 충분).

-    @Field(type = FieldType.Text,
-            analyzer = "standard",
-            searchAnalyzer = "standard",
-            indexOptions = IndexOptions.positions,
-            termVector = TermVector.no,
-            store = true)
+    @Field(type = FieldType.Text,
+            analyzer = "standard",
+            searchAnalyzer = "standard",
+            indexOptions = IndexOptions.positions,
+            termVector = TermVector.no)

필요하다면 나중에 field retrieval 경로에서만 선택적으로 store를 켜는 것을 고려해 주세요.


37-48: Enum 기반 필드의 Keyword 매핑 합리적(type/department/evaluation 등)

정확 일치 필터 및 정렬에 적합합니다. 쿼리 설계에서 소문자 정규화가 필요하면 normalizer 도입을 검토해 주세요.


61-63: Nested 타입 사용 여부 재검토 필요 (semesters / schedule)
Nested 타입은 강력하지만 쿼리 비용이 높습니다. 중첩 문서 단위의 join-like 매칭이 실제로 필요하다면 적합하지만, 단순 필터/집계만 필요하다면 object 타입으로도 충분할 수 있습니다.
현재 코드베이스에서 NestedQueryNestedQueryBuilder 등을 통해 해당 필드로 중첩 쿼리를 사용하는 흔적이 보이지 않습니다. 실제 쿼리 설계 및 사용 계획을 다시 한번 확인해 주세요.

– 검토 대상 위치
• src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java
– Lines 61–63 (semesters)
– Lines 67–69 (schedule)


31-32: 인덱스 재생성 & 매핑 로직 확인, Alias 기반 마이그레이션 전략 검토 필요

아래 위치에서 인덱스 삭제→생성→매핑 및 재색인 로직이 구현되어 있습니다.

  • LectureStartupIndexer
    (src/main/java/inu/codin/codin/domain/elasticsearch/indexer/LectureStartupIndexer.java:48–56)
    • indexOps.exists() → indexOps.delete()
    • indexOps.create() → indexOps.putMapping(...)
    • lectureElasticRepository.saveAll(...) → indexOps.refresh()

인덱스 설정(커스텀 분석기 포함)은 application.yml에 정의되어 있습니다.

  • src/main/resources/application.yml:35–40, 51–54 (lectures.settings.analysis, analyzer 등)

다만, alias 생성/전환(reindex API, rollover) 또는 versioned index 명명 전략은 코드베이스에서 확인되지 않습니다.
무중단 마이그레이션이 필요하다면 alias 전환 전략을 추가 구현하거나,
downtime 재색인이 허용되는지 여부를 검토 후 회신 부탁드립니다.

src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java (1)

71-83: 파일 저장 로직 개선됨

try-with-resources를 사용하여 리소스 관리가 개선되었습니다.

파일 덮어쓰기 전 기존 파일 백업을 고려하세요:

     private void saveFile(MultipartFile file) {
         String originalName = file.getOriginalFilename();
         if (originalName == null || originalName.isBlank()) {
             throw new LectureUploadException(LectureErrorCode.FILE_READ_FAIL, "파일 이름이 비어있습니다.");
         }
 
         File savedFile = new File(UPLOAD_DIR + file.getOriginalFilename());
+        if (savedFile.exists()) {
+            File backup = new File(UPLOAD_DIR + "backup_" + System.currentTimeMillis() + "_" + originalName);
+            savedFile.renameTo(backup);
+            log.info("기존 파일 백업: {}", backup.getName());
+        }
         try (FileOutputStream fos = new FileOutputStream(savedFile)) {
             fos.write(file.getBytes());
         } catch (IOException e) {
             throw new LectureUploadException(LectureErrorCode.FILE_READ_FAIL, e.getMessage());
         }
     }
📜 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 settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d6a00a8 and 8781f7d.

📒 Files selected for processing (5)
  • src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java (3 hunks)
  • src/main/java/inu/codin/codin/domain/elasticsearch/indexer/LectureStartupIndexer.java (2 hunks)
  • src/main/java/inu/codin/codin/domain/elasticsearch/service/LectureElasticService.java (3 hunks)
  • src/main/java/inu/codin/codin/domain/lecture/dto/LecturePreviewResponseDto.java (2 hunks)
  • src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/inu/codin/codin/domain/elasticsearch/service/LectureElasticService.java
  • src/main/java/inu/codin/codin/domain/lecture/dto/LecturePreviewResponseDto.java
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java (2)
src/main/java/inu/codin/codin/global/auth/filter/TokenValidationFilter.java (1)
  • RequiredArgsConstructor (21-65)
src/main/java/inu/codin/codin/domain/lecture/service/LectureService.java (1)
  • Service (32-128)
src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java (5)
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/EmotionInfo.java (1)
  • Getter (9-32)
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/SemesterInfo.java (1)
  • Getter (10-27)
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/ScheduleInfo.java (1)
  • Getter (9-33)
src/main/java/inu/codin/codin/domain/lecture/entity/Lecture.java (1)
  • Getter (16-89)
src/main/java/inu/codin/codin/domain/elasticsearch/event/LectureSavedEvent.java (1)
  • Getter (7-11)
🔇 Additional comments (6)
src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java (5)

55-60: likes/hits 타입 정합성 확보: Integer 매핑으로 수정된 점 좋습니다

이전 매핑(Long) ↔ 자바 필드(Integer) 불일치 이슈가 해소되어 정렬/집계 시 충돌 리스크가 줄었습니다.


73-85: syllabus 멀티필드 + offsets/termVector 설정 적절

하이라이팅(FVH) 및 구간 기반 스니펫 추출을 고려한 indexOptions=offsets와 termVector 설정이 합리적입니다.


31-32: credit(Integer) 필드 추가 적절

도메인과 매핑이 일치하며 정렬/필터에 활용 가능해 보입니다.


49-51: 확인 완료: preCourse → preCourses 변환 로직이 안전하게 처리되고 있습니다
LectureDocumentConverter#toPreCourses 메서드에서

  • preCoursenull 또는 빈 문자열일 때 빈 리스트 반환
  • 쉼표+공백(",\s*") 기준으로 안전하게 토큰 분리

로직이 요구사항에 부합하므로 추가 수정은 불필요합니다.


25-26: nori_autocomplete 분석기 설정 확인됨

application.yml(56–59행)에 이미 nori_autocomplete 커스텀 분석기가 정의되어 있어, 해당 Analyzer를 사용하는 인덱스 생성 시점에 설정이 적용됩니다.

• 설정 위치

  • src/main/resources/application.yml:56–59
src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java (1)

27-28: 의존성 주입이 올바르게 설정되었습니다

이전 리뷰에서 지적된 NPE 위험이 final 필드와 @RequiredArgsConstructor를 통해 해결되었습니다.

Comment on lines +34 to +45
public void lectureIndex() {
if (!indexerEnabled) {
log.info("ElasticSearch indexer 사용할 수 없습니다.");

return;
}

// 인덱스 존재 여부 확인 및 생성
manageIndex();
long totalProcessed = performIndexing();

log.info("ElasticSearch 인덱싱 성공. Total Indexed: {}", totalProcessed);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

인덱싱 오류 처리 보완 필요

lectureIndex() 메서드는 public으로 선언되어 외부(LectureUploadService)에서 호출되지만, Elasticsearch 연결 실패나 인덱싱 중 예외가 발생할 경우 호출자에게 전파되지 않고 있습니다. 강의 업로드는 성공했지만 인덱싱만 실패한 경우 사용자가 이를 인지하지 못할 수 있습니다.

예외를 명시적으로 처리하거나 호출자에게 전파하도록 수정:

-    public void lectureIndex() {
+    public void lectureIndex() throws ElasticsearchException {
         if (!indexerEnabled) {
             log.info("ElasticSearch indexer 사용할 수 없습니다.");
             return;
         }
 
-        manageIndex();
-        long totalProcessed = performIndexing();
-
-        log.info("ElasticSearch 인덱싱 성공. Total Indexed: {}", totalProcessed);
+        try {
+            manageIndex();
+            long totalProcessed = performIndexing();
+            log.info("ElasticSearch 인덱싱 성공. Total Indexed: {}", totalProcessed);
+        } catch (Exception e) {
+            log.error("ElasticSearch 인덱싱 실패: {}", e.getMessage(), e);
+            throw new ElasticsearchException("인덱싱 실패", e);
+        }
     }
📝 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
public void lectureIndex() {
if (!indexerEnabled) {
log.info("ElasticSearch indexer 사용할 수 없습니다.");
return;
}
// 인덱스 존재 여부 확인 및 생성
manageIndex();
long totalProcessed = performIndexing();
log.info("ElasticSearch 인덱싱 성공. Total Indexed: {}", totalProcessed);
}
public void lectureIndex() throws ElasticsearchException {
if (!indexerEnabled) {
log.info("ElasticSearch indexer 사용할 수 없습니다.");
return;
}
try {
manageIndex();
long totalProcessed = performIndexing();
log.info("ElasticSearch 인덱싱 성공. Total Indexed: {}", totalProcessed);
} catch (Exception e) {
log.error("ElasticSearch 인덱싱 실패: {}", e.getMessage(), e);
throw new ElasticsearchException("인덱싱 실패", e);
}
}
🤖 Prompt for AI Agents
In
src/main/java/inu/codin/codin/domain/elasticsearch/indexer/LectureStartupIndexer.java
around lines 34 to 45, lectureIndex() swallows any exceptions from
manageIndex()/performIndexing() and will not notify the caller when
Elasticsearch is unavailable; change the method to either declare and rethrow a
checked exception (or a custom domain-specific exception) or rethrow the caught
runtime exception after logging so the caller (LectureUploadService) can react;
implement a try/catch that logs the error with details and then throws a
meaningful exception (or add throws to the method signature) so failures are
propagated, and keep the indexerEnabled early-return but ensure any unexpected
exceptions do not get swallowed.

Comment on lines +47 to +58
private void manageIndex() {
IndexOperations indexOps = elasticsearchOperations.indexOps(LectureDocument.class);
if (!indexOps.exists()) {
log.info("ElasticSearch lectures 인덱스가 존재하지 않아 직접 생성합니다.");
indexOps.create();
indexOps.putMapping(indexOps.createMapping(LectureDocument.class));
log.info("ElasticSearch lectures 인덱스 및 매핑 생성 완료.");
if (indexOps.exists()) {
log.info("ElasticSearch lectures 문서 전체 재색인을 위해 인덱스를 삭제합니다.");
indexOps.delete();
}

long existingCount = lectureElasticRepository.count();
if (existingCount > 0) {
log.info("ElasticSearch 인덱스가 이미 존재합니다 ({}개), 전체 재인덱싱을 진행합니다.", existingCount);
// 선택적으로 기존 인덱스 삭제 후 재인덱싱
lectureElasticRepository.deleteAll();
log.info("기존 인덱스를 삭제했습니다.");
}
log.info("ElasticSearch lectures 인덱스를 생성합니다.");
indexOps.create();
indexOps.putMapping(indexOps.createMapping(LectureDocument.class));
log.info("ElasticSearch lectures 인덱스 및 매핑 생성 완료.");
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

기존 데이터를 모두 삭제하는 위험한 동작

manageIndex() 메서드가 무조건 기존 인덱스를 삭제하고 재생성하는 구조입니다. 이는 강의 업로드 시마다 전체 검색 데이터가 초기화되어 서비스 중단이 발생할 수 있습니다.

인덱스 삭제 대신 증분 업데이트나 별칭(alias)를 활용한 무중단 재색인을 고려하세요:

 private void manageIndex() {
     IndexOperations indexOps = elasticsearchOperations.indexOps(LectureDocument.class);
-    if (indexOps.exists()) {
-        log.info("ElasticSearch lectures 문서 전체 재색인을 위해 인덱스를 삭제합니다.");
-        indexOps.delete();
-    }
-
-    log.info("ElasticSearch lectures 인덱스를 생성합니다.");
-    indexOps.create();
-    indexOps.putMapping(indexOps.createMapping(LectureDocument.class));
-    log.info("ElasticSearch lectures 인덱스 및 매핑 생성 완료.");
+    if (!indexOps.exists()) {
+        log.info("ElasticSearch lectures 인덱스를 생성합니다.");
+        indexOps.create();
+        indexOps.putMapping(indexOps.createMapping(LectureDocument.class));
+        log.info("ElasticSearch lectures 인덱스 및 매핑 생성 완료.");
+    } else {
+        log.info("기존 ElasticSearch lectures 인덱스를 사용합니다.");
+    }
 }
📝 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
private void manageIndex() {
IndexOperations indexOps = elasticsearchOperations.indexOps(LectureDocument.class);
if (!indexOps.exists()) {
log.info("ElasticSearch lectures 인덱스가 존재하지 않아 직접 생성합니다.");
indexOps.create();
indexOps.putMapping(indexOps.createMapping(LectureDocument.class));
log.info("ElasticSearch lectures 인덱스 및 매핑 생성 완료.");
if (indexOps.exists()) {
log.info("ElasticSearch lectures 문서 전체 재색인을 위해 인덱스를 삭제합니다.");
indexOps.delete();
}
long existingCount = lectureElasticRepository.count();
if (existingCount > 0) {
log.info("ElasticSearch 인덱스가 이미 존재합니다 ({}개), 전체 재인덱싱을 진행합니다.", existingCount);
// 선택적으로 기존 인덱스 삭제 후 재인덱싱
lectureElasticRepository.deleteAll();
log.info("기존 인덱스를 삭제했습니다.");
}
log.info("ElasticSearch lectures 인덱스를 생성합니다.");
indexOps.create();
indexOps.putMapping(indexOps.createMapping(LectureDocument.class));
log.info("ElasticSearch lectures 인덱스 및 매핑 생성 완료.");
}
private void manageIndex() {
IndexOperations indexOps = elasticsearchOperations.indexOps(LectureDocument.class);
if (!indexOps.exists()) {
log.info("ElasticSearch lectures 인덱스를 생성합니다.");
indexOps.create();
indexOps.putMapping(indexOps.createMapping(LectureDocument.class));
log.info("ElasticSearch lectures 인덱스 및 매핑 생성 완료.");
} else {
log.info("기존 ElasticSearch lectures 인덱스를 사용합니다.");
}
}
🤖 Prompt for AI Agents
In
src/main/java/inu/codin/codin/domain/elasticsearch/indexer/LectureStartupIndexer.java
around lines 47 to 58, the manageIndex() method always deletes and recreates the
index which wipes existing data; change it to avoid unconditional index deletion
by: check if the index exists and if so only update mappings via
indexOps.putMapping(...) when compatible, otherwise implement a zero-downtime
reindex flow (create a new versioned index, index new/updated documents or call
Elasticsearch Reindex API, then switch an alias to the new index); if you need
to recreate because of incompatible mapping, create a new index name (e.g.,
append timestamp/version), populate it, then atomically swap the alias instead
of deleting the live index.

log.info("[uploadNewSemesterRooms] {} 강의실 현황 업데이트 완료", file.getName());
log.info("[uploadNewSemesterLectures] {} 학기 강의 정보 업로드 완료", file.getName());

indexer.lectureIndex();
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

인덱싱 실패 시 트랜잭션 롤백 고려 필요

강의 데이터는 DB에 저장되었지만 Elasticsearch 인덱싱이 실패할 경우, 데이터 불일치가 발생할 수 있습니다. 검색 결과와 실제 데이터가 다를 수 있습니다.

인덱싱 실패를 처리하거나 트랜잭션 범위를 확장하세요:

+    @Transactional
     public void uploadNewSemesterLectures(MultipartFile file){
         try {
             saveFile(file);
             int exitCode = executeGetExitCode(file, LECTURE_PROGRAM);
 
             if (exitCode != 0) {
                 log.error("[uploadNewSemesterLectures] {} 업로드 실패", file.getOriginalFilename());
                 throw new LectureUploadException(LectureErrorCode.LECTURE_UPLOAD_FAIL, exitCode);
             }
             log.info("[uploadNewSemesterLectures] {} 학기 강의 정보 업로드 완료", file.getName());
 
-            indexer.lectureIndex();
+            try {
+                indexer.lectureIndex();
+            } catch (Exception e) {
+                log.error("Elasticsearch 인덱싱 실패, 데이터 롤백 필요: {}", e.getMessage());
+                throw new LectureUploadException(LectureErrorCode.INDEXING_FAIL, "인덱싱 실패");
+            }
         } catch (IOException | InterruptedException e) {

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java
around line 44, calling indexer.lectureIndex() outside of the DB transaction
risks DB/Elasticsearch inconsistency when indexing fails; modify the flow so
indexing is part of a coordinated operation: either (a) perform indexing within
the same transactional boundary by deferring commit until indexing succeeds
(e.g., wrap DB save + index in a single service method and only commit after
index success), or (b) if indexing must be async, implement reliable
compensation and retry (store pending index records or publish an event and
retry with dead-letter handling) and ensure failures roll back the DB save or
mark the entity as not-indexed for later reconciliation; also add
logging/metrics for indexing failures and test the failure path to confirm
consistency.

Copy link
Member

@doma17 doma17 left a comment

Choose a reason for hiding this comment

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

수고하셨습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants