Skip to content

Feature/booth finally - 백엔드 마지막 - #49

Merged
suho-98 merged 10 commits into
devfrom
feature/booth-finally
Aug 10, 2026
Merged

suho-98 merged 10 commits into
devfrom
feature/booth-finally

Conversation

@suho-98

@suho-98 suho-98 commented Aug 10, 2026 •

Copy link
Copy Markdown
Collaborator

📄 작업 내용

무엇을, 왜 변경했는지 설명해주세요.

🔗 관련 이슈

Closes #

☑️ 체크리스트

  • 로컬에서 정상 동작 확인
  • 테스트 작성 및 통과
  • 불필요한 로그/주석/커밋 정리

📸 스크린샷 (UI 변경 시)

💬 리뷰어에게

Summary by CodeRabbit

  • 새로운 기능
    • 모바일 이벤트 안내에서 부스 목록, 검색, 상세 정보를 확인할 수 있습니다.
    • 부스 추천 목록과 실시간 혼잡도 정보를 제공합니다.
    • 교환 코드로 부스 방문을 체크인할 수 있습니다.
    • 부스 예약의 임시 선점과 빈자리 알림을 지원합니다.
    • 부스 후기 작성·수정·삭제, 검색 및 평균 평점 조회를 지원합니다.
    • 부스 신청, 승인·반려 확인 및 첨부파일 관리를 제공합니다.
    • 공지·자료 열람과 파일 다운로드를 지원합니다.
    • 행사 운영 현황, 인기 부스 및 통계 대시보드를 제공합니다.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c47a6a28-892b-4715-92b8-251489a72443

📥 Commits

Reviewing files that changed from the base of the PR and between 2bf58d0 and ff994ce.

📒 Files selected for processing (46)
  • BE/build.gradle
  • BE/src/main/java/com/min/edu/application/service/BoothApplicationDecision.java
  • BE/src/main/java/com/min/edu/application/service/BoothApplicationNotificationListener.java
  • BE/src/main/java/com/min/edu/application/service/BoothApplicationService.java
  • BE/src/main/java/com/min/edu/booth/config/AsyncConfiguration.java
  • BE/src/main/java/com/min/edu/booth/controller/BoothReservationController.java
  • BE/src/main/java/com/min/edu/booth/controller/BoothReviewController.java
  • BE/src/main/java/com/min/edu/booth/domain/BoothReservation.java
  • BE/src/main/java/com/min/edu/booth/domain/BoothReview.java
  • BE/src/main/java/com/min/edu/booth/dto/BoothReviewResponse.java
  • BE/src/main/java/com/min/edu/booth/dto/UpdateBoothReviewRequest.java
  • BE/src/main/java/com/min/edu/booth/event/BoothVacancyEvent.java
  • BE/src/main/java/com/min/edu/booth/event/BoothVacancyEventListener.java
  • BE/src/main/java/com/min/edu/booth/repository/BoothQrScanRepository.java
  • BE/src/main/java/com/min/edu/booth/repository/BoothReservationRepository.java
  • BE/src/main/java/com/min/edu/booth/repository/BoothReservationSlotRepository.java
  • BE/src/main/java/com/min/edu/booth/service/BoothCheckInService.java
  • BE/src/main/java/com/min/edu/booth/service/BoothRecommendationService.java
  • BE/src/main/java/com/min/edu/booth/service/BoothReservationWithRedisService.java
  • BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java
  • BE/src/main/java/com/min/edu/booth/service/BoothVacancyNotificationService.java
  • BE/src/main/java/com/min/edu/event/controller/EventContentController.java
  • BE/src/main/java/com/min/edu/event/dto/EventContentDtos.java
  • BE/src/main/java/com/min/edu/event/repository/EventRepository.java
  • BE/src/main/java/com/min/edu/event/service/EventContentService.java
  • BE/src/main/java/com/min/edu/notification/domain/NotificationType.java
  • BE/src/main/java/com/min/edu/notification/repository/NotificationRepository.java
  • BE/src/main/resources/application.properties
  • FE/src/api/boothApplicationApi.js
  • FE/src/api/contentApi.js
  • FE/src/api/fileApi.js
  • FE/src/api/statisticsApi.js
  • FE/src/components/FileDownloadLink.jsx
  • FE/src/components/FileUploadField.jsx
  • FE/src/components/TopNav.jsx
  • FE/src/main.jsx
  • FE/src/notifications/notificationPresentation.js
  • FE/src/pages/BoothApplicationList.jsx
  • FE/src/pages/BoothApply.jsx
  • FE/src/pages/EventDetail.jsx
  • FE/src/pages/MyPage.jsx
  • FE/src/pages/Notices.jsx
  • FE/src/pages/OrganizerAdmin.jsx
  • FE/src/pages/RecruitmentDetail.jsx
  • k8s/backend.yaml
  • k8s/frontend.yaml
💤 Files with no reviewable changes (1)
  • BE/src/main/java/com/min/edu/booth/repository/BoothReservationRepository.java
🚧 Files skipped from review as they are similar to previous changes (6)
  • BE/src/main/java/com/min/edu/booth/dto/UpdateBoothReviewRequest.java
  • BE/src/main/java/com/min/edu/booth/controller/BoothReservationController.java
  • BE/src/main/java/com/min/edu/booth/service/BoothRecommendationService.java
  • BE/src/main/java/com/min/edu/booth/controller/BoothReviewController.java
  • BE/src/main/java/com/min/edu/booth/service/BoothReservationWithRedisService.java
  • BE/src/main/java/com/min/edu/booth/service/BoothCheckInService.java

Walkthrough

부스 예약·체크인·후기·안내 기능을 추가했다. 참가 신청과 관리자 심사 흐름을 API 기반으로 구현했다. 행사 공지·자료와 첨부파일 관리 기능을 백엔드와 프론트엔드에 연결했다.

Changes

부스 플랫폼

Layer / File(s) Summary
예약·체크인·후기 흐름
BE/src/main/java/com/min/edu/booth/controller/*, BE/src/main/java/com/min/edu/booth/service/*, BE/src/main/java/com/min/edu/booth/repository/*, BE/src/main/java/com/min/edu/booth/domain/*
Redis 선점 예약, 예약 취소, 교환 코드 체크인, QR 스캔 기록, 후기 CRUD 및 평점 집계를 추가했다.
모바일 안내·추천·혼잡도
BE/src/main/java/com/min/edu/booth/dto/*, BE/src/main/java/com/min/edu/booth/controller/*Guide*, BE/src/main/java/com/min/edu/booth/service/*Guide*, BE/src/main/java/com/min/edu/booth/service/*Recommendation*, BE/src/main/java/com/min/edu/booth/service/*Congestion*
이벤트별 부스 안내, 추천 목록, 평면도 혼잡도 마커 조회를 추가했다.
빈자리 알림
BE/src/main/java/com/min/edu/booth/event/*, BE/src/main/java/com/min/edu/booth/service/BoothVacancyNotificationService.java, BE/src/main/java/com/min/edu/interest/*
예약 취소 커밋 후 비동기 빈자리 알림을 멱등적으로 저장한다.

부스 신청 플랫폼

Layer / File(s) Summary
신청 심사 이벤트
BE/src/main/java/com/min/edu/application/service/*, BE/src/main/java/com/min/edu/notification/*
신청 승인·반려 결과를 이벤트로 발행하고 사이트 알림과 이메일을 커밋 후 발송한다.
신청 제출·조회·관리
FE/src/api/boothApplicationApi.js, FE/src/pages/BoothApply.jsx, FE/src/pages/BoothApplicationList.jsx, FE/src/pages/OrganizerAdmin.jsx
실제 신청 API, 파일 검증·업로드, 참가기업 신청 현황, 관리자 심사 및 통계 화면을 구현했다.

행사 콘텐츠 플랫폼

Layer / File(s) Summary
콘텐츠 API와 파일 메타데이터
BE/src/main/java/com/min/edu/event/controller/*, BE/src/main/java/com/min/edu/event/dto/*, BE/src/main/java/com/min/edu/event/service/*, FE/src/api/contentApi.js
공개 공지·자료 목록과 콘텐츠별 파일명·크기 응답을 추가했다.
공지·자료 화면과 파일 처리
FE/src/pages/Notices.jsx, FE/src/pages/EventDetail.jsx, FE/src/components/FileDownloadLink.jsx, FE/src/components/FileUploadField.jsx, FE/src/pages/RecruitmentDetail.jsx
공지·자료 조회, 콘텐츠 관리, 첨부파일 다운로드, 모집 페이지 연결을 추가했다.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant 참가기업
  participant BoothApply
  participant boothApplicationApi
  participant BoothApplicationService
  participant BoothApplicationNotificationListener
  참가기업->>BoothApply: 신청 정보와 첨부파일 제출
  BoothApply->>boothApplicationApi: submitApplication
  boothApplicationApi->>BoothApplicationService: 신청 API 요청
  BoothApplicationService-->>BoothApplicationNotificationListener: 승인·반려 이벤트 발행
  BoothApplicationNotificationListener-->>참가기업: 사이트 알림과 이메일 발송
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning 템플릿의 제목과 체크박스만 있고 작업 내용, 이슈, 검증 결과, 스크린샷 및 리뷰어 안내가 작성되지 않았습니다. 변경 내용과 목적, 관련 이슈 번호, 로컬 검증 및 테스트 결과, UI 스크린샷 여부, 리뷰어 안내를 작성하고 체크리스트를 갱신하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 74.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed 부스 백엔드 기능 추가라는 실제 변경 사항을 언급하지만, 전체 변경 범위와 핵심 내용을 충분히 구체화하지는 않습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/booth-finally

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Trivy (0.72.0)

Trivy execution failed: 2026-08-10T18:22:35Z FATAL Fatal error run error: fs scan error: scan error: scan failed: failed analysis: post analysis error: post analysis error: ansible scan error: fs filter error: fs filter error: walk error range error: stat .coderabbit-opengrep-fallback.0b87124f-7a7d-4477-9716-b9ee4bd54bf4.yml: no such file or directory: range error: stat .coderabbit-opengrep-fallback.0b87124f-7a7d-4477-9716-b9ee4bd54bf4.yml: no such file or directory


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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 17

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (11)
BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java-68-73 (1)

68-73: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

리뷰가 없을 때 0.0을 반환하면 실제 평점과 구분되지 않습니다.

BoothAverageRatingResponse의 주석은 "null이면 리뷰 없음"이라고 정의합니다. 그런데 여기서는 orElse(0.0)으로 0.0을 반환합니다. 클라이언트는 "리뷰 없음"과 "평점 0점"을 구분할 수 없고, 별점 UI가 0개로 표시됩니다. 리뷰 개수(countByBoothId)를 함께 담은 응답으로 바꾸거나, null을 그대로 내려주는 쪽으로 계약을 정해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java` around
lines 68 - 73, Update BoothReviewService.getAverageRating to preserve the
documented null value when no reviews exist instead of converting an empty
Optional to 0.0, or change the service response contract to include
countByBoothId and distinguish no reviews from a zero rating; keep
BoothAverageRatingResponse behavior consistent with the chosen contract.
BE/src/main/java/com/min/edu/booth/dto/BoothReviewResponse.java-16-21 (1)

16-21: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

수정 시각이 응답에 전달되지 않습니다. 이번 변경에서 BoothReview.updatedAt 컬럼과 수정 API를 추가했지만, 응답 DTO에 필드가 없고 변환 메서드에도 매핑이 없습니다. 그 결과 클라이언트는 수정 성공 후에도 갱신 시각을 알 수 없습니다.

  • BE/src/main/java/com/min/edu/booth/dto/BoothReviewResponse.java#L16-L21: OffsetDateTime updatedAt 필드를 추가합니다.
  • BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java#L171-L179: toResponse에 .updatedAt(review.getUpdatedAt())를 추가합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/dto/BoothReviewResponse.java` around lines
16 - 21, 수정 시각이 응답에 포함되도록 BoothReviewResponse에 OffsetDateTime updatedAt 필드를
추가하고, BoothReviewService의 toResponse 변환에서 review.getUpdatedAt()을 해당 필드에 매핑하세요.
변경 대상은 BE/src/main/java/com/min/edu/booth/dto/BoothReviewResponse.java 16-21 및
BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java 171-179입니다.
BE/src/main/java/com/min/edu/booth/dto/CreateBoothReviewRequest.java-17-22 (1)

17-22: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

comment 길이 검증을 추가해 주세요.

BoothReview.comment 컬럼은 length = 300입니다. 지금은 DTO에 길이 제약이 없어서 301자 이상이 들어오면 DB 제약 위반으로 넘어갑니다. createReview가 DataIntegrityViolationException을 잡아 INVALID_INPUT_VALUE로 바꾸긴 하지만, 중복 리뷰 오류와 길이 초과 오류가 같은 코드로 내려가서 클라이언트가 원인을 구분할 수 없습니다. 입력 단계에서 막는 편이 좋습니다.

🛠️ 제안 수정
+import jakarta.validation.constraints.Size;
     `@NotNull`(message = "별점은 필수입니다")
     `@Min`(value = 1, message = "별점은 1 이상이어야 합니다")
     `@Max`(value = 5, message = "별점은 5 이하여야 합니다")
     private Short rating;
 
-    private String comment;  // 리뷰 텍스트 (선택사항)
+    `@Size`(max = 300, message = "리뷰는 300자 이하여야 합니다")
+    private String comment;  // 리뷰 텍스트 (선택사항)

위 지적은 경로 지침의 "인증, 권한, 입력값 검증 등 보안 문제" 항목에 따른 것입니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/dto/CreateBoothReviewRequest.java` around
lines 17 - 22, Update CreateBoothReviewRequest.comment with a validation
constraint limiting its length to the BoothReview column’s 300-character
maximum, while keeping it optional so null or omitted comments remain valid.

Source: Path instructions

BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java-107-116 (1)

107-116: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

keyword를 검증해 주세요.

keyword가 null이면 findByBoothIdAndCommentContainingIgnoreCase에 null이 전달됩니다. 파생 쿼리는 LIKE에 null 파라미터를 바인딩하고, 그 결과는 항상 빈 페이지입니다. 실패 원인이 드러나지 않습니다. 빈 문자열이면 전체 조회와 같아져 인덱스 없는 풀 스캔이 됩니다. 서비스 진입 시점에 null/공백을 걸러 주세요.

🛠️ 제안 수정
     public Page<BoothReviewResponse> searchReviews(
             Long boothId, String keyword, Pageable pageable) {
 
+        if (keyword == null || keyword.isBlank()) {
+            throw new BusinessException(GlobalErrorCode.INVALID_INPUT_VALUE);
+        }
+
         // 1) 키워드로 리뷰 검색

위 지적은 경로 지침의 "인증, 권한, 입력값 검증 등 보안 문제" 항목에 따른 것입니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java` around
lines 107 - 116, Update BoothReviewService.searchReviews to validate keyword
before calling
boothReviewRepository.findByBoothIdAndCommentContainingIgnoreCase: reject null,
empty, and whitespace-only values at service entry using the project’s
established validation/exception convention, and preserve the repository query
path only for meaningful keywords.

Source: Path instructions

BE/src/main/java/com/min/edu/booth/dto/BoothAverageRatingResponse.java-14-16 (1)

14-16: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

사용하지 않는 DTO를 제거하거나 평균 별점 API에 적용해 주세요.

BoothAverageRatingResponse는 선언과 미사용 import 외에 참조되지 않습니다. 현재 API는 Double만 반환하며, 리뷰가 없으면 0.0을 반환합니다. DTO를 사용할 경우 averageRating의 null 규칙과 서비스의 0.0 규칙을 통일하고 reviewCount도 함께 반환해 주세요. 사용하지 않을 경우 DTO와 import를 제거해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/dto/BoothAverageRatingResponse.java`
around lines 14 - 16, BoothAverageRatingResponse가 미사용 상태이므로 DTO와 관련 미사용 import를
제거하거나 평균 별점 API의 반환 타입으로 적용하세요. 적용할 경우 평균 별점 API와 서비스의 반환 규칙을 통일해 리뷰 없음 상태의
averageRating을 일관되게 처리하고 reviewCount도 함께 반환하도록 BoothAverageRatingResponse 사용 지점을
수정하세요.
BE/src/main/resources/db/migration/V14__Add_updated_at_to_booth_reviews.sql-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

updated_at을 TIMESTAMPTZ로 변경하고 기존 행을 created_at으로 백필해 주세요.

BoothReview의 두 필드는 OffsetDateTime이며, created_at과 기존 updated_at 컬럼은 TIMESTAMPTZ입니다. 현재 TIMESTAMP는 애플리케이션 매핑 및 스키마 검증과 일치하지 않습니다.

DEFAULT CURRENT_TIMESTAMP는 기존 리뷰의 updated_at을 마이그레이션 시각으로 설정합니다. 컬럼을 nullable로 추가한 뒤 created_at으로 백필하고 NOT NULL을 설정해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/resources/db/migration/V14__Add_updated_at_to_booth_reviews.sql`
at line 1, Update the migration statement to add updated_at as TIMESTAMPTZ
without an immediate default or NOT NULL constraint, backfill existing rows from
created_at, then enforce NOT NULL; preserve the BoothReview OffsetDateTime
mapping and schema consistency.
BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java-33-54 (1)

33-54: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

유령 데이터 방지와 오류 코드 구분을 분리해 주세요.

fk_booth_reviews_booth가 존재하지 않는 booth_id의 저장을 차단하므로 유령 리뷰는 생성되지 않습니다. 다만 DataIntegrityViolationException을 모두 INVALID_INPUT_VALUE로 변환하므로, 부스 없음과 중복 리뷰가 모두 400으로 반환됩니다. 부스 없음에 ENTITY_NOT_FOUND를 반환해야 한다면 BoothRepository.existsById 검증 또는 제약 조건별 예외 처리를 추가해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java` around
lines 33 - 54, Update createReview in BoothReviewService to distinguish missing
booths from duplicate reviews: validate boothId with BoothRepository.existsById
and return ENTITY_NOT_FOUND when absent, while preserving the existing
duplicate-review validation and INVALID_INPUT_VALUE behavior. Ensure the review
is only built and saved after the booth existence check.
BE/src/main/java/com/min/edu/booth/repository/BoothReviewRepository.java-62-72 (1)

62-72: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

빈 boothIds에 대한 조기 반환을 추가해 주세요.

MobileGuideService는 부스가 없거나 검색 결과가 없을 때 빈 Set을 두 batch 메서드에 전달합니다. 빈 컬렉션이 곧 SQL 문법 오류를 일으키지는 않지만, 불필요한 쿼리와 비효율적인 IN 조건이 발생할 수 있습니다. boothIds.isEmpty()이면 repository를 호출하지 않고 빈 map을 사용하세요.

JPQL AVG의 표준 반환 타입은 Double이며 rating은 Short이므로 BigDecimal에 따른 ClassCastException 주장은 성립하지 않습니다. 프로젝션 전환은 필수 수정이 아닙니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/repository/BoothReviewRepository.java`
around lines 62 - 72, Update the callers of findAverageRatingsByBoothIds and
findReviewCountsByBoothIds to check whether boothIds is empty before invoking
the repository; use an empty map for empty inputs and retain the existing
batch-query mapping for non-empty collections. Do not change the JPQL
projections or rating result type.
BE/src/main/java/com/min/edu/booth/service/BoothCheckInService.java-54-61 (1)

54-61: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

code의 nullable 필드 비교 순서를 방어적으로 바꿔 주세요.

code.getEventId()나 code.getHolderMemberId()가 null이면 equals 호출에서 NullPointerException이 발생하고 500 응답이 나갑니다. 보유자가 지정되지 않은 교환 코드가 존재할 수 있다면, 값이 있는 쪽을 기준으로 비교하거나 Objects.equals를 쓰는 편이 안전합니다. 보유자 미지정 코드는 권한 오류(FORBIDDEN)로 처리하는 것이 의도에 맞습니다.

🔧 제안 수정
-        if (!code.getEventId().equals(booth.getEventId())) {
+        if (!Objects.equals(code.getEventId(), booth.getEventId())) {
             throw new BusinessException(GlobalErrorCode.INVALID_INPUT_VALUE);  // 다른 행사
         }
 
         // 홀더 멤버ID 확인 (본인 코드인지)
-        if (!code.getHolderMemberId().equals(memberId)) {
+        if (!memberId.equals(code.getHolderMemberId())) {
             throw new BusinessException(GlobalErrorCode.FORBIDDEN);  // 권한 없음
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/service/BoothCheckInService.java` around
lines 54 - 61, Update the event and holder comparisons in BoothCheckInService to
be null-safe, preferably using Objects.equals or comparisons anchored on the
non-null values. Ensure a null code event ID still produces INVALID_INPUT_VALUE,
while a null holder member ID is treated as FORBIDDEN rather than causing a
NullPointerException.
BE/src/main/java/com/min/edu/booth/repository/BoothRepository.java-22-24 (1)

22-24: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

행사 범위로 부스를 조회해 주세요.

VenueMapCongestionService는 eventId로 평면도를 조회하지만, 부스 조회에는 findByIdIn을 사용합니다. 평면도 위치 데이터가 다른 행사의 부스를 참조하면 해당 부스가 응답에 포함될 수 있습니다. findByEventIdAndIdIn(eventId, boothIds)로 행사 범위를 함께 제한해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/repository/BoothRepository.java` around
lines 22 - 24, Update BoothRepository and the VenueMapCongestionService booth
lookup to use findByEventIdAndIdIn(eventId, boothIds) instead of findByIdIn,
ensuring only booths belonging to the requested event are returned.
BE/src/main/java/com/min/edu/booth/service/BoothReservationService.java-122-130 (1)

122-130: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

리스너를 실제 비동기로 실행하거나 주석을 수정해 주세요. AFTER_COMMIT 설정으로 롤백 후 알림 발송 문제는 없습니다. 그러나 @Async가 없어 알림 생성이 커밋 처리 스레드에서 동기 실행됩니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/service/BoothReservationService.java`
around lines 122 - 130, BoothReservationService의 BoothVacancyEvent 발행 주석과 실제 실행
방식이 일치하지 않습니다. BoothVacancyEvent 리스너에 `@Async를` 적용하고 비동기 실행 설정을 확인해 커밋 처리 스레드와
분리하거나, 비동기 처리를 지원하지 않는다면 해당 발행 주석에서 “커밋 후 비동기” 표현을 제거하세요.
🧹 Nitpick comments (13)
BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java (2)

25-25: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

조회 메서드에 @Transactional(readOnly = true)를 적용하는 편을 권합니다.

클래스 레벨 @Transactional이 모든 메서드에 쓰기 트랜잭션을 적용합니다. getAverageRating, getBoothReviews, getMyReviews, searchReviews는 읽기 전용입니다. 지금 설정에서는 불필요한 더티 체킹과 플러시가 발생하고, 읽기 전용 복제본으로 라우팅할 수도 없습니다. 클래스에는 readOnly = true를 두고, 쓰기 메서드에만 @Transactional을 붙이는 방식이 일반적입니다.

♻️ 제안 리팩터링
 `@Service`
 `@RequiredArgsConstructor`
-@Transactional
+@Transactional(readOnly = true)
 public class BoothReviewService {

createReview, updateReview, deleteReview에는 각각 @Transactional을 붙입니다.

위 지적은 경로 지침의 "성능 저하 가능성과 불필요한 쿼리 또는 연산" 항목에 따른 것입니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java` at line
25, Update BoothReviewService so the class-level `@Transactional` uses readOnly =
true, and explicitly annotate createReview, updateReview, and deleteReview with
read-write `@Transactional`; keep getAverageRating, getBoothReviews, getMyReviews,
and searchReviews covered by the class-level read-only transaction.

Source: Path instructions


86-91: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

getReviews는 getBoothReviews를 그대로 위임합니다.

같은 동작을 하는 메서드가 두 개 있습니다. 컨트롤러 호출명을 맞추기 위한 임시 조치로 보입니다. 하나만 남기고 컨트롤러를 그 이름으로 맞추는 편이 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java` around
lines 86 - 91, Remove the redundant getReviews wrapper in BoothReviewService and
retain getBoothReviews as the single review-fetching method. Update the
controller call sites that use getReviews to invoke getBoothReviews instead,
preserving the existing arguments and pagination behavior.
BE/src/main/java/com/min/edu/booth/dto/UpdateBoothReviewRequest.java (1)

18-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

필드명과 타입을 CreateBoothReviewRequest와 맞추는 편을 권합니다.

같은 리소스인데 생성 요청은 comment/Short, 수정 요청은 content/Integer를 씁니다. 이 불일치 때문에 BoothReview에 updateRating(Short)/updateRating(Integer) 오버로드가 생겼습니다. API 스펙이 아직 고정되지 않았다면 comment/Short로 통일하는 쪽이 유지보수에 유리합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/dto/UpdateBoothReviewRequest.java` around
lines 18 - 22, Update UpdateBoothReviewRequest to use the same field name and
type as CreateBoothReviewRequest: rename content to comment and change rating
from Integer to Short, then align its accessors and validation/update flow with
the unified types so BoothReview only needs the existing updateRating(Short)
signature.

Source: Path instructions

BE/src/main/java/com/min/edu/booth/controller/BoothReviewController.java (2)

91-97: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

평균 별점 응답을 DTO로 감싸는 편을 권합니다.

Double을 본문에 그대로 내려주면 필드 추가 시 응답 구조가 깨집니다. 이미 BoothAverageRatingResponse가 정의되어 있으므로, 리뷰 개수까지 담아 그 타입으로 반환하는 방식이 확장에 유리합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/controller/BoothReviewController.java`
around lines 91 - 97, Update BoothReviewController.getAverageRating to return
the existing BoothAverageRatingResponse DTO instead of a raw Double, and
populate its average rating and review count fields from the service-layer
result. Adjust the service return type and endpoint ResponseEntity consistently
so the response remains extensible.

26-27: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

생성 응답은 201 Created가 적절합니다.

리소스를 새로 만드는 요청에 200 OK를 돌려주고 있습니다. ResponseEntity.created(location)로 바꾸면 클라이언트가 생성 여부를 상태 코드로 구분할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/controller/BoothReviewController.java`
around lines 26 - 27, Update BoothReviewController.createReview to return HTTP
201 Created for successful review creation, using
ResponseEntity.created(location) with the newly created resource’s location
instead of 200 OK.
BE/src/main/java/com/min/edu/booth/domain/BoothReview.java (1)

54-64: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

updateRating 오버로드는 하나로 정리하는 편이 안전합니다.

세 가지가 걸립니다.

  1. Short/Integer 오버로드가 공존하면 updateRating(null) 같은 리터럴 호출이 컴파일 단계에서 모호해집니다. 지금 호출부는 타입이 명확해서 문제가 없지만, 나중에 호출부를 추가할 때 걸리기 쉽습니다.
  2. Integer 버전은 null을 조용히 무시합니다. 도메인 메서드가 "값을 바꿨다"고 알려주지 않으므로 호출자는 갱신 실패를 알 수 없습니다.
  3. shortValue()는 범위 검사 없는 축소 변환입니다. DTO 검증을 우회한 경로에서 호출되면 값이 잘립니다.

Short 버전만 남기고 변환은 서비스 계층에서 명시적으로 처리하는 방식을 권합니다.

♻️ 제안 리팩터링
-    // 1) 별점 수정 (기존 - Short)
     public void updateRating(Short rating) {
+        if (rating == null || rating < 1 || rating > 5) {
+            throw new IllegalArgumentException("별점은 1~5 사이여야 합니다");
+        }
         this.rating = rating;
     }
-
-    // 1-1) ✅ 별점 수정 (오버로드 - Integer)
-    public void updateRating(Integer rating) {
-        if (rating != null) {
-            this.rating = rating.shortValue();
-        }
-    }

BoothReviewService.updateReview에서는 다음처럼 변환합니다.

review.updateRating(request.getRating().shortValue());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/domain/BoothReview.java` around lines 54 -
64, In BoothReview, remove the Integer overload of updateRating and retain only
the Short-based method so null calls are not ambiguous and the domain method
does not silently ignore updates. Update BoothReviewService.updateReview to
explicitly convert the request rating to Short before calling updateRating,
preserving the existing validation flow and avoiding implicit narrowing inside
the domain entity.
BE/src/main/java/com/min/edu/booth/dto/BoothReviewResponse.java (1)

16-21: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

updatedAt 필드를 추가하는 편을 권합니다.

이번 변경에서 BoothReview.updatedAt과 수정 API가 추가되었습니다. 그런데 응답 DTO에는 updatedAt이 없습니다. 클라이언트는 수정 요청 성공 후에도 갱신 시각을 알 수 없고, "수정됨" 표시 같은 UI를 만들 수 없습니다. BoothReviewService.toResponse에 매핑도 함께 추가해 주세요.

🛠️ 제안 수정
     private String comment;
     private OffsetDateTime createdAt;
+    private OffsetDateTime updatedAt;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/dto/BoothReviewResponse.java` around lines
16 - 21, BoothReviewResponse에 OffsetDateTime 타입의 updatedAt 필드를 추가하고,
BoothReviewService.toResponse 변환 로직에서 BoothReview.updatedAt 값을 해당 응답 필드에 매핑하세요.
BE/src/main/java/com/min/edu/booth/controller/BoothRecommendationController.java (1)

24-31: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

페이지 크기 상한을 지정하는 편이 좋습니다.

Pageable을 그대로 받으면 클라이언트가 size를 매우 크게 보낼 수 있습니다. 이 요청은 QR 스캔 집계 쿼리를 실행하므로 부하가 커집니다. 전역 spring.data.web.pageable.max-page-size 설정이 없다면 @PageableDefault로 기본값을 지정해 주세요.

♻️ 제안 리팩터
     public ResponseEntity<RecommendedBoothsResponse> getRecommendedBooths(
             `@PathVariable` Long eventId,
-            Pageable pageable) {
+            `@PageableDefault`(size = 20) Pageable pageable) {
#!/bin/bash
# 전역 pageable 설정 확인
fd -e yml -e yaml -e properties . BE/src/main/resources --exec rg -n -i 'pageable' {}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@BE/src/main/java/com/min/edu/booth/controller/BoothRecommendationController.java`
around lines 24 - 31, Update getRecommendedBooths in
BoothRecommendationController to apply an explicit maximum page size to the
Pageable parameter using the project’s established pageable
annotation/configuration, such as `@PageableDefault`, so clients cannot request an
excessively large page; preserve the existing eventId forwarding and response
handling.
BE/src/main/java/com/min/edu/booth/repository/BoothRepository.java (2)

83-84: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

행사 전체 부스 무제한 조회는 부하가 커질 수 있습니다.

MobileGuideService.getGuideMain은 findByEventId로 행사의 모든 부스를 한 번에 읽고, 그 ID 전체로 평점·후기 수·관심 여부 배치 쿼리를 실행합니다. 부스가 수백 개 이상인 행사에서는 응답 크기와 메모리 사용량이 커집니다. 메인 화면에서도 페이징이나 상위 N개 제한을 적용하는 방향을 검토해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/repository/BoothRepository.java` around
lines 83 - 84, Update the MobileGuideService.getGuideMain flow to avoid loading
every event booth through BoothRepository.findByEventId; apply the main-screen
pagination or an explicit top-N limit before collecting booth IDs and executing
rating, review-count, and interest batch queries, while preserving the existing
response behavior for the selected booths.

75-81: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

빈 주석 블록과 중복 오버로드를 정리해 주세요.

라인 77~79에는 설명만 있고 대응하는 메서드가 없습니다. 라인 81의 List 반환 버전은 라인 89의 Page 버전과 조건이 동일합니다. 사용처가 없다면 제거해 인터페이스를 단순하게 유지하는 편이 좋습니다.

#!/bin/bash
# List 반환 오버로드 사용처 확인
rg -n -C3 'findByEventIdAndDisplayNameContainingIgnoreCase' --glob '*.java'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/repository/BoothRepository.java` around
lines 75 - 81, Remove the empty guide-comment block and, after confirming no
callers use it, remove the List-returning
findByEventIdAndDisplayNameContainingIgnoreCase overload; retain the
Page-returning method with the same query conditions and keep only meaningful
API documentation.
BE/src/main/java/com/min/edu/booth/service/RedisReservationService.java (1)

74-86: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Lua 스크립트를 상수로 올려 주세요.

releaseSlot은 호출마다 스크립트 문자열과 RedisScript 인스턴스를 새로 만듭니다. 정적 상수로 두면 불필요한 객체 생성이 사라지고 스크립트 캐시 재사용이 명확해집니다. 반환값도 확인하면 소유자 불일치로 해제되지 않은 경우를 로그로 남길 수 있습니다.

♻️ 제안 리팩터
+    private static final RedisScript<Long> RELEASE_SCRIPT = RedisScript.of(
+            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
+            "return redis.call('del', KEYS[1]) " +
+            "else return 0 end", Long.class);
+
     public void releaseSlot(Long boothId, Long slotId, Long memberId) {
         String key = buildKey(boothId, slotId);
-        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
-                "return redis.call('del', KEYS[1]) " +
-                "else " +
-                "return 0 " +
-                "end";
-
-        RedisScript<Long> script = RedisScript.of(luaScript, Long.class);
-
-        stringRedisTemplate.execute(
-                script,
-                Collections.singletonList(key),
-                memberId.toString()
-        );
+        Long result = stringRedisTemplate.execute(
+                RELEASE_SCRIPT,
+                Collections.singletonList(key),
+                memberId.toString()
+        );
+        if (result == null || result == 0L) {
+            log.debug("선점 해제 대상 없음 또는 소유자 불일치. key={}", key);
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/service/RedisReservationService.java`
around lines 74 - 86, Move the Lua script text and its RedisScript<Long> wrapper
out of releaseSlot into static constants, then reuse that prebuilt script in
stringRedisTemplate.execute. Capture the execute result and log when the return
value indicates the key was not deleted because the caller is not the owner.
BE/src/main/java/com/min/edu/interest/service/BoothInterestService.java (1)

21-28: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

orElseGet 사용을 고려해 주세요.

기존 BoothInterest가 조회되어도 orElse의 기본 인자를 먼저 생성합니다. 불필요한 객체 생성을 피하려면 orElseGet으로 변경할 수 있습니다.

uk_booth_interests (member_id, booth_id) 유니크 제약이 엔티티와 마이그레이션에 이미 정의되어 있으므로 중복 행은 저장되지 않습니다. 동시 요청 시 유니크 제약 위반 예외를 정상적인 멱등 응답으로 처리해야 한다면 별도 예외 처리를 추가해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/interest/service/BoothInterestService.java`
around lines 21 - 28, BoothInterest 조회 흐름에서 기존 엔티티가 있을 때 불필요한 기본 객체가 생성되지 않도록
orElse를 orElseGet으로 변경하세요. 기본 BoothInterest 생성 로직은 지연 실행되는 supplier로 유지하고,
saveAndFlush 동작은 그대로 보존하세요. 동시 요청의 유니크 제약 위반을 멱등 성공으로 처리해야 하는 요구사항이 있다면 해당 예외
처리도 이 서비스 흐름에 추가하세요.
BE/src/main/java/com/min/edu/booth/service/VenueMapCongestionService.java (1)

49-50: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

평면도 위치와 부스 조회를 배치 처리해주세요.

buildVenueMapWithCongestion은 평면도마다 findByVenueMapId와 findByIdIn을 호출합니다. 평면도 N개에서 두 조회가 각각 N회 실행됩니다. findByVenueMapIdIn으로 위치를 한 번 조회하고, 전체 boothId를 수집해 부스를 한 번 조회한 뒤 venueMapId별로 그룹화하여 기존 마커 응답을 유지해주세요. boothIds가 비어 있으면 부스 조회를 생략해주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/service/VenueMapCongestionService.java`
around lines 49 - 50, Update the venue-map processing around
buildVenueMapWithCongestion to batch-load all positions with findByVenueMapIdIn
and collect booth IDs across every map for a single findByIdIn call. Group the
fetched positions by venueMapId, pass the grouped data into the existing
marker-building flow, preserve the current marker response, and skip the booth
query when boothIds is empty.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@BE/src/main/java/com/min/edu/booth/controller/BoothReservationController.java`:
- Line 60: Update the cancelReservation call in BoothReservationController to
pass arguments in the service signature order: reservationId, boothId, then
principal.getMemberId().
- Around line 37-47: Update the security configuration for the reservation
endpoint handled by BoothReservationController.createReservation so
/booths/{boothId}/reservations requires authenticated access instead of falling
through to anyRequest().permitAll().

In `@BE/src/main/java/com/min/edu/booth/controller/BoothReviewController.java`:
- Around line 79-86: BoothReviewController.getReviews의 페이지 크기를 제한하고 공개 응답에서 내부
회원 식별자가 노출되지 않도록 수정하세요. Pageable에 `@PageableDefault를` 적용하고
spring.data.web.pageable.max-page-size 설정을 확인·추가해 클라이언트의 과도한 size 값을 제한하세요.
BoothReviewResponse 또는 reviewService.getReviews의 매핑에서 memberId를 제외하고 공개 목록의 기존
필드와 페이지 동작은 유지하세요.
- Around line 30-36: Update SecurityConfig to require authentication for
/booths/** before the anyRequest().permitAll() rule, and update the review
create, modify, and delete methods in BoothReviewController to handle a missing
AuthenticatedMemberDto principal by returning 401 rather than dereferencing it.

In `@BE/src/main/java/com/min/edu/booth/dto/UpdateBoothReviewRequest.java`:
- Around line 20-22: Update
BE/src/main/java/com/min/edu/booth/dto/UpdateBoothReviewRequest.java:20-22 by
adding `@NotNull` to rating so null requests are rejected. Update
BE/src/main/java/com/min/edu/booth/domain/BoothReview.java:59-64 by removing
updateRating(Integer) and retaining a single updateRating(Short) that throws for
null or values outside 1–5.

In `@BE/src/main/java/com/min/edu/booth/event/BoothVacancyEventListener.java`:
- Around line 25-32: Update BoothVacancyEventListener.handleBoothVacancy and the
notification flow to dispatch work asynchronously through a dedicated `@Async`
executor or durable retryable follow-up mechanism, ensuring failed post-commit
notifications can be retried without affecting the committed cancellation. In
notifyVacancyWithIdempotency, derive a deterministic idempotency identifier from
the event and member instead of generating UUID.randomUUID(), and remove the
extra closing brace near the listener method.

In `@BE/src/main/java/com/min/edu/booth/repository/BoothQrScanRepository.java`:
- Around line 27-39: Update findPopularBooths in BoothQrScanRepository by adding
an explicit countQuery that counts distinct bqs.boothId and applies the same
eventId, since, duplicate, and Booth join conditions as the main query. Preserve
the existing grouped result query and pagination parameters.

In
`@BE/src/main/java/com/min/edu/booth/repository/BoothReservationRepository.java`:
- Around line 21-22: Update
BoothReservationRepository.findByBoothIdOrderByCreatedAtDesc to use the valid
BoothReservation property reservedAt, renaming it to
findByBoothIdOrderByReservedAtDesc; if the method has no callers, remove it
instead.

In
`@BE/src/main/java/com/min/edu/booth/repository/BoothReservationSlotRepository.java`:
- Around line 57-61: 예약 생성 흐름에 BoothReservationSlot 갱신을 위한 PESSIMISTIC_WRITE 또는
`@Version` 동시성 제어를 적용하고, saveAndFlush 직후가 아닌 트랜잭션 커밋 이후에 Redis 슬롯 키를 해제하도록 변경하세요.
existsByBoothIdAndAvailableSlots는 Redis에서 선점된 슬롯 수를 반영해 DB의 가용 슬롯과 실제 예약 가능 상태가
일치하도록 수정하세요.

In `@BE/src/main/java/com/min/edu/booth/service/BoothCheckInService.java`:
- Around line 95-113: Redefine duplicate detection in
BoothCheckInService.checkDuplicateScan so it identifies a recent rescan by the
same member or exchange code at the same booth, rather than relying on a code
already marked redeemed. Add or use a BoothQrScanRepository query matching the
selected booth and identity criteria, update the method signature and call site
accordingly, and ensure BoothQrScan.duplicate reflects that result before
saving.

In `@BE/src/main/java/com/min/edu/booth/service/BoothRecommendationService.java`:
- Around line 32-73: Update getRecommendedBooths and its repository query so
recommendations are built from all Booth records, including booths with no
recent BoothQrScan entries, using a LEFT JOIN and treating missing scan counts
as zero. Keep the congested-booth query for the fixed TOP 3 exclusion, but
retrieve the remaining recommendations in ascending congestion order and assign
rank 1 to the least congested booth.
- Around line 55-73: Update BoothRecommendationService’s findPopularBooths query
flow so congestedBoothIds are excluded in the database predicate (for example,
through the repository query’s excludedIds parameter) before pagination, and
remove the in-memory filtering in the recommendation loop. Also reuse the
initial aggregate result when page is 0 and size is at least 3, avoiding the
duplicate query currently performed around findPopularBooths.

In `@BE/src/main/java/com/min/edu/booth/service/BoothReservationService.java`:
- Around line 59-84: Unify the duplicated reservation creation and cancellation
logic across BoothReservationService.java lines 59-84 and
BoothReservationWithRedisService.java lines 86-114: either remove
BoothReservationService if it is unused, or extract shared booth/slot
validation, locking, reservation persistence, seat-count updates, and toResponse
conversion into common methods and use them from both services. Update
BoothReservationWithRedisService.java lines 86-114 to retain only Redis
preemption while delegating the shared workflow, ensuring both implementations
have identical validation and persistence behavior.
- Around line 59-84: Update the reservation flow around the slot lookup and
capacity check to load the slot using a pessimistic lock, matching
BoothReservationWithRedisService, and validate reservedCount plus
request.partySize against capacity before saving. Preserve the existing
incrementReservedCount and reservation persistence flow while ensuring
concurrent requests cannot exceed the slot capacity.
- Around line 96-131: In cancelReservation, validate that the requested boothId
matches the reservation’s associated booth before changing state or publishing
events, and use the verified booth identity for the notification. Update the
slot lookup to use the locking method findByIdWithLock, following
BoothReservationWithRedisService.cancelReservation, and apply the same
lower-bound validation before decrementing the reserved count.

In
`@BE/src/main/java/com/min/edu/booth/service/BoothReservationWithRedisService.java`:
- Around line 86-114: BoothReservationWithRedisService의 예약 처리에서 Redis 선점 해제를
트랜잭션 커밋 이후로 지연시키고, 성공·실패 모두 afterCompletion에서 한 번만 수행하도록 변경하세요. 또한 슬롯 조회 로직이
사용하는 slot 조회 메서드에 비관적 잠금을 적용해 동시 예약의 reservedCount 갱신을 직렬화하고 정원 초과를 방지하세요.

In
`@BE/src/main/java/com/min/edu/booth/service/BoothVacancyNotificationService.java`:
- Around line 27-29: Update notifyVacancyWithIdempotency to propagate
idempotencyKey into the notification persistence flow instead of calling
notifyVacancy with a newly generated UUID. Make each member’s eventId
deterministic from idempotencyKey and memberId, or use that pair as the unique
insertIfAbsent key, while preserving per-member notification behavior across
repeated event deliveries.

---

Minor comments:
In `@BE/src/main/java/com/min/edu/booth/dto/BoothAverageRatingResponse.java`:
- Around line 14-16: BoothAverageRatingResponse가 미사용 상태이므로 DTO와 관련 미사용 import를
제거하거나 평균 별점 API의 반환 타입으로 적용하세요. 적용할 경우 평균 별점 API와 서비스의 반환 규칙을 통일해 리뷰 없음 상태의
averageRating을 일관되게 처리하고 reviewCount도 함께 반환하도록 BoothAverageRatingResponse 사용 지점을
수정하세요.

In `@BE/src/main/java/com/min/edu/booth/dto/BoothReviewResponse.java`:
- Around line 16-21: 수정 시각이 응답에 포함되도록 BoothReviewResponse에 OffsetDateTime
updatedAt 필드를 추가하고, BoothReviewService의 toResponse 변환에서 review.getUpdatedAt()을
해당 필드에 매핑하세요. 변경 대상은
BE/src/main/java/com/min/edu/booth/dto/BoothReviewResponse.java 16-21 및
BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java 171-179입니다.

In `@BE/src/main/java/com/min/edu/booth/dto/CreateBoothReviewRequest.java`:
- Around line 17-22: Update CreateBoothReviewRequest.comment with a validation
constraint limiting its length to the BoothReview column’s 300-character
maximum, while keeping it optional so null or omitted comments remain valid.

In `@BE/src/main/java/com/min/edu/booth/repository/BoothRepository.java`:
- Around line 22-24: Update BoothRepository and the VenueMapCongestionService
booth lookup to use findByEventIdAndIdIn(eventId, boothIds) instead of
findByIdIn, ensuring only booths belonging to the requested event are returned.

In `@BE/src/main/java/com/min/edu/booth/repository/BoothReviewRepository.java`:
- Around line 62-72: Update the callers of findAverageRatingsByBoothIds and
findReviewCountsByBoothIds to check whether boothIds is empty before invoking
the repository; use an empty map for empty inputs and retain the existing
batch-query mapping for non-empty collections. Do not change the JPQL
projections or rating result type.

In `@BE/src/main/java/com/min/edu/booth/service/BoothCheckInService.java`:
- Around line 54-61: Update the event and holder comparisons in
BoothCheckInService to be null-safe, preferably using Objects.equals or
comparisons anchored on the non-null values. Ensure a null code event ID still
produces INVALID_INPUT_VALUE, while a null holder member ID is treated as
FORBIDDEN rather than causing a NullPointerException.

In `@BE/src/main/java/com/min/edu/booth/service/BoothReservationService.java`:
- Around line 122-130: BoothReservationService의 BoothVacancyEvent 발행 주석과 실제 실행
방식이 일치하지 않습니다. BoothVacancyEvent 리스너에 `@Async를` 적용하고 비동기 실행 설정을 확인해 커밋 처리 스레드와
분리하거나, 비동기 처리를 지원하지 않는다면 해당 발행 주석에서 “커밋 후 비동기” 표현을 제거하세요.

In `@BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java`:
- Around line 68-73: Update BoothReviewService.getAverageRating to preserve the
documented null value when no reviews exist instead of converting an empty
Optional to 0.0, or change the service response contract to include
countByBoothId and distinguish no reviews from a zero rating; keep
BoothAverageRatingResponse behavior consistent with the chosen contract.
- Around line 107-116: Update BoothReviewService.searchReviews to validate
keyword before calling
boothReviewRepository.findByBoothIdAndCommentContainingIgnoreCase: reject null,
empty, and whitespace-only values at service entry using the project’s
established validation/exception convention, and preserve the repository query
path only for meaningful keywords.
- Around line 33-54: Update createReview in BoothReviewService to distinguish
missing booths from duplicate reviews: validate boothId with
BoothRepository.existsById and return ENTITY_NOT_FOUND when absent, while
preserving the existing duplicate-review validation and INVALID_INPUT_VALUE
behavior. Ensure the review is only built and saved after the booth existence
check.

In `@BE/src/main/resources/db/migration/V14__Add_updated_at_to_booth_reviews.sql`:
- Line 1: Update the migration statement to add updated_at as TIMESTAMPTZ
without an immediate default or NOT NULL constraint, backfill existing rows from
created_at, then enforce NOT NULL; preserve the BoothReview OffsetDateTime
mapping and schema consistency.

---

Nitpick comments:
In
`@BE/src/main/java/com/min/edu/booth/controller/BoothRecommendationController.java`:
- Around line 24-31: Update getRecommendedBooths in
BoothRecommendationController to apply an explicit maximum page size to the
Pageable parameter using the project’s established pageable
annotation/configuration, such as `@PageableDefault`, so clients cannot request an
excessively large page; preserve the existing eventId forwarding and response
handling.

In `@BE/src/main/java/com/min/edu/booth/controller/BoothReviewController.java`:
- Around line 91-97: Update BoothReviewController.getAverageRating to return the
existing BoothAverageRatingResponse DTO instead of a raw Double, and populate
its average rating and review count fields from the service-layer result. Adjust
the service return type and endpoint ResponseEntity consistently so the response
remains extensible.
- Around line 26-27: Update BoothReviewController.createReview to return HTTP
201 Created for successful review creation, using
ResponseEntity.created(location) with the newly created resource’s location
instead of 200 OK.

In `@BE/src/main/java/com/min/edu/booth/domain/BoothReview.java`:
- Around line 54-64: In BoothReview, remove the Integer overload of updateRating
and retain only the Short-based method so null calls are not ambiguous and the
domain method does not silently ignore updates. Update
BoothReviewService.updateReview to explicitly convert the request rating to
Short before calling updateRating, preserving the existing validation flow and
avoiding implicit narrowing inside the domain entity.

In `@BE/src/main/java/com/min/edu/booth/dto/BoothReviewResponse.java`:
- Around line 16-21: BoothReviewResponse에 OffsetDateTime 타입의 updatedAt 필드를 추가하고,
BoothReviewService.toResponse 변환 로직에서 BoothReview.updatedAt 값을 해당 응답 필드에 매핑하세요.

In `@BE/src/main/java/com/min/edu/booth/dto/UpdateBoothReviewRequest.java`:
- Around line 18-22: Update UpdateBoothReviewRequest to use the same field name
and type as CreateBoothReviewRequest: rename content to comment and change
rating from Integer to Short, then align its accessors and validation/update
flow with the unified types so BoothReview only needs the existing
updateRating(Short) signature.

In `@BE/src/main/java/com/min/edu/booth/repository/BoothRepository.java`:
- Around line 83-84: Update the MobileGuideService.getGuideMain flow to avoid
loading every event booth through BoothRepository.findByEventId; apply the
main-screen pagination or an explicit top-N limit before collecting booth IDs
and executing rating, review-count, and interest batch queries, while preserving
the existing response behavior for the selected booths.
- Around line 75-81: Remove the empty guide-comment block and, after confirming
no callers use it, remove the List-returning
findByEventIdAndDisplayNameContainingIgnoreCase overload; retain the
Page-returning method with the same query conditions and keep only meaningful
API documentation.

In `@BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java`:
- Line 25: Update BoothReviewService so the class-level `@Transactional` uses
readOnly = true, and explicitly annotate createReview, updateReview, and
deleteReview with read-write `@Transactional`; keep getAverageRating,
getBoothReviews, getMyReviews, and searchReviews covered by the class-level
read-only transaction.
- Around line 86-91: Remove the redundant getReviews wrapper in
BoothReviewService and retain getBoothReviews as the single review-fetching
method. Update the controller call sites that use getReviews to invoke
getBoothReviews instead, preserving the existing arguments and pagination
behavior.

In `@BE/src/main/java/com/min/edu/booth/service/RedisReservationService.java`:
- Around line 74-86: Move the Lua script text and its RedisScript<Long> wrapper
out of releaseSlot into static constants, then reuse that prebuilt script in
stringRedisTemplate.execute. Capture the execute result and log when the return
value indicates the key was not deleted because the caller is not the owner.

In `@BE/src/main/java/com/min/edu/booth/service/VenueMapCongestionService.java`:
- Around line 49-50: Update the venue-map processing around
buildVenueMapWithCongestion to batch-load all positions with findByVenueMapIdIn
and collect booth IDs across every map for a single findByIdIn call. Group the
fetched positions by venueMapId, pass the grouped data into the existing
marker-building flow, preserve the current marker response, and skip the booth
query when boothIds is empty.

In `@BE/src/main/java/com/min/edu/interest/service/BoothInterestService.java`:
- Around line 21-28: BoothInterest 조회 흐름에서 기존 엔티티가 있을 때 불필요한 기본 객체가 생성되지 않도록
orElse를 orElseGet으로 변경하세요. 기본 BoothInterest 생성 로직은 지연 실행되는 supplier로 유지하고,
saveAndFlush 동작은 그대로 보존하세요. 동시 요청의 유니크 제약 위반을 멱등 성공으로 처리해야 하는 요구사항이 있다면 해당 예외
처리도 이 서비스 흐름에 추가하세요.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 856911a7-9dc0-46a7-ad4a-71146848fe97

📥 Commits

Reviewing files that changed from the base of the PR and between b9dcf8f and 2bf58d0.

📒 Files selected for processing (37)
  • BE/src/main/java/com/min/edu/booth/controller/BoothCheckInController.java
  • BE/src/main/java/com/min/edu/booth/controller/BoothRecommendationController.java
  • BE/src/main/java/com/min/edu/booth/controller/BoothReservationController.java
  • BE/src/main/java/com/min/edu/booth/controller/BoothReviewController.java
  • BE/src/main/java/com/min/edu/booth/controller/MobileGuideController.java
  • BE/src/main/java/com/min/edu/booth/controller/VenueMapCongestionController.java
  • BE/src/main/java/com/min/edu/booth/domain/BoothReview.java
  • BE/src/main/java/com/min/edu/booth/dto/BoothAverageRatingResponse.java
  • BE/src/main/java/com/min/edu/booth/dto/BoothCheckInRequest.java
  • BE/src/main/java/com/min/edu/booth/dto/BoothReviewResponse.java
  • BE/src/main/java/com/min/edu/booth/dto/CreateBoothReviewRequest.java
  • BE/src/main/java/com/min/edu/booth/dto/MobileGuideBoothDetailResponse.java
  • BE/src/main/java/com/min/edu/booth/dto/MobileGuideBoothListResponse.java
  • BE/src/main/java/com/min/edu/booth/dto/MobileGuideMainResponse.java
  • BE/src/main/java/com/min/edu/booth/dto/RecommendedBoothsResponse.java
  • BE/src/main/java/com/min/edu/booth/dto/UpdateBoothReviewRequest.java
  • BE/src/main/java/com/min/edu/booth/dto/VenueMapWithCongestionResponseDto.java
  • BE/src/main/java/com/min/edu/booth/event/BoothVacancyEvent.java
  • BE/src/main/java/com/min/edu/booth/event/BoothVacancyEventListener.java
  • BE/src/main/java/com/min/edu/booth/repository/BoothQrScanRepository.java
  • BE/src/main/java/com/min/edu/booth/repository/BoothRepository.java
  • BE/src/main/java/com/min/edu/booth/repository/BoothReservationRepository.java
  • BE/src/main/java/com/min/edu/booth/repository/BoothReservationSlotRepository.java
  • BE/src/main/java/com/min/edu/booth/repository/BoothReviewRepository.java
  • BE/src/main/java/com/min/edu/booth/service/BoothCheckInService.java
  • BE/src/main/java/com/min/edu/booth/service/BoothRecommendationService.java
  • BE/src/main/java/com/min/edu/booth/service/BoothReservationService.java
  • BE/src/main/java/com/min/edu/booth/service/BoothReservationWithRedisService.java
  • BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java
  • BE/src/main/java/com/min/edu/booth/service/BoothVacancyNotificationService.java
  • BE/src/main/java/com/min/edu/booth/service/MobileGuideService.java
  • BE/src/main/java/com/min/edu/booth/service/RedisReservationService.java
  • BE/src/main/java/com/min/edu/booth/service/VenueMapCongestionService.java
  • BE/src/main/java/com/min/edu/interest/dto/InterestBoothResponse.java
  • BE/src/main/java/com/min/edu/interest/repository/BoothInterestRepository.java
  • BE/src/main/java/com/min/edu/interest/service/BoothInterestService.java
  • BE/src/main/resources/db/migration/V14__Add_updated_at_to_booth_reviews.sql

Comment on lines 37 to 47
public ResponseEntity<BoothReservationResponse> createReservation(
@PathVariable Long boothId,
@RequestBody @Valid CreateBoothReservationRequest request,
@AuthenticationPrincipal AuthenticatedMemberDto principal) {

BoothReservationResponse response = boothReservationService.createReservation(
boothId, request, principal.getMemberId()
// Service에서 Redis + DB를 통합 처리
BoothReservationResponse response = reservationService.createReservationWithRedis(
boothId,
request,
principal.getMemberId()
);

@coderabbitai coderabbitai Bot Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f -e java | xargs -r rg -ln 'SecurityFilterChain' | xargs -r rg -n 'requestMatchers|authenticated|permitAll|/booths' -C2

Repository: half-fifty/eventoday

Length of output: 4138


🏁 Script executed:

#!/bin/bash
set -e
echo '--- controller mapping and methods ---'
fd -t f -e java | rg 'BoothReservationController\.java$' | xargs -r cat -n
echo '--- security configuration context ---'
fd -t f -e java | xargs -r rg -l 'SecurityFilterChain' | xargs -r sh -c 'for f do echo "--- $f"; cat -n "$f"; done' sh
echo '--- reservation endpoint references ---'
rg -n -C3 'createReservationWithRedis|cancelReservation|BoothReservationController|booths' BE/src/main/java

Repository: half-fifty/eventoday

Length of output: 50005


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
import re

controller = Path("BE/src/main/java/com/min/edu/booth/controller/BoothReservationController.java").read_text()
security = Path("BE/src/main/java/com/min/edu/common/config/SecurityConfig.java").read_text()

mapping = re.search(r'`@RequestMapping`\("([^"]+)"\)', controller).group(1)
create = mapping + re.search(r'`@PostMapping`\s*(?:\(\))?', controller).group(0).replace("`@PostMapping`", "").replace("()", "")
create = mapping.rstrip("/")  # method-level `@PostMapping` adds no suffix

secured = re.search(r'\.requestMatchers\("/events/\*/booths/\*\*"\)\.authenticated\(\)', security)
fallback = re.search(r'\.anyRequest\(\)\.permitAll\(\)', security)

print(f"controller base mapping: {mapping}")
print(f"create endpoint shape: {create}")
print(f"matches authenticated /events/*/booths/** rule: {bool(secured and create.startswith('/events/'))}")
print(f"fallback anyRequest().permitAll(): {bool(fallback)}")
assert mapping.startswith("/booths/")
assert secured and fallback
assert not create.startswith("/events/")
PY

echo '--- authentication principal population ---'
fd -t f -e java BE/src/main/java | xargs -r rg -n -C4 \
  'SecurityContextHolder|setAuthentication|AuthenticatedMemberDto|OncePerRequestFilter|Bearer'

Repository: half-fifty/eventoday

Length of output: 776


예약 경로를 인증 필수로 지정해 주세요.
/booths/{boothId}/reservations는 /events/*/booths/** 규칙과 일치하지 않아 anyRequest().permitAll()이 적용됩니다. 비로그인 요청의 principal이 null이므로 principal.getMemberId()에서 NullPointerException이 발생하고 500 응답이 반환됩니다. 해당 경로를 authenticated()로 보호해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@BE/src/main/java/com/min/edu/booth/controller/BoothReservationController.java`
around lines 37 - 47, Update the security configuration for the reservation
endpoint handled by BoothReservationController.createReservation so
/booths/{boothId}/reservations requires authenticated access instead of falling
through to anyRequest().permitAll().

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@CodeRabbit
/booths/{boothId}/reservations SecurityConfig 업데이트 관련:

이 지적은 SKIP하는 이유:

  1. SecurityConfig는 팀 공용 설정

    • 이 파일은 제가 작성하지 않았으며 팀리드가 관리함
    • 팀 전체 보안 정책 수정은 팀 협의 필요
    • 팀리드와 함께 조정해야 함
  2. 현재 Controller는 이미 인증을 강제함

    • @AuthenticationPrincipal AuthenticatedMemberDto principal
    • 인증되지 않은 요청은 이 메서드에 도달하지 않음
    • 엔드포인트 레벨에서 보안 보장
  3. 아키텍처 설계상 선택

    • 팀이 의도적으로 전역 설정 대신 엔드포인트 레벨 인증 사용
    • 엔드포인트별 다양한 인증 전략 가능

결론: 현재 구현이 안전합니다. SecurityConfig 변경은
팀리드가 별도로 처리하여 전체 앱의 보안 아키텍처 일관성을 유지해야 합니다.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@suho-98, SecurityConfig의 변경 소유권이 팀리드에게 있다는 점은 이해했습니다. 다만 현재 근거로는 이 리뷰를 SKIP하거나 철회할 수 없습니다.

@AuthenticationPrincipal은 인증을 강제하지 않습니다. 이 어노테이션은 SecurityContext에 있는 principal을 주입합니다. 현재 경로는 SecurityConfig의 인증 규칙과 일치하지 않고 anyRequest().permitAll()로 처리됩니다. 따라서 비인증 요청도 컨트롤러 메서드에 도달할 수 있습니다.

그 경우 principal은 null이며, principal.getMemberId()에서 NullPointerException이 발생합니다. 결과는 인증 실패 응답이 아니라 500 응답입니다.

팀리드가 SecurityConfig에서 /booths/*/reservations/**를 authenticated()로 별도 반영한다면, 이 PR에서는 해당 작업을 분리할 수 있습니다. 하지만 반영 전까지는 보안 이슈가 남아 있습니다. 원하시면 팀리드 작업용 후속 이슈를 만들 수 있습니다.

@PreAuthorize("isAuthenticated()") 같은 메서드 보안을 의도했다면, 현재 BoothReservationController에는 그 설정도 없습니다.
🐇 …

You are interacting with an AI system.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@EnableMethodSecurity방식으로 해결완료!

Comment thread BE/src/main/java/com/min/edu/booth/controller/BoothReservationController.java Outdated
Comment thread BE/src/main/java/com/min/edu/booth/event/BoothVacancyEventListener.java Outdated
Comment thread BE/src/main/java/com/min/edu/booth/repository/BoothQrScanRepository.java Outdated
Comment thread BE/src/main/java/com/min/edu/booth/service/BoothRecommendationService.java Outdated
Comment thread BE/src/main/java/com/min/edu/booth/service/BoothVacancyNotificationService.java Outdated
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
@half-fifty half-fifty deleted a comment from coderabbitai Bot Aug 10, 2026
@half-fifty half-fifty deleted a comment from coderabbitai Bot Aug 10, 2026
@half-fifty half-fifty deleted a comment from coderabbitai Bot Aug 10, 2026
…oth domain

- fix(booth): Redis 선점 해제 Race Condition (afterCompletion 패턴)
- fix(notification): 멱등성 알림 (idempotencyKey propagate)
- refactor(booth): 부스 혼잡도 페이징 최적화 (countQuery)
- chore(booth): Dead Code 정리
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java (1)

49-56: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

리뷰 생성 시 memberName을 저장하세요.

BoothReviewResponse는 Line 182에서 review.getMemberName()을 반환하지만, 이 builder에는 .memberName(...) 할당이 없습니다. saveAndFlush만으로 회원명이 채워지지 않으므로 생성 응답과 이후 리뷰 목록에서 작성자명이 null이 될 수 있습니다. 회원명 스냅샷을 저장한다면 생성 전에 값을 조회하고, 기존 리뷰도 백필하세요. 실시간 회원명을 보여주는 설계라면 응답 조회를 join 또는 projection으로 변경하세요.

경로 지침의 비즈니스 로직 및 데이터 무결성 점검 항목에 따른 의견입니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java` around
lines 49 - 56, Update the review creation flow around the BoothReview builder to
populate memberName before saveAndFlush, using the existing member lookup and
preserving the stored author-name snapshot in BoothReviewResponse; also backfill
memberName for existing reviews so list and creation responses do not return
null.

Source: Path instructions

🧹 Nitpick comments (9)
FE/src/pages/OrganizerAdmin.jsx (2)

1109-1128: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

파일 확장자만 검사하고 크기는 검사하지 않습니다.

바로 위 안내 문구에는 "최대 10MB"라고 적혀 있습니다. 그러나 검증은 확장자만 합니다. 큰 파일을 고르면 업로드 후 서버에서 실패합니다. BoothApply.jsx의 validateUploadFile이 확장자와 크기를 모두 검사하므로, 그 헬퍼를 공용 모듈로 빼서 함께 쓰는 편이 좋겠습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FE/src/pages/OrganizerAdmin.jsx` around lines 1109 - 1128, Update the
file-selection validation in the OrganizerAdmin input handler to reuse the
shared helper extracted from BoothApply.jsx’s validateUploadFile, ensuring both
allowed extensions and the 10 MB size limit are checked before setContentFile.
Preserve the existing error/reset behavior and use the helper’s validation
result for the displayed file error.

439-467: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

appActionId 가드는 빠른 연속 클릭을 완전히 막지 못합니다.

if (appActionId) return; 다음에 setAppActionId(...)를 호출합니다. 상태 반영과 disabled 적용은 다음 렌더에서 일어납니다. 리렌더 전에 두 번째 클릭이 들어오면 두 핸들러가 모두 통과합니다. 승인은 부스 배정을 동반하니 클라이언트에서도 확실히 막는 편이 좋습니다. 서버가 UNDER_REVIEW 상태 검증과 비관적 락으로 두 번째 요청을 거부하므로 데이터가 깨지지는 않지만, 사용자에게는 불필요한 에러가 보입니다. useRef 기반 가드를 함께 쓰면 해결됩니다.

♻️ 제안 diff
+  // 상태 반영 전 연속 클릭까지 막기 위한 동기 가드
+  const appActionRef = useRef(null);
+
   const handleStartReview = async (applicationId) => {
-    if (appActionId) return; // 이미 처리 중인 요청이 있으면 무시
+    if (appActionRef.current) return; // 이미 처리 중인 요청이 있으면 무시
+    appActionRef.current = applicationId;
     setAppActionId(applicationId);
     try {
       await startReview(applicationId);
       patchApplicationStatus(applicationId, "UNDER_REVIEW");
       refreshAppCounts(); // 검토 대기 카운트 재조회
     } catch (error) {
       setAppError(error.message || "검토 시작에 실패했습니다.");
     } finally {
+      appActionRef.current = null;
       setAppActionId(null);
     }
   };
 
   const handleApprove = async (applicationId) => {
-    if (appActionId) return;
+    if (appActionRef.current) return;
     if (!window.confirm("이 신청을 승인하시겠습니까? 승인 시 부스가 자동 배정됩니다.")) return;
+    appActionRef.current = applicationId;
     setAppActionId(applicationId);
     try {
       await approveApplication(applicationId);
       patchApplicationStatus(applicationId, "APPROVED");
       refreshAppCounts(); // 검토 대기·승인 카운트 재조회
     } catch (error) {
       setAppError(error.message || "승인에 실패했습니다.");
     } finally {
+      appActionRef.current = null;
       setAppActionId(null);
     }
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FE/src/pages/OrganizerAdmin.jsx` around lines 439 - 467, Update
handleStartReview and handleApprove to use a synchronous useRef-based in-flight
guard alongside appActionId, checking and setting the ref before starting each
request so rapid clicks before rerender are rejected. Clear the ref in each
handler’s finally block, while preserving the existing state updates, error
handling, and appActionId reset.
FE/src/pages/BoothApplicationList.jsx (2)

218-227: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

상세 패널을 Esc 키로 닫을 수 없습니다.

오버레이 클릭과 닫기 버튼만 있습니다. 키보드 사용자는 패널을 닫기 어렵습니다. role="dialog", aria-modal="true"와 Esc 키 핸들러를 추가하면 좋겠습니다. 같은 PR의 OrganizerAdmin.jsx 모달은 role="dialog"를 이미 쓰고 있어 패턴을 맞추기도 쉽습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FE/src/pages/BoothApplicationList.jsx` around lines 218 - 227, Update the
selected-detail panel rendered by the selectedId block to include role="dialog"
and aria-modal="true", and add an Escape-key handler that calls closeDetail.
Reuse the existing closeDetail behavior and ensure the handler is attached while
the panel is open.

107-110: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

상세 조회 실패를 cancelError에 담고 있습니다.

cancelError는 취소 실패용 상태입니다. 상세 조회 실패까지 같은 상태를 쓰면, 나중에 두 에러가 동시에 발생하는 경우 메시지가 서로 덮어씁니다. 상태 이름과 의미도 어긋납니다. detailError를 따로 두는 편이 읽기 쉽습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FE/src/pages/BoothApplicationList.jsx` around lines 107 - 110, 상세 조회 요청의
catch 처리에서 취소 실패 상태인 cancelError 대신 별도 detailError 상태를 사용하도록 수정하세요.
BoothApplicationList의 상태 선언과 상세 조회 오류 메시지를 설정하는 setCancelError 호출을 찾아
detailError 및 해당 setter로 분리하고, 취소 오류 처리 흐름은 기존 cancelError를 계속 사용하세요.
FE/src/pages/BoothApply.jsx (1)

163-176: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

toggleFilter 안에 isEligible 로직이 다시 구현되어 있습니다.

isEligible과 동일한 조건이 next 필터 기준으로 복제되어 있습니다. 조건이 하나만 추가되면 두 곳을 같이 고쳐야 합니다. isEligible이 필터를 인자로 받게 만들면 중복이 없어집니다.

♻️ 제안 diff
-  const isEligible = (booth) =>
+  const isEligible = (booth, f = filters) =>
     booth.status === "AVAILABLE" &&
-    (!filters.elec || booth.electricityAvailable) &&
-    (!filters.water || booth.waterAvailable);
+    (!f.elec || booth.electricityAvailable) &&
+    (!f.water || booth.waterAvailable);
@@
     if (selectedBoothId) {
       const booth = booths.find((b) => b.id === selectedBoothId);
-      const stillEligible =
-        booth &&
-        booth.status === "AVAILABLE" &&
-        (!next.elec || booth.electricityAvailable) &&
-        (!next.water || booth.waterAvailable);
-      if (!stillEligible) setSelectedBoothId(null);
+      if (!booth || !isEligible(booth, next)) setSelectedBoothId(null);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FE/src/pages/BoothApply.jsx` around lines 163 - 176, Update isEligible to
accept an optional filter state, then have toggleFilter call it with next
instead of duplicating the booth eligibility conditions. Use that shared helper
to determine whether selectedBoothId remains valid and preserve the existing
deselection behavior.
BE/src/main/java/com/min/edu/application/service/BoothApplicationNotificationListener.java (1)

64-69: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

escapeHtml을 공통 유틸로 추출하세요.

동일한 구현이 세 서비스에 중복되어 있습니다. 공통 유틸 또는 HtmlUtils.htmlEscape를 사용하면 수정 누락으로 이스케이프 동작이 달라지는 문제를 방지할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@BE/src/main/java/com/min/edu/application/service/BoothApplicationNotificationListener.java`
around lines 64 - 69, BoothApplicationNotificationListener의 escapeHtml 구현을 제거하고,
세 서비스가 공유하는 공통 HTML 이스케이프 유틸 또는 기존 HtmlUtils.htmlEscape를 사용하도록 교체하세요. null 처리와
현재 이스케이프 동작을 유지하며, 중복된 서비스별 구현도 동일한 공통 경로로 통일하세요.
FE/src/pages/Notices.jsx (1)

89-90: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

filtered 별칭은 이제 필요하지 않습니다.

필터링을 BE로 옮겼으므로 filtered는 items와 항상 같습니다. 아래 렌더링에서 items를 직접 쓰면 "클라이언트에서도 한 번 더 거르나?"라는 오해를 줄일 수 있습니다. 급한 사안은 아니니 정리할 때 함께 반영해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FE/src/pages/Notices.jsx` around lines 89 - 90, Remove the unnecessary
filtered alias in the Notices component and update the related rendering logic
to use items directly, preserving the existing backend-filtered behavior and
avoiding any client-side filtering implication.
BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java (1)

58-64: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

무결성 예외를 모두 입력 오류로 변환하지 마세요.

DataIntegrityViolationException에는 중복 키뿐 아니라 null 제약조건, 길이 초과, 외래 키, 스키마 오류도 포함될 수 있습니다. 현재 구현은 서버 또는 스키마 오류를 INVALID_INPUT_VALUE로 응답하게 만듭니다. 중복 리뷰 제약조건만 분류하고 나머지는 원인 로그와 서버 오류 경로로 전달하세요.

경로 지침의 예외 처리 및 데이터 무결성 점검 항목에 따른 의견입니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java` around
lines 58 - 64, BoothReviewService의 saveAndFlush 예외 처리에서 모든
DataIntegrityViolationException을 INVALID_INPUT_VALUE로 변환하지 마세요. 중복 리뷰 제약조건에 해당하는
경우만 BusinessException(GlobalErrorCode.INVALID_INPUT_VALUE)으로 분류하고, 그 외 예외는 원인
정보를 로깅한 뒤 기존 서버 오류 처리 경로로 전파하세요.

Source: Path instructions

BE/src/main/java/com/min/edu/application/service/BoothApplicationDecision.java (1)

7-13: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

이벤트 계약에 반려 사유 불변식을 추가하세요.

현재 반려 API는 @Valid와 @NotBlank로 사유를 검증하므로, 현재 producer에서 null이 전달되지는 않습니다. 이벤트를 다른 경로에서도 생성할 수 있다면 approved = false일 때 rejectionReason이 비어 있지 않도록 compact constructor에서 검증하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@BE/src/main/java/com/min/edu/application/service/BoothApplicationDecision.java`
around lines 7 - 13, Update the compact constructor of the
BoothApplicationDecision record to validate that rejectionReason is nonblank
whenever approved is false, and reject invalid event instances consistently
regardless of their creation path. Preserve construction for approved decisions
and valid rejected decisions.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@BE/src/main/java/com/min/edu/application/service/BoothApplicationNotificationListener.java`:
- Around line 41-51: Update the notification creation flow around
NotificationEventDto.create in BoothApplicationNotificationListener to supply a
deterministic eventId derived from decision.applicationId() and the approval
outcome. Ensure identical BoothApplicationDecision values always produce the
same eventId, while different application IDs or approval decisions produce
distinct IDs, so duplicate processing is deduplicated by ON CONFLICT.

In `@BE/src/main/java/com/min/edu/booth/domain/BoothReservation.java`:
- Around line 38-39: Update V1__init_schema.sql to add a NOT NULL TIMESTAMPTZ
created_at column to booth_reservations, set nullable = false on
BoothReservation.createdAt, and initialize createdAt in both reservation
creation builder paths (or through shared initialization) so every persisted
reservation receives a value.

In `@BE/src/main/java/com/min/edu/booth/domain/BoothReview.java`:
- Around line 51-55: 엔티티 필드 BoothReview.memberName 및 BoothReview.updatedAt과
일치하도록 V1__init_schema.sql과 V14__Add_updated_at_to_booth_reviews.sql의
booth_reviews 스키마를 수정하세요. member_name 컬럼을 추가하고 기존 리뷰 데이터에 적용할 NULL 허용 또는 백필 정책을
명확히 반영하며, updated_at은 OffsetDateTime에 맞게 TIMESTAMPTZ로 선언하세요.

In `@BE/src/main/java/com/min/edu/booth/event/BoothVacancyEventListener.java`:
- Around line 33-44: Update BoothVacancyEventListener to stop generating a new
deterministic key via generateDeterministicIdempotencyKey(event) and instead use
the original event key from event.getIdempotencyKey(). Log and pass this value
to vacancyNotificationService.notifyVacancyWithIdempotency so each event retains
its stable reservationId-based identifier.

In `@BE/src/main/java/com/min/edu/booth/repository/BoothQrScanRepository.java`:
- Line 40: BoothQrScanRepository의 세 조회 쿼리에 부스 ID를 보조 정렬 기준으로 추가해 동률 결과의 순서를
고정하세요. BE/src/main/java/com/min/edu/booth/repository/BoothQrScanRepository.java
40행은 congestionCount 내림차순 뒤 bqs.boothId 오름차순을 사용하고, 64행과 85행은 congestionCount
오름차순 뒤 b.id 오름차순을 사용하세요.

In `@BE/src/main/java/com/min/edu/event/service/EventContentService.java`:
- Around line 111-149: 전체 목록 조회를 List 반환에서 Page 계약으로 변경하세요. EventContentService의
listAllContents에 Pageable을 전달하고 findAll(spec, pageable)을 사용해 Page<EventContent>를
Page<EventContentDtos.BoardItem>으로 변환하세요. 해당 메서드를 호출하는 컨트롤러도 page와 size를 받는
Pageable을 서비스에 전달하며, data.content와 data.last를 제공하는 페이지 응답을 유지하세요.

In
`@BE/src/main/java/com/min/edu/notification/repository/NotificationRepository.java`:
- Around line 54-58: Long eventId를 UUID 문자열로 조합해 캐스팅하지 말고,
BE/src/main/java/com/min/edu/notification/repository/NotificationRepository.java
54-58의 insert 경로에서 기존 UUID 파라미터를 받는 insertIfAbsent를 사용하세요.
BE/src/main/java/com/min/edu/booth/service/BoothVacancyNotificationService.java
89-105에서는 idempotencyKey와 memberId를 입력으로 UUID.nameUUIDFromBytes를 사용해 결정적 UUID를
생성하고, 변환 없이 저장소에 전달하세요.

In `@FE/src/pages/Notices.jsx`:
- Around line 66-87: Update the catch path in loadMore to compare requestVersion
with requestVersionRef.current before calling setError, matching the successful
response guard. Ignore errors from stale requests while preserving the existing
error message behavior for the current filter request.

In `@FE/src/pages/OrganizerAdmin.jsx`:
- Around line 696-703: Update the overview statistic displays in OrganizerAdmin,
including totalReservationCount, totalQrScanCount, and the popular-booth counts,
to apply a nullish fallback of 0 before calling toLocaleString. Preserve the
existing "-" fallback when overview itself is unavailable.

---

Outside diff comments:
In `@BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java`:
- Around line 49-56: Update the review creation flow around the BoothReview
builder to populate memberName before saveAndFlush, using the existing member
lookup and preserving the stored author-name snapshot in BoothReviewResponse;
also backfill memberName for existing reviews so list and creation responses do
not return null.

---

Nitpick comments:
In
`@BE/src/main/java/com/min/edu/application/service/BoothApplicationDecision.java`:
- Around line 7-13: Update the compact constructor of the
BoothApplicationDecision record to validate that rejectionReason is nonblank
whenever approved is false, and reject invalid event instances consistently
regardless of their creation path. Preserve construction for approved decisions
and valid rejected decisions.

In
`@BE/src/main/java/com/min/edu/application/service/BoothApplicationNotificationListener.java`:
- Around line 64-69: BoothApplicationNotificationListener의 escapeHtml 구현을 제거하고,
세 서비스가 공유하는 공통 HTML 이스케이프 유틸 또는 기존 HtmlUtils.htmlEscape를 사용하도록 교체하세요. null 처리와
현재 이스케이프 동작을 유지하며, 중복된 서비스별 구현도 동일한 공통 경로로 통일하세요.

In `@BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java`:
- Around line 58-64: BoothReviewService의 saveAndFlush 예외 처리에서 모든
DataIntegrityViolationException을 INVALID_INPUT_VALUE로 변환하지 마세요. 중복 리뷰 제약조건에 해당하는
경우만 BusinessException(GlobalErrorCode.INVALID_INPUT_VALUE)으로 분류하고, 그 외 예외는 원인
정보를 로깅한 뒤 기존 서버 오류 처리 경로로 전파하세요.

In `@FE/src/pages/BoothApplicationList.jsx`:
- Around line 218-227: Update the selected-detail panel rendered by the
selectedId block to include role="dialog" and aria-modal="true", and add an
Escape-key handler that calls closeDetail. Reuse the existing closeDetail
behavior and ensure the handler is attached while the panel is open.
- Around line 107-110: 상세 조회 요청의 catch 처리에서 취소 실패 상태인 cancelError 대신 별도
detailError 상태를 사용하도록 수정하세요. BoothApplicationList의 상태 선언과 상세 조회 오류 메시지를 설정하는
setCancelError 호출을 찾아 detailError 및 해당 setter로 분리하고, 취소 오류 처리 흐름은 기존
cancelError를 계속 사용하세요.

In `@FE/src/pages/BoothApply.jsx`:
- Around line 163-176: Update isEligible to accept an optional filter state,
then have toggleFilter call it with next instead of duplicating the booth
eligibility conditions. Use that shared helper to determine whether
selectedBoothId remains valid and preserve the existing deselection behavior.

In `@FE/src/pages/Notices.jsx`:
- Around line 89-90: Remove the unnecessary filtered alias in the Notices
component and update the related rendering logic to use items directly,
preserving the existing backend-filtered behavior and avoiding any client-side
filtering implication.

In `@FE/src/pages/OrganizerAdmin.jsx`:
- Around line 1109-1128: Update the file-selection validation in the
OrganizerAdmin input handler to reuse the shared helper extracted from
BoothApply.jsx’s validateUploadFile, ensuring both allowed extensions and the 10
MB size limit are checked before setContentFile. Preserve the existing
error/reset behavior and use the helper’s validation result for the displayed
file error.
- Around line 439-467: Update handleStartReview and handleApprove to use a
synchronous useRef-based in-flight guard alongside appActionId, checking and
setting the ref before starting each request so rapid clicks before rerender are
rejected. Clear the ref in each handler’s finally block, while preserving the
existing state updates, error handling, and appActionId reset.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c47a6a28-892b-4715-92b8-251489a72443

📥 Commits

Reviewing files that changed from the base of the PR and between 2bf58d0 and ff994ce.

📒 Files selected for processing (46)
  • BE/build.gradle
  • BE/src/main/java/com/min/edu/application/service/BoothApplicationDecision.java
  • BE/src/main/java/com/min/edu/application/service/BoothApplicationNotificationListener.java
  • BE/src/main/java/com/min/edu/application/service/BoothApplicationService.java
  • BE/src/main/java/com/min/edu/booth/config/AsyncConfiguration.java
  • BE/src/main/java/com/min/edu/booth/controller/BoothReservationController.java
  • BE/src/main/java/com/min/edu/booth/controller/BoothReviewController.java
  • BE/src/main/java/com/min/edu/booth/domain/BoothReservation.java
  • BE/src/main/java/com/min/edu/booth/domain/BoothReview.java
  • BE/src/main/java/com/min/edu/booth/dto/BoothReviewResponse.java
  • BE/src/main/java/com/min/edu/booth/dto/UpdateBoothReviewRequest.java
  • BE/src/main/java/com/min/edu/booth/event/BoothVacancyEvent.java
  • BE/src/main/java/com/min/edu/booth/event/BoothVacancyEventListener.java
  • BE/src/main/java/com/min/edu/booth/repository/BoothQrScanRepository.java
  • BE/src/main/java/com/min/edu/booth/repository/BoothReservationRepository.java
  • BE/src/main/java/com/min/edu/booth/repository/BoothReservationSlotRepository.java
  • BE/src/main/java/com/min/edu/booth/service/BoothCheckInService.java
  • BE/src/main/java/com/min/edu/booth/service/BoothRecommendationService.java
  • BE/src/main/java/com/min/edu/booth/service/BoothReservationWithRedisService.java
  • BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java
  • BE/src/main/java/com/min/edu/booth/service/BoothVacancyNotificationService.java
  • BE/src/main/java/com/min/edu/event/controller/EventContentController.java
  • BE/src/main/java/com/min/edu/event/dto/EventContentDtos.java
  • BE/src/main/java/com/min/edu/event/repository/EventRepository.java
  • BE/src/main/java/com/min/edu/event/service/EventContentService.java
  • BE/src/main/java/com/min/edu/notification/domain/NotificationType.java
  • BE/src/main/java/com/min/edu/notification/repository/NotificationRepository.java
  • BE/src/main/resources/application.properties
  • FE/src/api/boothApplicationApi.js
  • FE/src/api/contentApi.js
  • FE/src/api/fileApi.js
  • FE/src/api/statisticsApi.js
  • FE/src/components/FileDownloadLink.jsx
  • FE/src/components/FileUploadField.jsx
  • FE/src/components/TopNav.jsx
  • FE/src/main.jsx
  • FE/src/notifications/notificationPresentation.js
  • FE/src/pages/BoothApplicationList.jsx
  • FE/src/pages/BoothApply.jsx
  • FE/src/pages/EventDetail.jsx
  • FE/src/pages/MyPage.jsx
  • FE/src/pages/Notices.jsx
  • FE/src/pages/OrganizerAdmin.jsx
  • FE/src/pages/RecruitmentDetail.jsx
  • k8s/backend.yaml
  • k8s/frontend.yaml
💤 Files with no reviewable changes (1)
  • BE/src/main/java/com/min/edu/booth/repository/BoothReservationRepository.java
🚧 Files skipped from review as they are similar to previous changes (6)
  • BE/src/main/java/com/min/edu/booth/dto/UpdateBoothReviewRequest.java
  • BE/src/main/java/com/min/edu/booth/controller/BoothReservationController.java
  • BE/src/main/java/com/min/edu/booth/service/BoothRecommendationService.java
  • BE/src/main/java/com/min/edu/booth/controller/BoothReviewController.java
  • BE/src/main/java/com/min/edu/booth/service/BoothReservationWithRedisService.java
  • BE/src/main/java/com/min/edu/booth/service/BoothCheckInService.java

Comment on lines +38 to +39
@Column(name = "created_at")
private OffsetDateTime createdAt;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 'BoothReservation\.builder|\.createdAt\(' BE/src/main/java
rg -n -C 4 'created_at' BE/src/main/resources/db/migration

Repository: half-fifty/eventoday

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- BoothReservation.java ---'
sed -n '1,180p' BE/src/main/java/com/min/edu/booth/domain/BoothReservation.java

echo '--- BoothReservation 생성 호출부 ---'
rg -n -A 14 -B 3 'BoothReservation\.builder\(' BE/src/main/java

echo '--- booth_reservations 스키마 정의 ---'
rg -n -A 24 -B 4 'CREATE TABLE booth_reservations|created_at' BE/src/main/resources/db/migration/V1__init_schema.sql \
  | rg -n -A 24 -B 4 'booth_reservations|created_at' | head -n 120

Repository: half-fifty/eventoday

Length of output: 12055


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- booth_reservations 테이블 정의 ---'
line="$(rg -n 'CREATE TABLE booth_reservations' BE/src/main/resources/db/migration/V1__init_schema.sql | cut -d: -f1)"
if [ -z "$line" ]; then
  echo 'booth_reservations 테이블 정의를 찾지 못했습니다.'
  exit 0
fi
start=$((line))
end=$((line + 28))
sed -n "${start},${end}p" BE/src/main/resources/db/migration/V1__init_schema.sql

echo '--- booth_reservations 관련 후속 마이그레이션 ---'
rg -n -C 3 'booth_reservations|created_at' BE/src/main/resources/db/migration \
  | rg 'booth_reservations|created_at|V[0-9]+__' | tail -n 120

echo '--- 예약 생성 서비스의 트랜잭션 경계 ---'
rg -n -A 8 -B 8 '`@Transactional`|saveAndFlush|createReservation' \
  BE/src/main/java/com/min/edu/booth/service/BoothReservationService.java \
  BE/src/main/java/com/min/edu/booth/service/BoothReservationWithRedisService.java

Repository: half-fifty/eventoday

Length of output: 35260


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- JPA 스키마 생성 설정 ---'
rg -n -C 3 'ddl-auto|flyway|database-platform|jpa.hibernate' \
  BE/src/main/resources BE/src/main/java BE/build.gradle* BE/pom.xml 2>/dev/null || true

echo '--- created_at 컬럼을 추가하는 마이그레이션 ---'
rg -n -C 4 'ALTER TABLE\s+booth_reservations|ADD COLUMN\s+created_at|created_at.*booth_reservations|booth_reservations.*created_at' \
  BE/src/main/resources/db/migration || true

echo '--- 예약 응답의 createdAt 사용 여부 ---'
rg -n -C 4 'getCreatedAt|createdAt' \
  BE/src/main/java/com/min/edu/booth \
  | head -n 160

Repository: half-fifty/eventoday

Length of output: 19312


booth_reservations.created_at 스키마와 생성 로직을 함께 반영하세요.

BoothReservation은 created_at을 매핑하지만 V1__init_schema.sql의 booth_reservations에는 해당 컬럼이 없습니다. spring.jpa.hibernate.ddl-auto=validate 설정으로 애플리케이션이 시작되지 않습니다. 또한 두 예약 생성 경로에서 createdAt을 설정하지 않습니다. Flyway 마이그레이션에 created_at TIMESTAMPTZ NOT NULL을 추가하고, 엔티티에 nullable = false를 지정한 뒤 중앙 초기화 또는 두 builder의 값 설정을 적용하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/domain/BoothReservation.java` around lines
38 - 39, Update V1__init_schema.sql to add a NOT NULL TIMESTAMPTZ created_at
column to booth_reservations, set nullable = false on
BoothReservation.createdAt, and initialize createdAt in both reservation
creation builder paths (or through shared initialization) so every persisted
reservation receives a value.

Source: Path instructions

Comment on lines +51 to +55
@Column(name = "updated_at", nullable = false)
private OffsetDateTime updatedAt;

@Column(name = "member_name")
private String memberName;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 'updated_at|member_name' BE/src/main/resources/db/migration
rg -n -C 4 'BoothReview\.builder|\.updatedAt\(|\.memberName\(' \
  BE/src/main/java/com/min/edu/booth

Repository: half-fifty/eventoday

Length of output: 28430


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- migration files ---'
git ls-files 'BE/src/main/resources/db/migration/*' | sort

echo '--- booth_reviews schema ---'
rg -n -C 12 'CREATE TABLE booth_reviews|ALTER TABLE booth_reviews|member_name|updated_at' \
  BE/src/main/resources/db/migration

echo '--- BoothReview entity ---'
cat -n BE/src/main/java/com/min/edu/booth/domain/BoothReview.java

echo '--- BoothReviewService relevant sections ---'
cat -n BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java | sed -n '1,230p'

echo '--- all memberName references ---'
rg -n -C 3 'memberName|member_name' BE/src/main/java BE/src/main/resources

Repository: half-fifty/eventoday

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- JPA and datasource schema settings ---'
rg -n -C 2 'ddl-auto|database-platform|hibernate|datasource|jdbc-url|url:' \
  BE/src/main/resources BE --glob '*.yml' --glob '*.yaml' --glob '*.properties' --glob 'build.gradle*' --glob 'pom.xml'

echo '--- timestamp column conventions ---'
rg -n 'TIMESTAMP( WITH TIME ZONE)?|TIMESTAMPTZ' BE/src/main/resources/db/migration

echo '--- focused schema facts ---'
python3 - <<'PY'
from pathlib import Path
import re

root = Path("BE/src/main/resources/db/migration")
schema = (root / "V1__init_schema.sql").read_text()
v14 = (root / "V14__Add_updated_at_to_booth_reviews.sql").read_text()

match = re.search(r"CREATE TABLE booth_reviews\s*\((.*?)\n\);", schema, re.S)
if not match:
    raise SystemExit("booth_reviews definition not found")
columns = re.findall(r"^\s*([a-z_]+)\s+", match.group(1), re.M)
print("V1 booth_reviews columns:", columns)
print("V14 adds updated_at:", bool(re.search(r"ALTER TABLE\s+booth_reviews\s+ADD COLUMN\s+updated_at\b", v14, re.I)))
print("V14 adds member_name:", bool(re.search(r"ALTER TABLE\s+booth_reviews\s+ADD COLUMN\s+member_name\b", v14, re.I)))
print("V14 updated_at declaration:", v14.strip())
PY

Repository: half-fifty/eventoday

Length of output: 10110


booth_reviews 마이그레이션을 엔티티 매핑과 일치시키세요.

spring.jpa.hibernate.ddl-auto=validate 설정에서 V1__init_schema.sql과 V14__Add_updated_at_to_booth_reviews.sql에 member_name 컬럼이 없습니다. BoothReview.memberName 매핑 때문에 애플리케이션 기동 시 스키마 검증이 실패합니다. 또한 OffsetDateTime에 맞게 updated_at은 TIMESTAMPTZ로 선언하고, 기존 리뷰의 member_name 처리 방식을 정해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/domain/BoothReview.java` around lines 51 -
55, 엔티티 필드 BoothReview.memberName 및 BoothReview.updatedAt과 일치하도록
V1__init_schema.sql과 V14__Add_updated_at_to_booth_reviews.sql의 booth_reviews
스키마를 수정하세요. member_name 컬럼을 추가하고 기존 리뷰 데이터에 적용할 NULL 허용 또는 백필 정책을 명확히 반영하며,
updated_at은 OffsetDateTime에 맞게 TIMESTAMPTZ로 선언하세요.

Source: Path instructions

Comment on lines +33 to +44
// 결정적 Idempotency Key 생성 (Long 타입)
Long deterministicIdempotencyKey = generateDeterministicIdempotencyKey(event);

log.info("Processing booth vacancy notification - boothId: {}, slotId: {}, idempotencyKey: {}",
event.getBoothId(), event.getSlotId(), deterministicIdempotencyKey);

vacancyNotificationService.notifyVacancyWithIdempotency(
event.getBoothId(),
event.getDisplayName(),
event.getSlotId(),
deterministicIdempotencyKey // ← Long 타입
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

이벤트가 제공한 idempotencyKey를 그대로 사용하세요.

Line 34는 예약 취소 이벤트의 원본 idempotencyKey를 버리고 32비트 String.hashCode()를 새로 만듭니다. 서로 다른 이벤트가 충돌하면 같은 회원의 eventId가 같아지고, ON CONFLICT가 뒤 이벤트의 알림을 누락시킵니다.

Line 39에서는 event.getIdempotencyKey()를 전달하세요. BoothVacancyEvent를 생성할 때 전달한 reservationId는 이 이벤트의 안정적인 식별자입니다.

수정 예시
- Long deterministicIdempotencyKey = generateDeterministicIdempotencyKey(event);
+ Long idempotencyKey = event.getIdempotencyKey();

  vacancyNotificationService.notifyVacancyWithIdempotency(
      event.getBoothId(),
      event.getDisplayName(),
      event.getSlotId(),
-     deterministicIdempotencyKey
+     idempotencyKey
  );

As per path instructions, '중복 요청과 동시성 문제'를 중점 확인했습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/event/BoothVacancyEventListener.java`
around lines 33 - 44, Update BoothVacancyEventListener to stop generating a new
deterministic key via generateDeterministicIdempotencyKey(event) and instead use
the original event key from event.getIdempotencyKey(). Log and pass this value
to vacancyNotificationService.notifyVacancyWithIdempotency so each event retains
its stable reservationId-based identifier.

Source: Path instructions

"AND bqs.scannedAt >= :since " +
"AND bqs.duplicate = false " +
"GROUP BY bqs.boothId " +
"ORDER BY congestionCount DESC",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

동률 정렬 기준을 추가해 페이지 순서를 고정하세요.

congestionCount가 같은 부스의 순서는 보장되지 않습니다. 페이지 조회에서는 같은 부스가 다른 페이지에 중복되거나 일부 부스가 누락될 수 있습니다. 추천 응답의 rank도 요청마다 달라질 수 있습니다.

  • BE/src/main/java/com/min/edu/booth/repository/BoothQrScanRepository.java#L40-L40: ORDER BY congestionCount DESC, bqs.boothId ASC처럼 부스 ID 보조 정렬을 추가하세요.
  • BE/src/main/java/com/min/edu/booth/repository/BoothQrScanRepository.java#L64-L64: ORDER BY congestionCount ASC, b.id ASC처럼 부스 ID 보조 정렬을 추가하세요.
  • BE/src/main/java/com/min/edu/booth/repository/BoothQrScanRepository.java#L85-L85: ORDER BY congestionCount ASC, b.id ASC처럼 부스 ID 보조 정렬을 추가하세요.
📍 Affects 1 file
  • BE/src/main/java/com/min/edu/booth/repository/BoothQrScanRepository.java#L40-L40 (this comment)
  • BE/src/main/java/com/min/edu/booth/repository/BoothQrScanRepository.java#L64-L64
  • BE/src/main/java/com/min/edu/booth/repository/BoothQrScanRepository.java#L85-L85
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/repository/BoothQrScanRepository.java` at
line 40, BoothQrScanRepository의 세 조회 쿼리에 부스 ID를 보조 정렬 기준으로 추가해 동률 결과의 순서를 고정하세요.
BE/src/main/java/com/min/edu/booth/repository/BoothQrScanRepository.java 40행은
congestionCount 내림차순 뒤 bqs.boothId 오름차순을 사용하고, 64행과 85행은 congestionCount 오름차순 뒤
b.id 오름차순을 사용하세요.

Comment on lines +54 to +58
CAST(:eventId || '-0000-0000-0000-000000000000' AS uuid),
:memberId, :notificationType, :referenceType,
:referenceId, :title, :content, :createdAt
)
ON CONFLICT (event_id) DO NOTHING

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Long eventId를 UUID 문자열로 잘못 변환합니다.

generateDeterministicEventId는 임의의 64비트 Long을 반환합니다. 그러나 Line 54는 그 값을 10진 문자열로 UUID의 첫 그룹에 붙입니다. 생성 값은 대부분 18~19자리이므로 UUID 형식이 아니며 PostgreSQL 캐스트가 실패합니다. 리스너가 예외를 소비하므로 예약 취소는 성공하지만 빈자리 알림은 유실됩니다.

  • BE/src/main/java/com/min/edu/notification/repository/NotificationRepository.java#L54-L58: Long 문자열 조합과 UUID 캐스트를 제거하세요. UUID 파라미터를 받는 기존 insertIfAbsent를 사용하세요.
  • BE/src/main/java/com/min/edu/booth/service/BoothVacancyNotificationService.java#L89-L105: 결정적 식별자를 UUID로 생성하세요. 예를 들어 idempotencyKey와 memberId에서 UUID.nameUUIDFromBytes를 생성하고 저장소에 그대로 전달하세요.

As per path instructions, '예외 처리 누락'과 '중복 요청과 동시성 문제'를 중점 확인했습니다.

📍 Affects 2 files
  • BE/src/main/java/com/min/edu/notification/repository/NotificationRepository.java#L54-L58 (this comment)
  • BE/src/main/java/com/min/edu/booth/service/BoothVacancyNotificationService.java#L89-L105
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@BE/src/main/java/com/min/edu/notification/repository/NotificationRepository.java`
around lines 54 - 58, Long eventId를 UUID 문자열로 조합해 캐스팅하지 말고,
BE/src/main/java/com/min/edu/notification/repository/NotificationRepository.java
54-58의 insert 경로에서 기존 UUID 파라미터를 받는 insertIfAbsent를 사용하세요.
BE/src/main/java/com/min/edu/booth/service/BoothVacancyNotificationService.java
89-105에서는 idempotencyKey와 memberId를 입력으로 UUID.nameUUIDFromBytes를 사용해 결정적 UUID를
생성하고, 변환 없이 저장소에 전달하세요.

Source: Path instructions

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java (1)

49-56: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

리뷰 생성 시 memberName을 저장하세요.

BoothReviewResponse는 Line 182에서 review.getMemberName()을 반환하지만, 이 builder에는 .memberName(...) 할당이 없습니다. saveAndFlush만으로 회원명이 채워지지 않으므로 생성 응답과 이후 리뷰 목록에서 작성자명이 null이 될 수 있습니다. 회원명 스냅샷을 저장한다면 생성 전에 값을 조회하고, 기존 리뷰도 백필하세요. 실시간 회원명을 보여주는 설계라면 응답 조회를 join 또는 projection으로 변경하세요.

경로 지침의 비즈니스 로직 및 데이터 무결성 점검 항목에 따른 의견입니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java` around
lines 49 - 56, Update the review creation flow around the BoothReview builder to
populate memberName before saveAndFlush, using the existing member lookup and
preserving the stored author-name snapshot in BoothReviewResponse; also backfill
memberName for existing reviews so list and creation responses do not return
null.

Source: Path instructions

🧹 Nitpick comments (9)
FE/src/pages/OrganizerAdmin.jsx (2)

1109-1128: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

파일 확장자만 검사하고 크기는 검사하지 않습니다.

바로 위 안내 문구에는 "최대 10MB"라고 적혀 있습니다. 그러나 검증은 확장자만 합니다. 큰 파일을 고르면 업로드 후 서버에서 실패합니다. BoothApply.jsx의 validateUploadFile이 확장자와 크기를 모두 검사하므로, 그 헬퍼를 공용 모듈로 빼서 함께 쓰는 편이 좋겠습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FE/src/pages/OrganizerAdmin.jsx` around lines 1109 - 1128, Update the
file-selection validation in the OrganizerAdmin input handler to reuse the
shared helper extracted from BoothApply.jsx’s validateUploadFile, ensuring both
allowed extensions and the 10 MB size limit are checked before setContentFile.
Preserve the existing error/reset behavior and use the helper’s validation
result for the displayed file error.

439-467: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

appActionId 가드는 빠른 연속 클릭을 완전히 막지 못합니다.

if (appActionId) return; 다음에 setAppActionId(...)를 호출합니다. 상태 반영과 disabled 적용은 다음 렌더에서 일어납니다. 리렌더 전에 두 번째 클릭이 들어오면 두 핸들러가 모두 통과합니다. 승인은 부스 배정을 동반하니 클라이언트에서도 확실히 막는 편이 좋습니다. 서버가 UNDER_REVIEW 상태 검증과 비관적 락으로 두 번째 요청을 거부하므로 데이터가 깨지지는 않지만, 사용자에게는 불필요한 에러가 보입니다. useRef 기반 가드를 함께 쓰면 해결됩니다.

♻️ 제안 diff
+  // 상태 반영 전 연속 클릭까지 막기 위한 동기 가드
+  const appActionRef = useRef(null);
+
   const handleStartReview = async (applicationId) => {
-    if (appActionId) return; // 이미 처리 중인 요청이 있으면 무시
+    if (appActionRef.current) return; // 이미 처리 중인 요청이 있으면 무시
+    appActionRef.current = applicationId;
     setAppActionId(applicationId);
     try {
       await startReview(applicationId);
       patchApplicationStatus(applicationId, "UNDER_REVIEW");
       refreshAppCounts(); // 검토 대기 카운트 재조회
     } catch (error) {
       setAppError(error.message || "검토 시작에 실패했습니다.");
     } finally {
+      appActionRef.current = null;
       setAppActionId(null);
     }
   };
 
   const handleApprove = async (applicationId) => {
-    if (appActionId) return;
+    if (appActionRef.current) return;
     if (!window.confirm("이 신청을 승인하시겠습니까? 승인 시 부스가 자동 배정됩니다.")) return;
+    appActionRef.current = applicationId;
     setAppActionId(applicationId);
     try {
       await approveApplication(applicationId);
       patchApplicationStatus(applicationId, "APPROVED");
       refreshAppCounts(); // 검토 대기·승인 카운트 재조회
     } catch (error) {
       setAppError(error.message || "승인에 실패했습니다.");
     } finally {
+      appActionRef.current = null;
       setAppActionId(null);
     }
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FE/src/pages/OrganizerAdmin.jsx` around lines 439 - 467, Update
handleStartReview and handleApprove to use a synchronous useRef-based in-flight
guard alongside appActionId, checking and setting the ref before starting each
request so rapid clicks before rerender are rejected. Clear the ref in each
handler’s finally block, while preserving the existing state updates, error
handling, and appActionId reset.
FE/src/pages/BoothApplicationList.jsx (2)

218-227: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

상세 패널을 Esc 키로 닫을 수 없습니다.

오버레이 클릭과 닫기 버튼만 있습니다. 키보드 사용자는 패널을 닫기 어렵습니다. role="dialog", aria-modal="true"와 Esc 키 핸들러를 추가하면 좋겠습니다. 같은 PR의 OrganizerAdmin.jsx 모달은 role="dialog"를 이미 쓰고 있어 패턴을 맞추기도 쉽습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FE/src/pages/BoothApplicationList.jsx` around lines 218 - 227, Update the
selected-detail panel rendered by the selectedId block to include role="dialog"
and aria-modal="true", and add an Escape-key handler that calls closeDetail.
Reuse the existing closeDetail behavior and ensure the handler is attached while
the panel is open.

107-110: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

상세 조회 실패를 cancelError에 담고 있습니다.

cancelError는 취소 실패용 상태입니다. 상세 조회 실패까지 같은 상태를 쓰면, 나중에 두 에러가 동시에 발생하는 경우 메시지가 서로 덮어씁니다. 상태 이름과 의미도 어긋납니다. detailError를 따로 두는 편이 읽기 쉽습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FE/src/pages/BoothApplicationList.jsx` around lines 107 - 110, 상세 조회 요청의
catch 처리에서 취소 실패 상태인 cancelError 대신 별도 detailError 상태를 사용하도록 수정하세요.
BoothApplicationList의 상태 선언과 상세 조회 오류 메시지를 설정하는 setCancelError 호출을 찾아
detailError 및 해당 setter로 분리하고, 취소 오류 처리 흐름은 기존 cancelError를 계속 사용하세요.
FE/src/pages/BoothApply.jsx (1)

163-176: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

toggleFilter 안에 isEligible 로직이 다시 구현되어 있습니다.

isEligible과 동일한 조건이 next 필터 기준으로 복제되어 있습니다. 조건이 하나만 추가되면 두 곳을 같이 고쳐야 합니다. isEligible이 필터를 인자로 받게 만들면 중복이 없어집니다.

♻️ 제안 diff
-  const isEligible = (booth) =>
+  const isEligible = (booth, f = filters) =>
     booth.status === "AVAILABLE" &&
-    (!filters.elec || booth.electricityAvailable) &&
-    (!filters.water || booth.waterAvailable);
+    (!f.elec || booth.electricityAvailable) &&
+    (!f.water || booth.waterAvailable);
@@
     if (selectedBoothId) {
       const booth = booths.find((b) => b.id === selectedBoothId);
-      const stillEligible =
-        booth &&
-        booth.status === "AVAILABLE" &&
-        (!next.elec || booth.electricityAvailable) &&
-        (!next.water || booth.waterAvailable);
-      if (!stillEligible) setSelectedBoothId(null);
+      if (!booth || !isEligible(booth, next)) setSelectedBoothId(null);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FE/src/pages/BoothApply.jsx` around lines 163 - 176, Update isEligible to
accept an optional filter state, then have toggleFilter call it with next
instead of duplicating the booth eligibility conditions. Use that shared helper
to determine whether selectedBoothId remains valid and preserve the existing
deselection behavior.
BE/src/main/java/com/min/edu/application/service/BoothApplicationNotificationListener.java (1)

64-69: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

escapeHtml을 공통 유틸로 추출하세요.

동일한 구현이 세 서비스에 중복되어 있습니다. 공통 유틸 또는 HtmlUtils.htmlEscape를 사용하면 수정 누락으로 이스케이프 동작이 달라지는 문제를 방지할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@BE/src/main/java/com/min/edu/application/service/BoothApplicationNotificationListener.java`
around lines 64 - 69, BoothApplicationNotificationListener의 escapeHtml 구현을 제거하고,
세 서비스가 공유하는 공통 HTML 이스케이프 유틸 또는 기존 HtmlUtils.htmlEscape를 사용하도록 교체하세요. null 처리와
현재 이스케이프 동작을 유지하며, 중복된 서비스별 구현도 동일한 공통 경로로 통일하세요.
FE/src/pages/Notices.jsx (1)

89-90: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

filtered 별칭은 이제 필요하지 않습니다.

필터링을 BE로 옮겼으므로 filtered는 items와 항상 같습니다. 아래 렌더링에서 items를 직접 쓰면 "클라이언트에서도 한 번 더 거르나?"라는 오해를 줄일 수 있습니다. 급한 사안은 아니니 정리할 때 함께 반영해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FE/src/pages/Notices.jsx` around lines 89 - 90, Remove the unnecessary
filtered alias in the Notices component and update the related rendering logic
to use items directly, preserving the existing backend-filtered behavior and
avoiding any client-side filtering implication.
BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java (1)

58-64: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

무결성 예외를 모두 입력 오류로 변환하지 마세요.

DataIntegrityViolationException에는 중복 키뿐 아니라 null 제약조건, 길이 초과, 외래 키, 스키마 오류도 포함될 수 있습니다. 현재 구현은 서버 또는 스키마 오류를 INVALID_INPUT_VALUE로 응답하게 만듭니다. 중복 리뷰 제약조건만 분류하고 나머지는 원인 로그와 서버 오류 경로로 전달하세요.

경로 지침의 예외 처리 및 데이터 무결성 점검 항목에 따른 의견입니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java` around
lines 58 - 64, BoothReviewService의 saveAndFlush 예외 처리에서 모든
DataIntegrityViolationException을 INVALID_INPUT_VALUE로 변환하지 마세요. 중복 리뷰 제약조건에 해당하는
경우만 BusinessException(GlobalErrorCode.INVALID_INPUT_VALUE)으로 분류하고, 그 외 예외는 원인
정보를 로깅한 뒤 기존 서버 오류 처리 경로로 전파하세요.

Source: Path instructions

BE/src/main/java/com/min/edu/application/service/BoothApplicationDecision.java (1)

7-13: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

이벤트 계약에 반려 사유 불변식을 추가하세요.

현재 반려 API는 @Valid와 @NotBlank로 사유를 검증하므로, 현재 producer에서 null이 전달되지는 않습니다. 이벤트를 다른 경로에서도 생성할 수 있다면 approved = false일 때 rejectionReason이 비어 있지 않도록 compact constructor에서 검증하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@BE/src/main/java/com/min/edu/application/service/BoothApplicationDecision.java`
around lines 7 - 13, Update the compact constructor of the
BoothApplicationDecision record to validate that rejectionReason is nonblank
whenever approved is false, and reject invalid event instances consistently
regardless of their creation path. Preserve construction for approved decisions
and valid rejected decisions.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@BE/src/main/java/com/min/edu/application/service/BoothApplicationNotificationListener.java`:
- Around line 41-51: Update the notification creation flow around
NotificationEventDto.create in BoothApplicationNotificationListener to supply a
deterministic eventId derived from decision.applicationId() and the approval
outcome. Ensure identical BoothApplicationDecision values always produce the
same eventId, while different application IDs or approval decisions produce
distinct IDs, so duplicate processing is deduplicated by ON CONFLICT.

In `@BE/src/main/java/com/min/edu/booth/domain/BoothReservation.java`:
- Around line 38-39: Update V1__init_schema.sql to add a NOT NULL TIMESTAMPTZ
created_at column to booth_reservations, set nullable = false on
BoothReservation.createdAt, and initialize createdAt in both reservation
creation builder paths (or through shared initialization) so every persisted
reservation receives a value.

In `@BE/src/main/java/com/min/edu/booth/domain/BoothReview.java`:
- Around line 51-55: 엔티티 필드 BoothReview.memberName 및 BoothReview.updatedAt과
일치하도록 V1__init_schema.sql과 V14__Add_updated_at_to_booth_reviews.sql의
booth_reviews 스키마를 수정하세요. member_name 컬럼을 추가하고 기존 리뷰 데이터에 적용할 NULL 허용 또는 백필 정책을
명확히 반영하며, updated_at은 OffsetDateTime에 맞게 TIMESTAMPTZ로 선언하세요.

In `@BE/src/main/java/com/min/edu/booth/event/BoothVacancyEventListener.java`:
- Around line 33-44: Update BoothVacancyEventListener to stop generating a new
deterministic key via generateDeterministicIdempotencyKey(event) and instead use
the original event key from event.getIdempotencyKey(). Log and pass this value
to vacancyNotificationService.notifyVacancyWithIdempotency so each event retains
its stable reservationId-based identifier.

In `@BE/src/main/java/com/min/edu/booth/repository/BoothQrScanRepository.java`:
- Line 40: BoothQrScanRepository의 세 조회 쿼리에 부스 ID를 보조 정렬 기준으로 추가해 동률 결과의 순서를
고정하세요. BE/src/main/java/com/min/edu/booth/repository/BoothQrScanRepository.java
40행은 congestionCount 내림차순 뒤 bqs.boothId 오름차순을 사용하고, 64행과 85행은 congestionCount
오름차순 뒤 b.id 오름차순을 사용하세요.

In `@BE/src/main/java/com/min/edu/event/service/EventContentService.java`:
- Around line 111-149: 전체 목록 조회를 List 반환에서 Page 계약으로 변경하세요. EventContentService의
listAllContents에 Pageable을 전달하고 findAll(spec, pageable)을 사용해 Page<EventContent>를
Page<EventContentDtos.BoardItem>으로 변환하세요. 해당 메서드를 호출하는 컨트롤러도 page와 size를 받는
Pageable을 서비스에 전달하며, data.content와 data.last를 제공하는 페이지 응답을 유지하세요.

In
`@BE/src/main/java/com/min/edu/notification/repository/NotificationRepository.java`:
- Around line 54-58: Long eventId를 UUID 문자열로 조합해 캐스팅하지 말고,
BE/src/main/java/com/min/edu/notification/repository/NotificationRepository.java
54-58의 insert 경로에서 기존 UUID 파라미터를 받는 insertIfAbsent를 사용하세요.
BE/src/main/java/com/min/edu/booth/service/BoothVacancyNotificationService.java
89-105에서는 idempotencyKey와 memberId를 입력으로 UUID.nameUUIDFromBytes를 사용해 결정적 UUID를
생성하고, 변환 없이 저장소에 전달하세요.

In `@FE/src/pages/Notices.jsx`:
- Around line 66-87: Update the catch path in loadMore to compare requestVersion
with requestVersionRef.current before calling setError, matching the successful
response guard. Ignore errors from stale requests while preserving the existing
error message behavior for the current filter request.

In `@FE/src/pages/OrganizerAdmin.jsx`:
- Around line 696-703: Update the overview statistic displays in OrganizerAdmin,
including totalReservationCount, totalQrScanCount, and the popular-booth counts,
to apply a nullish fallback of 0 before calling toLocaleString. Preserve the
existing "-" fallback when overview itself is unavailable.

---

Outside diff comments:
In `@BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java`:
- Around line 49-56: Update the review creation flow around the BoothReview
builder to populate memberName before saveAndFlush, using the existing member
lookup and preserving the stored author-name snapshot in BoothReviewResponse;
also backfill memberName for existing reviews so list and creation responses do
not return null.

---

Nitpick comments:
In
`@BE/src/main/java/com/min/edu/application/service/BoothApplicationDecision.java`:
- Around line 7-13: Update the compact constructor of the
BoothApplicationDecision record to validate that rejectionReason is nonblank
whenever approved is false, and reject invalid event instances consistently
regardless of their creation path. Preserve construction for approved decisions
and valid rejected decisions.

In
`@BE/src/main/java/com/min/edu/application/service/BoothApplicationNotificationListener.java`:
- Around line 64-69: BoothApplicationNotificationListener의 escapeHtml 구현을 제거하고,
세 서비스가 공유하는 공통 HTML 이스케이프 유틸 또는 기존 HtmlUtils.htmlEscape를 사용하도록 교체하세요. null 처리와
현재 이스케이프 동작을 유지하며, 중복된 서비스별 구현도 동일한 공통 경로로 통일하세요.

In `@BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java`:
- Around line 58-64: BoothReviewService의 saveAndFlush 예외 처리에서 모든
DataIntegrityViolationException을 INVALID_INPUT_VALUE로 변환하지 마세요. 중복 리뷰 제약조건에 해당하는
경우만 BusinessException(GlobalErrorCode.INVALID_INPUT_VALUE)으로 분류하고, 그 외 예외는 원인
정보를 로깅한 뒤 기존 서버 오류 처리 경로로 전파하세요.

In `@FE/src/pages/BoothApplicationList.jsx`:
- Around line 218-227: Update the selected-detail panel rendered by the
selectedId block to include role="dialog" and aria-modal="true", and add an
Escape-key handler that calls closeDetail. Reuse the existing closeDetail
behavior and ensure the handler is attached while the panel is open.
- Around line 107-110: 상세 조회 요청의 catch 처리에서 취소 실패 상태인 cancelError 대신 별도
detailError 상태를 사용하도록 수정하세요. BoothApplicationList의 상태 선언과 상세 조회 오류 메시지를 설정하는
setCancelError 호출을 찾아 detailError 및 해당 setter로 분리하고, 취소 오류 처리 흐름은 기존
cancelError를 계속 사용하세요.

In `@FE/src/pages/BoothApply.jsx`:
- Around line 163-176: Update isEligible to accept an optional filter state,
then have toggleFilter call it with next instead of duplicating the booth
eligibility conditions. Use that shared helper to determine whether
selectedBoothId remains valid and preserve the existing deselection behavior.

In `@FE/src/pages/Notices.jsx`:
- Around line 89-90: Remove the unnecessary filtered alias in the Notices
component and update the related rendering logic to use items directly,
preserving the existing backend-filtered behavior and avoiding any client-side
filtering implication.

In `@FE/src/pages/OrganizerAdmin.jsx`:
- Around line 1109-1128: Update the file-selection validation in the
OrganizerAdmin input handler to reuse the shared helper extracted from
BoothApply.jsx’s validateUploadFile, ensuring both allowed extensions and the 10
MB size limit are checked before setContentFile. Preserve the existing
error/reset behavior and use the helper’s validation result for the displayed
file error.
- Around line 439-467: Update handleStartReview and handleApprove to use a
synchronous useRef-based in-flight guard alongside appActionId, checking and
setting the ref before starting each request so rapid clicks before rerender are
rejected. Clear the ref in each handler’s finally block, while preserving the
existing state updates, error handling, and appActionId reset.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c47a6a28-892b-4715-92b8-251489a72443

📥 Commits

Reviewing files that changed from the base of the PR and between 2bf58d0 and ff994ce.

📒 Files selected for processing (46)
  • BE/build.gradle
  • BE/src/main/java/com/min/edu/application/service/BoothApplicationDecision.java
  • BE/src/main/java/com/min/edu/application/service/BoothApplicationNotificationListener.java
  • BE/src/main/java/com/min/edu/application/service/BoothApplicationService.java
  • BE/src/main/java/com/min/edu/booth/config/AsyncConfiguration.java
  • BE/src/main/java/com/min/edu/booth/controller/BoothReservationController.java
  • BE/src/main/java/com/min/edu/booth/controller/BoothReviewController.java
  • BE/src/main/java/com/min/edu/booth/domain/BoothReservation.java
  • BE/src/main/java/com/min/edu/booth/domain/BoothReview.java
  • BE/src/main/java/com/min/edu/booth/dto/BoothReviewResponse.java
  • BE/src/main/java/com/min/edu/booth/dto/UpdateBoothReviewRequest.java
  • BE/src/main/java/com/min/edu/booth/event/BoothVacancyEvent.java
  • BE/src/main/java/com/min/edu/booth/event/BoothVacancyEventListener.java
  • BE/src/main/java/com/min/edu/booth/repository/BoothQrScanRepository.java
  • BE/src/main/java/com/min/edu/booth/repository/BoothReservationRepository.java
  • BE/src/main/java/com/min/edu/booth/repository/BoothReservationSlotRepository.java
  • BE/src/main/java/com/min/edu/booth/service/BoothCheckInService.java
  • BE/src/main/java/com/min/edu/booth/service/BoothRecommendationService.java
  • BE/src/main/java/com/min/edu/booth/service/BoothReservationWithRedisService.java
  • BE/src/main/java/com/min/edu/booth/service/BoothReviewService.java
  • BE/src/main/java/com/min/edu/booth/service/BoothVacancyNotificationService.java
  • BE/src/main/java/com/min/edu/event/controller/EventContentController.java
  • BE/src/main/java/com/min/edu/event/dto/EventContentDtos.java
  • BE/src/main/java/com/min/edu/event/repository/EventRepository.java
  • BE/src/main/java/com/min/edu/event/service/EventContentService.java
  • BE/src/main/java/com/min/edu/notification/domain/NotificationType.java
  • BE/src/main/java/com/min/edu/notification/repository/NotificationRepository.java
  • BE/src/main/resources/application.properties
  • FE/src/api/boothApplicationApi.js
  • FE/src/api/contentApi.js
  • FE/src/api/fileApi.js
  • FE/src/api/statisticsApi.js
  • FE/src/components/FileDownloadLink.jsx
  • FE/src/components/FileUploadField.jsx
  • FE/src/components/TopNav.jsx
  • FE/src/main.jsx
  • FE/src/notifications/notificationPresentation.js
  • FE/src/pages/BoothApplicationList.jsx
  • FE/src/pages/BoothApply.jsx
  • FE/src/pages/EventDetail.jsx
  • FE/src/pages/MyPage.jsx
  • FE/src/pages/Notices.jsx
  • FE/src/pages/OrganizerAdmin.jsx
  • FE/src/pages/RecruitmentDetail.jsx
  • k8s/backend.yaml
  • k8s/frontend.yaml
💤 Files with no reviewable changes (1)
  • BE/src/main/java/com/min/edu/booth/repository/BoothReservationRepository.java
🚧 Files skipped from review as they are similar to previous changes (6)
  • BE/src/main/java/com/min/edu/booth/dto/UpdateBoothReviewRequest.java
  • BE/src/main/java/com/min/edu/booth/controller/BoothReservationController.java
  • BE/src/main/java/com/min/edu/booth/service/BoothRecommendationService.java
  • BE/src/main/java/com/min/edu/booth/controller/BoothReviewController.java
  • BE/src/main/java/com/min/edu/booth/service/BoothReservationWithRedisService.java
  • BE/src/main/java/com/min/edu/booth/service/BoothCheckInService.java
🛑 Comments failed to post (4)
BE/src/main/java/com/min/edu/application/service/BoothApplicationNotificationListener.java (1)

41-51: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# NotificationEventDto.create 오버로드와 idempotencyKey 사용 패턴 확인
fd -g 'NotificationEventDto.java' --exec cat -n {} \;
rg -n --type=java -C2 'NotificationEventDto\.create\('

Repository: half-fifty/eventoday

Length of output: 1226


🏁 Script executed:

#!/bin/bash
set -eu

echo '--- 후보 파일 ---'
fd -i -t f 'BoothApplicationNotificationListener|.*Notification.*Listener|NotificationEventDto|NotificationProducer' . || true

echo '--- 관련 호출 ---'
rg -n --type=java -C4 'NotificationEventDto\.create|idempotencyKey|NotificationProducer|notifyDecision' . || true

echo '--- DTO 파일 목록 ---'
fd -i -t f 'NotificationEventDto.java' . --exec sh -c 'echo "$1"; cat -n "$1"' sh {} \;

Repository: half-fifty/eventoday

Length of output: 40704


🏁 Script executed:

#!/bin/bash
set -eu

echo '--- 대상 리스너 ---'
cat -n BE/src/main/java/com/min/edu/application/service/BoothApplicationNotificationListener.java

echo '--- 알림 발행기 ---'
cat -n BE/src/main/java/com/min/edu/notification/producer/NotificationProducer.java

echo '--- 알림 관련 파일 ---'
fd -i -t f . BE/src/main/java | rg '/notification/|Notification|notification' | sort

echo '--- 알림 저장소 및 엔티티 사용부 ---'
rg -n --type=java -C5 'insertIfAbsent|NotificationEventDto|eventId\(\)|eventId|notificationRepository' BE/src/main/java/com/min/edu/notification BE/src/main/java/com/min/edu/booth BE/src/main/java/com/min/edu/application || true

echo '--- 빈자리 알림 경로 ---'
cat -n BE/src/main/java/com/min/edu/booth/event/BoothVacancyEventListener.java
cat -n BE/src/main/java/com/min/edu/booth/service/BoothVacancyNotificationService.java

Repository: half-fifty/eventoday

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -eu

echo '--- Kafka 소비 및 중복 저장 경로 ---'
cat -n BE/src/main/java/com/min/edu/notification/consumer/NotificationConsumer.java
cat -n BE/src/main/java/com/min/edu/notification/service/NotificationService.java
cat -n BE/src/main/java/com/min/edu/notification/repository/NotificationRepository.java

echo '--- 승인/반려 결정 이벤트 정의와 발행 ---'
rg -n --type=java -C6 'record BoothApplicationDecision|class BoothApplicationDecision|new BoothApplicationDecision|publishEvent\(.*BoothApplication|BoothApplicationDecision' BE/src/main/java BE/src/test/java || true

echo '--- 이벤트 리스너 설정 및 재처리 관련 설정 ---'
rg -n --type=java -C4 '`@Async`|`@Retryable`|DefaultErrorHandler|CommonErrorHandler|AckMode|enable.auto.commit|`@TransactionalEventListener`|BoothApplicationDecision' BE/src/main/java BE/src/test/java || true

echo '--- 알림 DTO/서비스 테스트 ---'
rg -n --type=java -C5 'createIfAbsent|NotificationConsumer|eventId|duplicate|중복' BE/src/test/java/com/min/edu/notification || true

Repository: half-fifty/eventoday

Length of output: 43084


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

dto = Path("BE/src/main/java/com/min/edu/notification/dto/NotificationEventDto.java").read_text()
listener = Path("BE/src/main/java/com/min/edu/application/service/BoothApplicationNotificationListener.java").read_text()
consumer = Path("BE/src/main/java/com/min/edu/notification/consumer/NotificationConsumer.java").read_text()
repository = Path("BE/src/main/java/com/min/edu/notification/repository/NotificationRepository.java").read_text()
decision = Path("BE/src/main/java/com/min/edu/application/service/BoothApplicationDecision.java").read_text()

assert "UUID.randomUUID()" in dto
assert "NotificationEventDto.create(" in listener
assert "eventDto.eventId()" in consumer
assert "ON CONFLICT (event_id) DO NOTHING" in repository
assert re.search(r"record BoothApplicationDecision\(\s*Long applicationId,", decision, re.S)

# The factory has no input that can identify a business decision.
factory = re.search(
    r"public static NotificationEventDto create\((.*?)\)\s*\{(.*?)\n\s*\}",
    dto,
    re.S,
)
assert factory, "NotificationEventDto.create factory not found"
params, body = factory.groups()
assert "UUID" not in params
assert "UUID.randomUUID()" in body

print("NotificationEventDto.create: random eventId")
print("NotificationConsumer: persists eventDto.eventId()")
print("NotificationRepository: deduplicates only identical event_id values")
print("BoothApplicationDecision: applicationId is the stable decision identifier")
print("Conclusion: reprocessing the same decision through create() produces a new deduplication key")
PY

Repository: half-fifty/eventoday

Length of output: 487


결정적 eventId를 사용해 중복 알림을 방지해 주세요.

NotificationEventDto.create(...)는 UUID.randomUUID()를 생성합니다. 동일한 BoothApplicationDecision이 다시 처리되면 event_id가 달라져 ON CONFLICT가 중복을 막지 못합니다. applicationId와 승인 여부를 기반으로 동일한 결정에 항상 같은 eventId를 생성해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@BE/src/main/java/com/min/edu/application/service/BoothApplicationNotificationListener.java`
around lines 41 - 51, Update the notification creation flow around
NotificationEventDto.create in BoothApplicationNotificationListener to supply a
deterministic eventId derived from decision.applicationId() and the approval
outcome. Ensure identical BoothApplicationDecision values always produce the
same eventId, while different application IDs or approval decisions produce
distinct IDs, so duplicate processing is deduplicated by ON CONFLICT.
BE/src/main/java/com/min/edu/event/service/EventContentService.java (1)

111-149: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

전체 목록 API를 Page 계약으로 변경하세요.

현재 메서드는 List를 반환하고 page 및 size를 처리하지 않습니다. 반면 FE/src/pages/Notices.jsx는 data.content와 data.last를 사용합니다. 따라서 이 엔드포인트는 배열을 반환할 때 공지 목록을 비우고, 더 보기 요청을 계속 시도합니다.

Pageable을 컨트롤러와 서비스에 전달하세요. findAll(spec, pageable) 결과를 Page<BoardItem>으로 변환하세요. 이렇게 하면 프런트엔드 계약을 맞추고 전체 콘텐츠를 매 요청마다 조회하는 문제도 제거할 수 있습니다.

수정 방향
- public List<EventContentDtos.BoardItem> listAllContents(
-         EventContentType contentType, AuthenticatedMemberDto member) {
+ public Page<EventContentDtos.BoardItem> listAllContents(
+         EventContentType contentType,
+         AuthenticatedMemberDto member,
+         Pageable pageable) {
...
-     List<EventContent> contents = eventContentRepository.findAll(spec, sort);
+     Page<EventContent> contents = eventContentRepository.findAll(
+             spec,
+             PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), sort)
+     );
...
-     return contents.stream().map(...).toList();
+     return contents.map(...);
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BE/src/main/java/com/min/edu/event/service/EventContentService.java` around
lines 111 - 149, 전체 목록 조회를 List 반환에서 Page 계약으로 변경하세요. EventContentService의
listAllContents에 Pageable을 전달하고 findAll(spec, pageable)을 사용해 Page<EventContent>를
Page<EventContentDtos.BoardItem>으로 변환하세요. 해당 메서드를 호출하는 컨트롤러도 page와 size를 받는
Pageable을 서비스에 전달하며, data.content와 data.last를 제공하는 페이지 응답을 유지하세요.
FE/src/pages/Notices.jsx (1)

66-87: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

loadMore의 실패 경로에는 버전 가드가 빠져 있습니다.

성공 경로(Line 78)는 requestVersion 비교로 오래된 응답을 버립니다. 그런데 catch 블록은 비교 없이 setError를 호출합니다. 사용자가 유형 필터를 바꾼 직후 이전 페이지 요청이 실패하면, 새 필터의 목록 위에 이전 요청의 에러 메시지가 남습니다. 새 useEffect가 setError("")를 먼저 실행한 뒤이므로 화면에서 지워지지 않습니다. 성공 경로와 동일하게 가드를 추가하면 됩니다.

🐛 제안 수정
     } catch (requestError) {
+      if (requestVersion !== requestVersionRef.current) return;
       setError(requestError.message || "공지사항을 더 불러오지 못했습니다.");
     } finally {
-      setLoadingMore(false);
+      if (requestVersion === requestVersionRef.current) setLoadingMore(false);
     }
📝 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.

  const loadMore = async () => {
    if (loadingMore || !hasMore) return;
    const nextPage = page + 1;
    // 요청 시점의 필터 버전 캡처 — 응답 도착 전 필터가 바뀌었으면 결과를 버린다
    const requestVersion = requestVersionRef.current;
    setLoadingMore(true);
    try {
      const data = await listAllContents({
        contentType: typeFilter || undefined,
        page: nextPage,
        size: PAGE_SIZE,
      });
      if (requestVersion !== requestVersionRef.current) return;
      setItems((prev) => [...prev, ...flatten(data?.content)]);
      setHasMore(data ? !data.last : false);
      setPage(nextPage);
    } catch (requestError) {
      if (requestVersion !== requestVersionRef.current) return;
      setError(requestError.message || "공지사항을 더 불러오지 못했습니다.");
    } finally {
      if (requestVersion === requestVersionRef.current) setLoadingMore(false);
    }
  };
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 80-80: Avoid using the initial state variable in setState
Context: setPage(nextPage)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FE/src/pages/Notices.jsx` around lines 66 - 87, Update the catch path in
loadMore to compare requestVersion with requestVersionRef.current before calling
setError, matching the successful response guard. Ignore errors from stale
requests while preserving the existing error message behavior for the current
filter request.
FE/src/pages/OrganizerAdmin.jsx (1)

696-703: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

통계 수치가 null이면 화면이 깨집니다.

overview.totalReservationCount.toLocaleString()은 값이 null일 때 TypeError를 던집니다. 같은 PR의 BoothApplicationList.jsx 309번째 줄 주석에도 "집계가 없는 날짜에 수치가 null일 수 있어 기본값 0 처리"라고 적혀 있습니다. 즉 null은 실제로 올 수 있는 값입니다. 인기 부스 행(720-721)도 같은 상황입니다. ?? 0으로 방어해 주세요.

🛡️ 제안 diff
-                  <span className="font-display-md text-[26px]">{overview ? overview.totalReservationCount.toLocaleString() : "-"}</span>
+                  <span className="font-display-md text-[26px]">{overview ? (overview.totalReservationCount ?? 0).toLocaleString() : "-"}</span>
@@
-                  <span className="font-display-md text-[26px]">{overview ? overview.totalQrScanCount.toLocaleString() : "-"}</span>
+                  <span className="font-display-md text-[26px]">{overview ? (overview.totalQrScanCount ?? 0).toLocaleString() : "-"}</span>
@@
-                        <span className="text-caption text-ink-muted">예약 {booth.totalReservationCount.toLocaleString()}건</span>
-                        <span className="text-caption text-ink-muted">방문 {booth.totalQrScanCount.toLocaleString()}건</span>
+                        <span className="text-caption text-ink-muted">예약 {(booth.totalReservationCount ?? 0).toLocaleString()}건</span>
+                        <span className="text-caption text-ink-muted">방문 {(booth.totalQrScanCount ?? 0).toLocaleString()}건</span>

Also applies to: 715-723

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FE/src/pages/OrganizerAdmin.jsx` around lines 696 - 703, Update the overview
statistic displays in OrganizerAdmin, including totalReservationCount,
totalQrScanCount, and the popular-booth counts, to apply a nullish fallback of 0
before calling toLocaleString. Preserve the existing "-" fallback when overview
itself is unavailable.

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.

1 participant