Feature/booth finally - 백엔드 마지막 - #49
Conversation
…roller.java Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (46)
💤 Files with no reviewable changes (1)
🚧 Files skipped from review as they are similar to previous changes (6)
Walkthrough부스 예약·체크인·후기·안내 기능을 추가했다. 참가 신청과 관리자 심사 흐름을 API 기반으로 구현했다. 행사 공지·자료와 첨부파일 관리 기능을 백엔드와 프론트엔드에 연결했다. Changes부스 플랫폼
부스 신청 플랫폼
행사 콘텐츠 플랫폼
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-->>참가기업: 사이트 알림과 이메일 발송
Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
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오버로드는 하나로 정리하는 편이 안전합니다.세 가지가 걸립니다.
Short/Integer오버로드가 공존하면updateRating(null)같은 리터럴 호출이 컴파일 단계에서 모호해집니다. 지금 호출부는 타입이 명확해서 문제가 없지만, 나중에 호출부를 추가할 때 걸리기 쉽습니다.Integer버전은 null을 조용히 무시합니다. 도메인 메서드가 "값을 바꿨다"고 알려주지 않으므로 호출자는 갱신 실패를 알 수 없습니다.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 winLua 스크립트를 상수로 올려 주세요.
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
📒 Files selected for processing (37)
BE/src/main/java/com/min/edu/booth/controller/BoothCheckInController.javaBE/src/main/java/com/min/edu/booth/controller/BoothRecommendationController.javaBE/src/main/java/com/min/edu/booth/controller/BoothReservationController.javaBE/src/main/java/com/min/edu/booth/controller/BoothReviewController.javaBE/src/main/java/com/min/edu/booth/controller/MobileGuideController.javaBE/src/main/java/com/min/edu/booth/controller/VenueMapCongestionController.javaBE/src/main/java/com/min/edu/booth/domain/BoothReview.javaBE/src/main/java/com/min/edu/booth/dto/BoothAverageRatingResponse.javaBE/src/main/java/com/min/edu/booth/dto/BoothCheckInRequest.javaBE/src/main/java/com/min/edu/booth/dto/BoothReviewResponse.javaBE/src/main/java/com/min/edu/booth/dto/CreateBoothReviewRequest.javaBE/src/main/java/com/min/edu/booth/dto/MobileGuideBoothDetailResponse.javaBE/src/main/java/com/min/edu/booth/dto/MobileGuideBoothListResponse.javaBE/src/main/java/com/min/edu/booth/dto/MobileGuideMainResponse.javaBE/src/main/java/com/min/edu/booth/dto/RecommendedBoothsResponse.javaBE/src/main/java/com/min/edu/booth/dto/UpdateBoothReviewRequest.javaBE/src/main/java/com/min/edu/booth/dto/VenueMapWithCongestionResponseDto.javaBE/src/main/java/com/min/edu/booth/event/BoothVacancyEvent.javaBE/src/main/java/com/min/edu/booth/event/BoothVacancyEventListener.javaBE/src/main/java/com/min/edu/booth/repository/BoothQrScanRepository.javaBE/src/main/java/com/min/edu/booth/repository/BoothRepository.javaBE/src/main/java/com/min/edu/booth/repository/BoothReservationRepository.javaBE/src/main/java/com/min/edu/booth/repository/BoothReservationSlotRepository.javaBE/src/main/java/com/min/edu/booth/repository/BoothReviewRepository.javaBE/src/main/java/com/min/edu/booth/service/BoothCheckInService.javaBE/src/main/java/com/min/edu/booth/service/BoothRecommendationService.javaBE/src/main/java/com/min/edu/booth/service/BoothReservationService.javaBE/src/main/java/com/min/edu/booth/service/BoothReservationWithRedisService.javaBE/src/main/java/com/min/edu/booth/service/BoothReviewService.javaBE/src/main/java/com/min/edu/booth/service/BoothVacancyNotificationService.javaBE/src/main/java/com/min/edu/booth/service/MobileGuideService.javaBE/src/main/java/com/min/edu/booth/service/RedisReservationService.javaBE/src/main/java/com/min/edu/booth/service/VenueMapCongestionService.javaBE/src/main/java/com/min/edu/interest/dto/InterestBoothResponse.javaBE/src/main/java/com/min/edu/interest/repository/BoothInterestRepository.javaBE/src/main/java/com/min/edu/interest/service/BoothInterestService.javaBE/src/main/resources/db/migration/V14__Add_updated_at_to_booth_reviews.sql
| 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() | ||
| ); |
There was a problem hiding this comment.
🩺 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' -C2Repository: 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/javaRepository: 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().
There was a problem hiding this comment.
@CodeRabbit
/booths/{boothId}/reservations SecurityConfig 업데이트 관련:
이 지적은 SKIP하는 이유:
-
SecurityConfig는 팀 공용 설정
- 이 파일은 제가 작성하지 않았으며 팀리드가 관리함
- 팀 전체 보안 정책 수정은 팀 협의 필요
- 팀리드와 함께 조정해야 함
-
현재 Controller는 이미 인증을 강제함
- @AuthenticationPrincipal AuthenticatedMemberDto principal
- 인증되지 않은 요청은 이 메서드에 도달하지 않음
- 엔드포인트 레벨에서 보안 보장
-
아키텍처 설계상 선택
- 팀이 의도적으로 전역 설정 대신 엔드포인트 레벨 인증 사용
- 엔드포인트별 다양한 인증 전략 가능
결론: 현재 구현이 안전합니다. SecurityConfig 변경은
팀리드가 별도로 처리하여 전체 앱의 보안 아키텍처 일관성을 유지해야 합니다.
There was a problem hiding this comment.
@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.
# 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.
…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.
There was a problem hiding this comment.
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
📒 Files selected for processing (46)
BE/build.gradleBE/src/main/java/com/min/edu/application/service/BoothApplicationDecision.javaBE/src/main/java/com/min/edu/application/service/BoothApplicationNotificationListener.javaBE/src/main/java/com/min/edu/application/service/BoothApplicationService.javaBE/src/main/java/com/min/edu/booth/config/AsyncConfiguration.javaBE/src/main/java/com/min/edu/booth/controller/BoothReservationController.javaBE/src/main/java/com/min/edu/booth/controller/BoothReviewController.javaBE/src/main/java/com/min/edu/booth/domain/BoothReservation.javaBE/src/main/java/com/min/edu/booth/domain/BoothReview.javaBE/src/main/java/com/min/edu/booth/dto/BoothReviewResponse.javaBE/src/main/java/com/min/edu/booth/dto/UpdateBoothReviewRequest.javaBE/src/main/java/com/min/edu/booth/event/BoothVacancyEvent.javaBE/src/main/java/com/min/edu/booth/event/BoothVacancyEventListener.javaBE/src/main/java/com/min/edu/booth/repository/BoothQrScanRepository.javaBE/src/main/java/com/min/edu/booth/repository/BoothReservationRepository.javaBE/src/main/java/com/min/edu/booth/repository/BoothReservationSlotRepository.javaBE/src/main/java/com/min/edu/booth/service/BoothCheckInService.javaBE/src/main/java/com/min/edu/booth/service/BoothRecommendationService.javaBE/src/main/java/com/min/edu/booth/service/BoothReservationWithRedisService.javaBE/src/main/java/com/min/edu/booth/service/BoothReviewService.javaBE/src/main/java/com/min/edu/booth/service/BoothVacancyNotificationService.javaBE/src/main/java/com/min/edu/event/controller/EventContentController.javaBE/src/main/java/com/min/edu/event/dto/EventContentDtos.javaBE/src/main/java/com/min/edu/event/repository/EventRepository.javaBE/src/main/java/com/min/edu/event/service/EventContentService.javaBE/src/main/java/com/min/edu/notification/domain/NotificationType.javaBE/src/main/java/com/min/edu/notification/repository/NotificationRepository.javaBE/src/main/resources/application.propertiesFE/src/api/boothApplicationApi.jsFE/src/api/contentApi.jsFE/src/api/fileApi.jsFE/src/api/statisticsApi.jsFE/src/components/FileDownloadLink.jsxFE/src/components/FileUploadField.jsxFE/src/components/TopNav.jsxFE/src/main.jsxFE/src/notifications/notificationPresentation.jsFE/src/pages/BoothApplicationList.jsxFE/src/pages/BoothApply.jsxFE/src/pages/EventDetail.jsxFE/src/pages/MyPage.jsxFE/src/pages/Notices.jsxFE/src/pages/OrganizerAdmin.jsxFE/src/pages/RecruitmentDetail.jsxk8s/backend.yamlk8s/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
| @Column(name = "created_at") | ||
| private OffsetDateTime createdAt; |
There was a problem hiding this comment.
🗄️ 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/migrationRepository: 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 120Repository: 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.javaRepository: 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 160Repository: 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
| @Column(name = "updated_at", nullable = false) | ||
| private OffsetDateTime updatedAt; | ||
|
|
||
| @Column(name = "member_name") | ||
| private String memberName; |
There was a problem hiding this comment.
🗄️ 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/boothRepository: 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/resourcesRepository: 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())
PYRepository: 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
| // 결정적 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 타입 | ||
| ); |
There was a problem hiding this comment.
🗄️ 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", |
There was a problem hiding this comment.
🎯 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-L64BE/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 오름차순을 사용하세요.
| CAST(:eventId || '-0000-0000-0000-000000000000' AS uuid), | ||
| :memberId, :notificationType, :referenceType, | ||
| :referenceId, :title, :content, :createdAt | ||
| ) | ||
| ON CONFLICT (event_id) DO NOTHING |
There was a problem hiding this comment.
🗄️ 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
There was a problem hiding this comment.
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
📒 Files selected for processing (46)
BE/build.gradleBE/src/main/java/com/min/edu/application/service/BoothApplicationDecision.javaBE/src/main/java/com/min/edu/application/service/BoothApplicationNotificationListener.javaBE/src/main/java/com/min/edu/application/service/BoothApplicationService.javaBE/src/main/java/com/min/edu/booth/config/AsyncConfiguration.javaBE/src/main/java/com/min/edu/booth/controller/BoothReservationController.javaBE/src/main/java/com/min/edu/booth/controller/BoothReviewController.javaBE/src/main/java/com/min/edu/booth/domain/BoothReservation.javaBE/src/main/java/com/min/edu/booth/domain/BoothReview.javaBE/src/main/java/com/min/edu/booth/dto/BoothReviewResponse.javaBE/src/main/java/com/min/edu/booth/dto/UpdateBoothReviewRequest.javaBE/src/main/java/com/min/edu/booth/event/BoothVacancyEvent.javaBE/src/main/java/com/min/edu/booth/event/BoothVacancyEventListener.javaBE/src/main/java/com/min/edu/booth/repository/BoothQrScanRepository.javaBE/src/main/java/com/min/edu/booth/repository/BoothReservationRepository.javaBE/src/main/java/com/min/edu/booth/repository/BoothReservationSlotRepository.javaBE/src/main/java/com/min/edu/booth/service/BoothCheckInService.javaBE/src/main/java/com/min/edu/booth/service/BoothRecommendationService.javaBE/src/main/java/com/min/edu/booth/service/BoothReservationWithRedisService.javaBE/src/main/java/com/min/edu/booth/service/BoothReviewService.javaBE/src/main/java/com/min/edu/booth/service/BoothVacancyNotificationService.javaBE/src/main/java/com/min/edu/event/controller/EventContentController.javaBE/src/main/java/com/min/edu/event/dto/EventContentDtos.javaBE/src/main/java/com/min/edu/event/repository/EventRepository.javaBE/src/main/java/com/min/edu/event/service/EventContentService.javaBE/src/main/java/com/min/edu/notification/domain/NotificationType.javaBE/src/main/java/com/min/edu/notification/repository/NotificationRepository.javaBE/src/main/resources/application.propertiesFE/src/api/boothApplicationApi.jsFE/src/api/contentApi.jsFE/src/api/fileApi.jsFE/src/api/statisticsApi.jsFE/src/components/FileDownloadLink.jsxFE/src/components/FileUploadField.jsxFE/src/components/TopNav.jsxFE/src/main.jsxFE/src/notifications/notificationPresentation.jsFE/src/pages/BoothApplicationList.jsxFE/src/pages/BoothApply.jsxFE/src/pages/EventDetail.jsxFE/src/pages/MyPage.jsxFE/src/pages/Notices.jsxFE/src/pages/OrganizerAdmin.jsxFE/src/pages/RecruitmentDetail.jsxk8s/backend.yamlk8s/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.javaRepository: 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 || trueRepository: 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") PYRepository: 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.jsx309번째 줄 주석에도 "집계가 없는 날짜에 수치가 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.
📄 작업 내용
🔗 관련 이슈
Closes #
☑️ 체크리스트
📸 스크린샷 (UI 변경 시)
💬 리뷰어에게
Summary by CodeRabbit