Skip to content

Conversation

@CSE-Shaco
Copy link
Contributor

@CSE-Shaco CSE-Shaco commented Dec 19, 2025

📌 연관된 이슈

ex) #이슈번호, #이슈번호

✨ 작업 내용

이번 PR에서 작업한 내용을 간략히 설명해주세요

💬 리뷰 요구사항(선택)

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능
    • 방명록 항목 생성, 조회, 목록 보기, 삭제 기능 추가
    • 경품 추첨 시스템 추가
    • 당첨자 관리 및 초기화 기능 추가

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 19, 2025

Walkthrough

새로운 방명록 관리 기능을 추가합니다. 방명록 항목 CRUD 작업, 럭키드로우 추첨 기능, 우승자 관리 기능을 포함한 REST API 엔드포인트, 서비스 계층, JPA 엔티티 및 관련 DTO들을 도입합니다.

Changes

Cohort / File(s) 변경 요약
Controller 계층
src/main/java/inha/gdgoc/domain/guestbook/controller/GuestbookController.java
방명록 및 럭키드로우 작업을 위한 REST 컨트롤러 신규 추가. 항목 생성/조회/삭제, 우승자 추첨/조회/초기화 엔드포인트 제공. ORGANIZER, ADMIN 역할로 보안 설정.
메시지 상수
src/main/java/inha/gdgoc/domain/guestbook/controller/message/GuestbookMessage.java
방명록 작업(생성, 조회, 삭제, 추첨) 관련 성공 메시지 7개 상수 추가.
요청 DTO
src/main/java/inha/gdgoc/domain/guestbook/dto/request/GuestbookCreateRequest.java, LuckyDrawRequest.java
방명록 항목 생성 요청 및 럭키드로우 요청 DTO 신규 추가. 유효성 검증 애너테이션 포함.
응답 DTO
src/main/java/inha/gdgoc/domain/guestbook/dto/response/GuestbookEntryResponse.java, LuckyDrawWinnerResponse.java
방명록 항목 및 우승자 정보 응답 DTO 신규 추가. 엔티티 변환 팩토리 메서드 포함.
JPA 엔티티
src/main/java/inha/gdgoc/domain/guestbook/entity/GuestbookEntry.java
방명록 항목 엔티티 신규 추가. wristbandSerial(고유), name, createdAt(불변), wonAt 필드 포함. 우승 여부 확인 및 표시 메서드 제공.
Repository 계층
src/main/java/inha/gdgoc/domain/guestbook/repository/GuestbookRepository.java
Spring Data JPA 리포지토리 인터페이스 신규 추가. 항목 조회, 우승 항목 잠금 조회, 우승자 초기화 벌크 작업 메서드 제공. PESSIMISTIC_WRITE 잠금 전략 사용.
Service 계층
src/main/java/inha/gdgoc/domain/guestbook/service/GuestbookService.java
방명록 항목 관리 및 럭키드로우 로직 서비스 클래스 신규 추가. 항목 등록/조회/삭제, 우승자 추첨/조회/초기화 기능 구현. 비즈니스 예외 3개 정의.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller as GuestbookController
    participant Service as GuestbookService
    participant Repository as GuestbookRepository
    participant DB as Database

    rect rgb(200, 220, 255)
    Note over Client,DB: 방명록 항목 생성 흐름
    Client->>Controller: POST /entries (wristbandSerial, name)
    Controller->>Service: register(wristbandSerial, name)
    Service->>Repository: existsByWristbandSerial()
    Repository->>DB: SELECT COUNT(*)
    DB-->>Repository: 0
    Repository-->>Service: false
    Service->>Repository: save(new GuestbookEntry)
    Repository->>DB: INSERT
    DB-->>Repository: saved entry
    Repository-->>Service: GuestbookEntry
    Service-->>Controller: GuestbookEntryResponse
    Controller-->>Client: 201 Created (ApiResponse)
    end

    rect rgb(200, 255, 220)
    Note over Client,DB: 럭키드로우 추첨 흐름
    Client->>Controller: POST /lucky-draw (count)
    Controller->>Service: draw(LuckyDrawRequest)
    Service->>Repository: findAllByWonAtIsNullForUpdate()
    Repository->>DB: SELECT * WHERE wonAt IS NULL (PESSIMISTIC_WRITE)
    DB-->>Repository: candidates
    Repository-->>Service: List<GuestbookEntry>
    alt Count validation
        Service->>Service: validate(candidateCount >= requestCount)
    end
    Service->>Service: shuffle & select winners
    loop For each winner
        Service->>Service: markWon()
        Service->>Repository: save()
        Repository->>DB: UPDATE wonAt
    end
    Service-->>Controller: List<LuckyDrawWinnerResponse>
    Controller-->>Client: 200 OK (ApiResponse)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50분

  • 특별 주의 사항:
    • Lucky draw 로직에서 PESSIMISTIC_WRITE 잠금 메커니즘과 동시성 처리 검증 필요
    • findAllByWonAtIsNullForUpdate() 쿼리의 wristbandSerial 고유성 제약 및 인덱싱 전략 확인
    • 럭키드로우 추첨 시 충분한 후보자 검증 및 예외 처리 로직
    • @Transactional 경계와 flush/clear 자동화 설정의 일관성 확인
    • GuestbookCreateRequest에서 wristbandSerial 중복 시 처리 흐름 및 오류 메시지 명확성
    • Repository의 벌크 업데이트(clearAllWinners()) 작동 검증

