-
Notifications
You must be signed in to change notification settings - Fork 1
feat(guestbook, luckydraw): 방명록 및 방명록 연계 추첨 기능 추가 #262
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Walkthrough새로운 방명록 관리 기능을 추가합니다. 방명록 항목 CRUD 작업, 럭키드로우 추첨 기능, 우승자 관리 기능을 포함한 REST API 엔드포인트, 서비스 계층, JPA 엔티티 및 관련 DTO들을 도입합니다. Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50분
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this 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: 생성자 입력값 검증 고려생성자에서
wristbandSerial과name에 대한 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: 수동 입력 검증의 중복성 검토서비스 레이어에서
wristbandSerial과name을 수동으로 검증하고 있습니다. 이러한 검증은GuestbookCreateRequestDTO의@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 상태 코드 및 중복 검증 확인엔드포인트 구현은 정확하지만 다음 사항을 고려하세요:
- HTTP 상태 코드: 리소스 생성 시
200 OK대신201 CREATED를 반환하는 것이 REST 모범 사례입니다- 중복 처리: 동일한
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
📒 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: 럭키드로우 추첨 로직의 동시성 처리는 이미 적절히 구현됨코드 검토 결과, 럭키드로우 추첨 로직은 다음과 같이 이미 동시성 문제를 적절히 처리하고 있습니다:
- GuestbookService는 클래스 레벨의
@Transactional로 보호됨 (draw 메서드 포함)findAllByWonAtIsNullForUpdate()는@Lock(LockModeType.PESSIMISTIC_WRITE)를 사용하여 비관적 락 적용markWon()으로 당첨자를 표시하고, 쿼리의WHERE g.wonAt is null조건으로 재선정 방지동시 추첨 요청은 락으로 인해 직렬화되며, 당첨자 중복 선정은 없습니다.
Likely an incorrect or invalid review comment.
| @GetMapping("/entries/{entryId}") | ||
| public ResponseEntity<ApiResponse<GuestbookEntryResponse, Void>> getEntry(@PathVariable Long entryId) { | ||
| return ResponseEntity.ok(ApiResponse.ok(GuestbookMessage.ENTRY_RETRIEVED_SUCCESS, service.get(entryId))); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
경로 변수 검증 누락
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.
| @PostMapping("/lucky-draw/reset") | ||
| public ResponseEntity<ApiResponse<Map<String, Object>, Void>> resetWinners() { | ||
| long updated = service.resetWinners(); | ||
| return okUpdated(GuestbookMessage.WINNER_RESET_SUCCESS, updated); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
위험한 삭제 작업 보호 필요
당첨자 초기화는 데이터를 삭제하는 위험한 작업입니다. 현재 구현은 확인 절차 없이 즉시 실행됩니다.
다음 중 하나 이상의 보호 장치를 고려하세요:
- 확인 매개변수: 요청 본문에
{"confirm": true}같은 명시적 확인 필드 추가 - 환경별 제한: 프로덕션 환경에서는 비활성화하고 개발/스테이징 환경에서만 활성화
- 추가 권한 검증: ADMIN 역할만 접근 가능하도록 메서드 레벨
@PreAuthorize추가 - 감사 로그: 누가 언제 초기화했는지 기록
🔎 확인 매개변수 추가 예시
먼저 확인 요청 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")}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
인덱스 컬럼명 수정 필요
인덱스 정의에서 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.
| @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.
| @Column(nullable = false, updatable = false) | ||
| private final LocalDateTime createdAt = LocalDateTime.now(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
createdAt 초기화 방식 수정 필요
createdAt을 final 필드로 선언하고 LocalDateTime.now()로 직접 초기화하는 방식은 다음과 같은 문제가 있습니다:
- 객체 생성 시점과 실제 DB 저장 시점이 다를 경우 부정확한 타임스탬프가 기록됩니다
- JPA는
final필드를 프록시 생성 시 처리하기 어렵습니다 - 프로젝트 내 다른 엔티티들이 사용하는
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이면 미당첨)참고: BaseEntity는 Instant 타입의 createdAt과 updatedAt을 제공하므로, 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.
📌 연관된 이슈
✨ 작업 내용
💬 리뷰 요구사항(선택)
Summary by CodeRabbit
릴리스 노트
✏️ Tip: You can customize this high-level summary in your review settings.