Poem

🐰 새로운 방명록이 피어났네,
우승자를 골라내는 행운의 주사위,
요청과 응답이 춤을 추고,
데이터베이스에 기록되는 추억들.
럭키드로우는 설레는 마음, ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 74.07% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 방명록(guestbook)과 연계된 추첨(lucky draw) 기능 추가를 명확하게 설명하고 있으며, 변경 사항의 주요 내용과 일치합니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (10)
src/main/java/inha/gdgoc/domain/guestbook/controller/message/GuestbookMessage.java (1)

3-13: 유틸리티 클래스 인스턴스화 방지 권장

상수만 포함하는 유틸리티 클래스는 인스턴스화를 방지하기 위해 final로 선언하고 private 생성자를 추가하는 것이 좋습니다.

🔎 제안하는 수정사항
-public class GuestbookMessage {
+public final class GuestbookMessage {
+
+    private GuestbookMessage() {
+        throw new AssertionError("Utility class should not be instantiated");
+    }
 
     public static final String ENTRY_CREATED_SUCCESS = "방명록 작성이 완료되었습니다.";
src/main/java/inha/gdgoc/domain/guestbook/entity/GuestbookEntry.java (2)

26-26: 명시적 컬럼명 지정 권장

wonAt 필드에 명시적으로 @Column(name = "won_at")을 지정하여 데이터베이스 스키마를 명확히 하는 것이 좋습니다.

🔎 제안하는 수정사항
+    @Column(name = "won_at")
     private LocalDateTime wonAt; // 당첨 시각 (null이면 미당첨)

30-33: 생성자 입력값 검증 고려

생성자에서 wristbandSerialname에 대한 null 체크나 공백 검증을 수행하는 것을 고려해보세요. 서비스 레이어에서 검증하더라도 도메인 객체 수준에서 방어적으로 처리하는 것이 안전합니다.

🔎 제안하는 수정사항
     public GuestbookEntry(String wristbandSerial, String name) {
+        if (wristbandSerial == null || wristbandSerial.isBlank()) {
+            throw new IllegalArgumentException("wristbandSerial must not be blank");
+        }
+        if (name == null || name.isBlank()) {
+            throw new IllegalArgumentException("name must not be blank");
+        }
         this.wristbandSerial = wristbandSerial;
         this.name = name;
     }
src/main/java/inha/gdgoc/domain/guestbook/service/GuestbookService.java (3)

24-37: 수동 입력 검증의 중복성 검토

서비스 레이어에서 wristbandSerialname을 수동으로 검증하고 있습니다. 이러한 검증은 GuestbookCreateRequest DTO의 @NotBlank 애노테이션과 중복됩니다.

컨트롤러 레이어에서 @Valid를 사용하여 Bean Validation을 활성화하면, 서비스 메서드는 이미 검증된 데이터를 받게 되므로 중복 검증을 제거할 수 있습니다. 또한, 서비스 메서드가 개별 String 파라미터 대신 DTO 객체를 직접 받도록 수정하는 것도 고려해보세요.


57-74: 추첨 공정성을 위한 SecureRandom 사용 고려

Collections.shuffle(pool)은 내부적으로 일반 Random 인스턴스를 사용하여 예측 가능한 난수를 생성합니다. 추첨의 공정성과 예측 불가능성이 중요한 경우, SecureRandom을 사용하는 것을 고려하세요.

또한, 후보 풀이 큰 경우 전체 리스트를 셔플하는 것은 비효율적일 수 있습니다. Fisher-Yates 샘플링 알고리즘을 사용하여 필요한 개수만큼만 선택하는 방식도 고려해보세요.

🔎 SecureRandom을 사용한 수정 예시
+import java.security.SecureRandom;
+import java.util.Random;
+
 @Service
 @Transactional
 @RequiredArgsConstructor
 public class GuestbookService {
 
     private final GuestbookRepository repo;
+    private static final Random SECURE_RANDOM = new SecureRandom();
 
     /* ===== 추첨 ===== */
     public List<LuckyDrawWinnerResponse> draw(LuckyDrawRequest req) {
         int count = req.count();
 
         List<GuestbookEntry> pool = repo.findAllByWonAtIsNullForUpdate();
         if (pool.size() < count) {
             throw new NoCandidatesException();
         }
 
-        Collections.shuffle(pool);
+        Collections.shuffle(pool, SECURE_RANDOM);
 
         List<LuckyDrawWinnerResponse> winners = new ArrayList<>();
         for (int i = 0; i < count; i++) {
             GuestbookEntry e = pool.get(i);
             e.markWon();
             winners.add(LuckyDrawWinnerResponse.from(e));
         }
         return winners;
     }

88-98: 예외 클래스에 메시지 추가 권장

커스텀 예외 클래스들이 비어있어 디버깅이 어렵습니다. 의미 있는 기본 메시지를 포함하거나, 동적 메시지를 받을 수 있는 생성자를 추가하는 것을 권장합니다.

🔎 제안하는 수정사항
     /* ===== exceptions ===== */
     public static class DuplicateWristbandSerialException extends RuntimeException {
-
+        public DuplicateWristbandSerialException() {
+            super("이미 등록된 손목밴드 일련번호입니다.");
+        }
     }
 
     public static class NoCandidatesException extends RuntimeException {
-
+        public NoCandidatesException() {
+            super("추첨 가능한 후보가 부족합니다.");
+        }
+        public NoCandidatesException(int required, int available) {
+            super(String.format("추첨 가능한 후보가 부족합니다. (요청: %d, 가능: %d)", required, available));
+        }
     }
 
     public static class NotFoundException extends RuntimeException {
-
+        public NotFoundException() {
+            super("방명록 항목을 찾을 수 없습니다.");
+        }
     }
src/main/java/inha/gdgoc/domain/guestbook/controller/GuestbookController.java (4)

41-45: 페이지네이션 지원 고려

현재 모든 방명록 항목을 한 번에 반환합니다. 방명록 항목이 많아질 경우 성능 문제와 메모리 부족이 발생할 수 있습니다.

Spring Data의 Pageable을 사용한 페이지네이션 지원을 고려하세요.

🔎 페이지네이션 적용 예시
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
 /* ===== 방명록: 목록 ===== */
 @GetMapping("/entries")
-public ResponseEntity<ApiResponse<List<GuestbookEntryResponse>, Void>> listEntries() {
-    var result = service.listEntries();
+public ResponseEntity<ApiResponse<Page<GuestbookEntryResponse>, Void>> listEntries(Pageable pageable) {
+    var result = service.listEntries(pageable);
     return ResponseEntity.ok(ApiResponse.ok(GuestbookMessage.ENTRY_LIST_RETRIEVED_SUCCESS, result));
 }

이 경우 서비스 레이어와 리포지토리도 함께 수정이 필요합니다.


19-23: API 문서화 추가 고려

컨트롤러에 OpenAPI/Swagger 어노테이션이 없어 자동 API 문서가 생성되지 않습니다. @Operation, @ApiResponse, @Parameter 등의 어노테이션 추가를 고려하세요.

🔎 API 문서화 예시
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
 @RestController
 @RequestMapping("/api/v1/guestbook")
 @RequiredArgsConstructor
 @PreAuthorize("hasAnyRole('ORGANIZER','ADMIN')")
+@Tag(name = "Guestbook", description = "방명록 및 럭키드로우 관리 API")
 public class GuestbookController {

     /* ===== 방명록: 등록(자동 응모) ===== */
+    @Operation(summary = "방명록 등록", description = "손목밴드 번호와 이름으로 방명록을 등록하고 자동으로 추첨 대상에 포함")
+    @ApiResponse(responseCode = "200", description = "등록 성공")
     @PostMapping("/entries")
     public ResponseEntity<ApiResponse<GuestbookEntryResponse, Void>> createEntry(@Valid @RequestBody GuestbookCreateRequest req) {

34-38: HTTP 상태 코드 및 중복 검증 확인

엔드포인트 구현은 정확하지만 다음 사항을 고려하세요:

  1. HTTP 상태 코드: 리소스 생성 시 200 OK 대신 201 CREATED를 반환하는 것이 REST 모범 사례입니다
  2. 중복 처리: 동일한 wristbandSerial로 중복 등록이 가능한지 확인 필요합니다. 서비스 레이어에서 중복 검증을 수행하는지 확인하세요
🔎 201 CREATED 상태 코드 사용 제안
 @PostMapping("/entries")
 public ResponseEntity<ApiResponse<GuestbookEntryResponse, Void>> createEntry(@Valid @RequestBody GuestbookCreateRequest req) {
     GuestbookEntryResponse saved = service.register(req.wristbandSerial(), req.name());
-    return ResponseEntity.ok(ApiResponse.ok(GuestbookMessage.ENTRY_CREATED_SUCCESS, saved));
+    return ResponseEntity.status(201).body(ApiResponse.ok(GuestbookMessage.ENTRY_CREATED_SUCCESS, saved));
 }

중복 처리 로직 확인:

#!/bin/bash
# Description: 서비스 레이어에서 wristbandSerial 중복 검증 확인

# GuestbookService에서 중복 검증 로직 검색
ast-grep --pattern 'class GuestbookService {
  $$$
  register($$$) {
    $$$
  }
  $$$
}'

# Repository에서 existsByWristbandSerial 같은 메서드 검색
rg -n --type=java -C3 'existsBy.*[Ww]ristband' src/main/java/inha/gdgoc/domain/guestbook/

19-23: 클래스 레벨 보안 설정 재검토 권장

@PreAuthorize("hasAnyRole('ORGANIZER','ADMIN')")이 클래스 레벨에 적용되어 모든 엔드포인트가 ORGANIZER 또는 ADMIN 역할을 요구합니다. 이는 일반 사용자가 방명록을 직접 작성할 수 없고, 조회 작업도 제한됨을 의미합니다.

Line 33의 한글 주석 "운영진 PC에서 손목밴드 번호 + 이름 입력"에 따르면 운영진이 대신 입력하는 키오스크 방식으로 설계된 것으로 보입니다. 이 경우:

  • 현재 설계 의도가 맞다면: 전체 구조가 일관성 있고 적절합니다.
  • 향후 개선이 필요하다면: listEntries, getEntry, listWinners 등 조회 엔드포인트에 메서드 레벨 @PreAuthorize를 적용하여 인증된 사용자나 특정 역할에만 허용하도록 세분화할 수 있습니다.

메서드 레벨에서 @PreAuthorize를 적용하는 것이 Spring Security 모범 사례입니다.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a683d8 and 78fddbd.

📒 Files selected for processing (9)
  • src/main/java/inha/gdgoc/domain/guestbook/controller/GuestbookController.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/guestbook/controller/message/GuestbookMessage.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/guestbook/dto/request/GuestbookCreateRequest.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/guestbook/dto/request/LuckyDrawRequest.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/guestbook/dto/response/GuestbookEntryResponse.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/guestbook/dto/response/LuckyDrawWinnerResponse.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/guestbook/entity/GuestbookEntry.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/guestbook/repository/GuestbookRepository.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/guestbook/service/GuestbookService.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/main/java/inha/gdgoc/domain/guestbook/entity/GuestbookEntry.java (2)
src/main/java/inha/gdgoc/global/entity/BaseEntity.java (1)
  • Getter (12-24)
src/main/java/inha/gdgoc/domain/core/attendance/entity/AttendanceRecord.java (2)
  • Entity (9-54)
  • PrePersist (49-53)
src/main/java/inha/gdgoc/domain/guestbook/controller/GuestbookController.java (1)
src/main/java/inha/gdgoc/domain/guestbook/controller/message/GuestbookMessage.java (1)
  • GuestbookMessage (3-13)
🔇 Additional comments (8)
src/main/java/inha/gdgoc/domain/guestbook/dto/request/GuestbookCreateRequest.java (1)

6-7: LGTM!

Record 타입을 활용한 간결한 요청 DTO 구현이며, 적절한 유효성 검증 애노테이션이 적용되어 있습니다.

src/main/java/inha/gdgoc/domain/guestbook/dto/response/GuestbookEntryResponse.java (1)

7-12: LGTM!

엔티티를 응답 DTO로 변환하는 팩토리 메서드가 깔끔하게 구현되어 있습니다.

src/main/java/inha/gdgoc/domain/guestbook/dto/response/LuckyDrawWinnerResponse.java (1)

7-11: LGTM!

당첨자 정보를 담는 응답 DTO가 일관된 패턴으로 잘 구현되어 있습니다.

src/main/java/inha/gdgoc/domain/guestbook/dto/request/LuckyDrawRequest.java (1)

8-13: LGTM!

Jackson 역직렬화를 위한 커스텀 생성자가 적절하게 구현되어 있으며, null 값을 안전한 기본값(1)으로 처리하고 있습니다.

src/main/java/inha/gdgoc/domain/guestbook/repository/GuestbookRepository.java (1)

20-22: 동시성 제어가 적절하게 구현됨

PESSIMISTIC_WRITE 락을 사용하여 추첨 과정에서 동일한 후보가 중복 선택되는 것을 방지하는 것은 올바른 접근입니다. 동시에 여러 추첨이 실행되더라도 데이터 일관성이 보장됩니다.

src/main/java/inha/gdgoc/domain/guestbook/service/GuestbookService.java (1)

52-54: 존재하지 않는 ID 삭제 시 동작 확인

deleteById()는 해당 ID가 존재하지 않아도 예외를 발생시키지 않고 조용히 성공합니다. 이것이 의도된 동작인지 확인이 필요합니다. 만약 존재하지 않는 경우 예외를 발생시켜야 한다면 먼저 존재 여부를 확인하거나 findById().ifPresentOrElse() 패턴을 사용하세요.

삭제 전 존재 여부를 확인하려면 다음과 같이 수정할 수 있습니다:

public void delete(Long id) {
    if (!repo.existsById(id)) {
        throw new NotFoundException();
    }
    repo.deleteById(id);
}
src/main/java/inha/gdgoc/domain/guestbook/controller/GuestbookController.java (2)

28-30: 헬퍼 메서드 구현 확인

일관된 응답 형식을 제공하는 헬퍼 메서드로 적절하게 구현되었습니다. 코드 중복을 줄이고 가독성을 향상시킵니다.


62-66: 럭키드로우 추첨 로직의 동시성 처리는 이미 적절히 구현됨

코드 검토 결과, 럭키드로우 추첨 로직은 다음과 같이 이미 동시성 문제를 적절히 처리하고 있습니다:

  1. GuestbookService는 클래스 레벨의 @Transactional로 보호됨 (draw 메서드 포함)
  2. findAllByWonAtIsNullForUpdate()@Lock(LockModeType.PESSIMISTIC_WRITE)를 사용하여 비관적 락 적용
  3. markWon()으로 당첨자를 표시하고, 쿼리의 WHERE g.wonAt is null 조건으로 재선정 방지

동시 추첨 요청은 락으로 인해 직렬화되며, 당첨자 중복 선정은 없습니다.

Likely an incorrect or invalid review comment.

Comment on lines +48 to +51
@GetMapping("/entries/{entryId}")
public ResponseEntity<ApiResponse<GuestbookEntryResponse, Void>> getEntry(@PathVariable Long entryId) {
return ResponseEntity.ok(ApiResponse.ok(GuestbookMessage.ENTRY_RETRIEVED_SUCCESS, service.get(entryId)));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

경로 변수 검증 누락

entryId 경로 변수에 검증 어노테이션이 없어 음수나 0 같은 유효하지 않은 값이 전달될 수 있습니다. 이는 서비스 레이어에서 혼란스러운 오류 메시지를 유발하거나 예상치 못한 동작을 일으킬 수 있습니다.

🔎 경로 변수 검증 추가 제안
+import jakarta.validation.constraints.Positive;
+
 /* ===== 방명록: 단건 조회(필요하면) ===== */
 @GetMapping("/entries/{entryId}")
-public ResponseEntity<ApiResponse<GuestbookEntryResponse, Void>> getEntry(@PathVariable Long entryId) {
+public ResponseEntity<ApiResponse<GuestbookEntryResponse, Void>> getEntry(@PathVariable @Positive Long entryId) {
     return ResponseEntity.ok(ApiResponse.ok(GuestbookMessage.ENTRY_RETRIEVED_SUCCESS, service.get(entryId)));
 }

 /* ===== 방명록: 삭제(운영 중 실수 입력 정정용) ===== */
 @DeleteMapping("/entries/{entryId}")
-public ResponseEntity<ApiResponse<Map<String, Object>, Void>> deleteEntry(@PathVariable Long entryId) {
+public ResponseEntity<ApiResponse<Map<String, Object>, Void>> deleteEntry(@PathVariable @Positive Long entryId) {
     service.delete(entryId);
     return okUpdated(GuestbookMessage.ENTRY_DELETED_SUCCESS, 1L);
 }

Also applies to: 54-58

🤖 Prompt for AI Agents
In src/main/java/inha/gdgoc/domain/guestbook/controller/GuestbookController.java
around lines 48-51 (and similarly for lines 54-58), the @PathVariable Long
entryId lacks validation allowing 0 or negative IDs; add validation by
annotating the parameter with @Positive (or @Min(1)) and ensure the controller
class is annotated with @Validated (import javax.validation.constraints.Positive
or javax.validation.constraints.Min and
org.springframework.validation.annotation.Validated) so Spring enforces
constraint violations; update method signatures to use @PathVariable @Positive
Long entryId (or @Min(1)) and add required imports — repeat the same change for
the other endpoint at lines 54-58.

Comment on lines +75 to +79
@PostMapping("/lucky-draw/reset")
public ResponseEntity<ApiResponse<Map<String, Object>, Void>> resetWinners() {
long updated = service.resetWinners();
return okUpdated(GuestbookMessage.WINNER_RESET_SUCCESS, updated);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

위험한 삭제 작업 보호 필요

당첨자 초기화는 데이터를 삭제하는 위험한 작업입니다. 현재 구현은 확인 절차 없이 즉시 실행됩니다.

다음 중 하나 이상의 보호 장치를 고려하세요:

  1. 확인 매개변수: 요청 본문에 {"confirm": true} 같은 명시적 확인 필드 추가
  2. 환경별 제한: 프로덕션 환경에서는 비활성화하고 개발/스테이징 환경에서만 활성화
  3. 추가 권한 검증: ADMIN 역할만 접근 가능하도록 메서드 레벨 @PreAuthorize 추가
  4. 감사 로그: 누가 언제 초기화했는지 기록
🔎 확인 매개변수 추가 예시

먼저 확인 요청 DTO를 생성:

public record ResetConfirmRequest(
    @NotNull
    Boolean confirm
) {}

그 다음 엔드포인트 수정:

 @PostMapping("/lucky-draw/reset")
-public ResponseEntity<ApiResponse<Map<String, Object>, Void>> resetWinners() {
+public ResponseEntity<ApiResponse<Map<String, Object>, Void>> resetWinners(@Valid @RequestBody ResetConfirmRequest req) {
+    if (!Boolean.TRUE.equals(req.confirm())) {
+        throw new IllegalArgumentException("명시적 확인이 필요합니다");
+    }
     long updated = service.resetWinners();
     return okUpdated(GuestbookMessage.WINNER_RESET_SUCCESS, updated);
 }

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

🤖 Prompt for AI Agents
In src/main/java/inha/gdgoc/domain/guestbook/controller/GuestbookController.java
around lines 75-79, the resetWinners endpoint performs a dangerous delete
without protection; update it to require explicit confirmation and stronger
access/audit controls: change the handler to accept a request body DTO (e.g.,
ResetConfirmRequest with a non-null Boolean confirm) and return 400 if confirm
is not true, add method-level authorization (e.g.,
@PreAuthorize("hasRole('ADMIN')")) so only admins can invoke it, gate the
operation by environment if needed (disable in production via a config flag),
and record an audit entry with the user id and timestamp before performing
service.resetWinners(); ensure validation annotations are applied and requests
lacking confirmation are rejected.


@Getter
@Entity
@Table(name = "guestbook_entry", indexes = {@Index(name = "idx_guestbook_created_at", columnList = "createdAt")})
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

인덱스 컬럼명 수정 필요

인덱스 정의에서 Java 필드명(createdAt) 대신 실제 데이터베이스 컬럼명(created_at)을 사용해야 합니다.

🔎 제안하는 수정사항
-@Table(name = "guestbook_entry", indexes = {@Index(name = "idx_guestbook_created_at", columnList = "createdAt")})
+@Table(name = "guestbook_entry", indexes = {@Index(name = "idx_guestbook_created_at", columnList = "created_at")})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Table(name = "guestbook_entry", indexes = {@Index(name = "idx_guestbook_created_at", columnList = "createdAt")})
@Table(name = "guestbook_entry", indexes = {@Index(name = "idx_guestbook_created_at", columnList = "created_at")})
🤖 Prompt for AI Agents
In src/main/java/inha/gdgoc/domain/guestbook/entity/GuestbookEntry.java around
line 10, the @Table index uses the Java field name "createdAt" instead of the
actual DB column name "created_at"; update the @Index annotation's columnList to
use the database column name "created_at" (i.e., replace "createdAt" with
"created_at") so the index is created on the correct column.

Comment on lines +23 to +24
@Column(nullable = false, updatable = false)
private final LocalDateTime createdAt = LocalDateTime.now();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

createdAt 초기화 방식 수정 필요

createdAtfinal 필드로 선언하고 LocalDateTime.now()로 직접 초기화하는 방식은 다음과 같은 문제가 있습니다:

  1. 객체 생성 시점과 실제 DB 저장 시점이 다를 경우 부정확한 타임스탬프가 기록됩니다
  2. JPA는 final 필드를 프록시 생성 시 처리하기 어렵습니다
  3. 프로젝트 내 다른 엔티티들이 사용하는 BaseEntity의 감사(auditing) 패턴과 일관성이 없습니다

BaseEntity를 상속받아 @CreatedDate를 사용하거나, @PrePersist를 활용하는 것을 권장합니다.

🔎 제안하는 수정사항 (BaseEntity 상속)

BaseEntity를 상속받는 방식:

+import inha.gdgoc.global.entity.BaseEntity;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
 @Getter
 @Entity
+@EntityListeners(AuditingEntityListener.class)
-@Table(name = "guestbook_entry", indexes = {@Index(name = "idx_guestbook_created_at", columnList = "createdAt")})
-public class GuestbookEntry {
+@Table(name = "guestbook_entry", indexes = {@Index(name = "idx_guestbook_created_at", columnList = "created_at")})
+public class GuestbookEntry extends BaseEntity {
 
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
     private Long id;
 
     @Column(name = "wristband_serial", nullable = false, unique = true, length = 32)
     private String wristbandSerial; // 손목밴드 일련번호(키)
 
     @Column(nullable = false, length = 50)
     private String name;
 
-    @Column(nullable = false, updatable = false)
-    private final LocalDateTime createdAt = LocalDateTime.now();
-
     private LocalDateTime wonAt; // 당첨 시각 (null이면 미당첨)

참고: BaseEntityInstant 타입의 createdAtupdatedAt을 제공하므로, LocalDateTime이 필요한 경우 별도 고려가 필요합니다.

🤖 Prompt for AI Agents
In src/main/java/inha/gdgoc/domain/guestbook/entity/GuestbookEntry.java around
lines 23-24, the createdAt field is declared final and initialized with
LocalDateTime.now(), which causes inaccurate timestamps, breaks JPA proxying,
and is inconsistent with the project's auditing pattern; remove the final and
the direct initialization, inherit and use the project's BaseEntity (which
exposes createdAt via @CreatedDate) or annotate a non-final
Instant/LocalDateTime field with @CreatedDate, or implement a @PrePersist method
to set the timestamp at persist time, ensuring the field is mutable and
compatible with JPA auditing.

@CSE-Shaco CSE-Shaco merged commit 303b53d into GDGoCINHA:develop Dec 19, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant