Skip to content

Feature/ticket#10

Merged
naooung merged 5 commits intomainfrom
feature/ticket
Dec 13, 2025
Merged

Feature/ticket#10
naooung merged 5 commits intomainfrom
feature/ticket

Conversation

@naooung
Copy link
Member

@naooung naooung commented Dec 12, 2025

✨ 새로운 기능

  • 공통 티켓 발급·검증·체크인 시스템을 추가했습니다.
  • EVENT / CLOTH 유형을 하나의 Ticket 도메인으로 통합 관리합니다.
  • QR 기반 체크인 플로우를 도입하여 관리자가 간편하게 티켓을 검증·사용 처리할 수 있습니다.
  • 사용자는 발급된 티켓을 QR 형태로 간편하게 보관·제시할 수 있습니다.
  • EVENT 티켓은 행사 참여 이력 확인이 가능하고, CLOTH 티켓은 지급 내역을 한눈에 확인할 수 있습니다.
  • QR 재스캔 시에도 안전하게 처리되는 멱등 체크인으로 현장 혼선을 방지합니다.

🛠 개발 상세

  • Ticket 도메인 신규 설계
    • 단일 Ticket 엔티티로 EVENT / CLOTH 티켓을 통합 관리
    • type + targetId + userId + token 기반 구조
  • 토큰 기반 QR 설계
    • 서버에서 난수 토큰 발급
    • QR Payload: https://api.refitlab.site/ticket?v=1&token={token}
  • 주요 기능
    • 티켓 발급 (issueTicket)
    • 티켓 검증 (verifyTicket) – 사용 여부 확인만 수행
    • 티켓 사용(체크인) (consumeTicket) – 멱등 처리
  • 권한 분리
    • 조회: 사용자 권한
    • 검증/체크인: 관리자 권한
  • Mapper 책임 분리
    • Ticket 생성, QR payload 조립, Response DTO 변환 로직을 Mapper로 이동
  • 조회 정책
    • EVENT: 사용 완료된 티켓만 사용자 조회 가능
    • CLOTH: 발급된 티켓 전체 조회(사용 여부 무관)

🔗 관련 문서 / 이슈

Summary by CodeRabbit

  • 새로운 기능
    • 티켓 발급, 검증, 사용(consume) API 추가 — 발급·검증·사용 흐름 제공
    • 내 이벤트·의류 티켓 목록 조회 기능 추가
    • QR 페이로드 생성 및 URL 안전 토큰 생성 기능 추가
    • 티켓 타입(이벤트/의류) 도입 및 관련 응답 DTO 추가
    • 티켓 관련 표준화된 오류 코드 추가 (상태별 메시지 포함)

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

@naooung naooung self-assigned this Dec 12, 2025
@naooung naooung added ✨ feature 새로운 기능 요청 🟠 priority: medium 중간 우선순위 labels Dec 12, 2025
@naooung naooung linked an issue Dec 12, 2025 that may be closed by this pull request
3 tasks
@coderabbitai
Copy link

coderabbitai bot commented Dec 12, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

이 PR은 티켓 발급·검증·소비 기능을 도메인으로 추가합니다. REST 컨트롤러, 서비스 구현, JPA 엔티티와 리포지토리(비관적 락 포함), DTO들, 매퍼, 토큰/QR 유틸리티, 및 티켓 전용 에러 코드가 새로 도입됩니다. (약 50단어)

Changes

Cohort / File(s) Summary
Controller Layer
src/main/java/com/sku/refit/domain/ticket/controller/*
\src/main/java/com/sku/refit/domain/ticket/controller/TicketController.java`, `src/main/java/com/sku/refit/domain/ticket/controller/TicketControllerImpl.java``
인터페이스와 구현체 추가. /api/tickets 아래 5개 엔드포인트(POST /dev/issue, POST /admin/verify, POST /admin/consume, GET /my/events, GET /my/cloth)를 노출하고 TicketService로 위임하여 BaseResponse로 응답을 감쌉니다.
DTOs (request/response)
src/main/java/com/sku/refit/domain/ticket/dto/request/TicketRequest.java, \src/main/java/com/sku/refit/domain/ticket/dto/response/TicketResponse.java``
요청 DTO 3종(IssueTicketRequest, VerifyTicketRequest, ConsumeTicketRequest)과 응답 DTO 4종(TicketDetailResponse, VerifyTicketResponse, ConsumeTicketResponse, MyTicketItemResponse)을 추가(검증 어노테이션·빌더 포함).
Entity & Enum
src/main/java/com/sku/refit/domain/ticket/entity/*
\src/main/java/com/sku/refit/domain/ticket/entity/Ticket.java`, `src/main/java/com/sku/refit/domain/ticket/entity/TicketType.java``
JPA 엔티티 Ticket 추가(테이블 매핑·인덱스·필드·도메인 메서드 isUsed/consume/validate 포함). 티켓 타입 enum TicketType(EVENT, CLOTH) 추가.
Repository
src/main/java/com/sku/refit/domain/ticket/repository/TicketRepository.java
JpaRepository 확장 및 토큰으로 조회하는 메서드, 사용자·타입 기반 목록 조회, 그리고 @Lock(PESSIMISTIC_WRITE)를 사용한 findByTokenForUpdate 쿼리 추가.
Service Layer
src/main/java/com/sku/refit/domain/ticket/service/*
\src/main/java/com/sku/refit/domain/ticket/service/TicketService.java`, `src/main/java/com/sku/refit/domain/ticket/service/TicketServiceImpl.java``
서비스 인터페이스와 구현체 추가. issueTicket, verifyTicket, consumeTicket, getMyTicketsUsedEvents, getMyClothTickets 구현(트랜잭션·검증·에러 처리 포함).
Mapper & Utilities
src/main/java/com/sku/refit/domain/ticket/mapper/TicketMapper.java, src/main/java/com/sku/refit/domain/ticket/util/*
\TicketMapper.java`, `TicketTokenGenerator.java`, `TicketQrPayloadFactory.java``
엔티티↔DTO 매핑기 추가. 32바이트 난수 기반 URL-safe Base64 토큰 생성기와 QR 페이로드 URL 생성기 추가.
Error Handling
src/main/java/com/sku/refit/domain/ticket/exception/TicketErrorCode.java
BaseErrorCode 구현 enum 추가: 여러 티켓 관련 에러 코드와 메시지 및 HttpStatus 매핑을 제공.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller as TicketController
    participant Service as TicketService
    participant TokenGen as TicketTokenGenerator
    participant Repo as TicketRepository
    participant DB as Database

    Client->>Controller: POST /dev/issue (type, targetId, userId)
    Controller->>Service: issueTicket(type, targetId, userId)
    Service->>TokenGen: generate()
    TokenGen-->>Service: token
    Service->>Repo: save(Ticket entity)
    Repo->>DB: INSERT ticket
    DB-->>Repo: persisted ticket
    Repo-->>Service: saved entity
    Service-->>Controller: TicketDetailResponse
    Controller-->>Client: BaseResponse<TicketDetailResponse>
Loading
sequenceDiagram
    participant Client
    participant Controller as TicketController
    participant Service as TicketService
    participant Repo as TicketRepository
    participant DB as Database
    participant Mapper as TicketMapper

    Client->>Controller: POST /admin/verify (token)
    Controller->>Service: verifyTicket(request)
    Service->>Repo: findByTokenForUpdate(token)
    Note over Repo: PESSIMISTIC_WRITE
    Repo->>DB: SELECT ... FOR UPDATE
    alt found
        DB-->>Repo: Ticket
        Repo-->>Service: Ticket
        Service->>Mapper: toVerifyFound(ticket)
        Mapper-->>Service: VerifyTicketResponse(valid=true,...)
    else not found
        DB-->>Repo: empty
        Repo-->>Service: empty
        Service->>Mapper: toVerifyNotFound()
        Mapper-->>Service: VerifyTicketResponse(valid=false)
    end
    Service-->>Controller: VerifyTicketResponse
    Controller-->>Client: BaseResponse<VerifyTicketResponse>
Loading
sequenceDiagram
    participant Client
    participant Controller as TicketController
    participant Service as TicketService
    participant Repo as TicketRepository
    participant DB as Database
    participant Mapper as TicketMapper

    Client->>Controller: POST /admin/consume (token)
    Controller->>Service: consumeTicket(request)
    Service->>Repo: findByToken(token)
    Repo->>DB: SELECT ticket
    alt found
        DB-->>Repo: Ticket
        Repo-->>Service: Ticket
        Service->>Service: ticket.consume(now) (idempotent)
        Service->>Repo: save(ticket)
        Repo->>DB: UPDATE ticket
        Service->>Mapper: toConsume(ticket, consumed)
        Mapper-->>Service: ConsumeTicketResponse
    else not found
        Repo-->>Service: empty -> throw not-found
    end
    Service-->>Controller: ConsumeTicketResponse
    Controller-->>Client: BaseResponse<ConsumeTicketResponse>
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20–30 minutes

  • 집중 검토 권장 파일/영역:
    • src/main/java/com/sku/refit/domain/ticket/service/TicketServiceImpl.java: 트랜잭션 경계, 예외 매핑, 검증 로직
    • src/main/java/com/sku/refit/domain/ticket/repository/TicketRepository.java: PESSIMISTIC_WRITE 쿼리 및 동시성 영향
    • src/main/java/com/sku/refit/domain/ticket/entity/Ticket.java: consume()의 idempotency 및 validate()의 필드 검증
    • 토큰 생성 및 QR 페이로드: TicketTokenGenerator / TicketQrPayloadFactory의 보안·구성 적절성
    • 에러 코드 매핑(TicketErrorCode)과 기존 에러 처리 관례의 일관성

Poem

토큰을 굴리는 작은 토끼가 말하네,
"삐걱대던 발권이 이제는 다 정리됐네.
QR은 반짝, 락은 꽉! 모두 안전히,
발급·검증·소비, 한입에 해결했네." 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.35% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive PR 제목이 '새로운 기능'을 암시하지만 구체적인 변경사항을 명확히 설명하지 않으므로 너무 일반적이고 모호합니다. 제목을 'Feature: Unified Ticket System with QR-based Check-in' 또는 '통합 티켓 도메인 및 QR 기반 체크인 시스템 추가'와 같이 더 구체적으로 변경하세요.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed PR 설명이 필수 섹션(새로운 기능, 개발 상세, 관련 이슈)을 완벽하게 포함하고 있으며, 기능, 구현 방식, 권한 분리, 조회 정책을 상세히 설명하고 있습니다.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between df7943c and 943406e.

📒 Files selected for processing (1)
  • src/main/java/com/sku/refit/domain/ticket/service/TicketService.java (1 hunks)

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: 5

🧹 Nitpick comments (9)
src/main/java/com/sku/refit/domain/ticket/util/TicketQrPayloadFactory.java (2)

11-12: QR 기본 URL을 설정 파일로 외부화하는 것을 권장합니다.

현재 QR 페이로드 기본 URL과 버전이 하드코딩되어 있습니다. 환경별(개발/스테이징/프로덕션) URL 관리와 유연성을 위해 application.properties 또는 application.yml로 외부화하는 것을 고려해보세요.

예시:

@Component
public class TicketQrPayloadFactory {

  @Value("${ticket.qr.base-url}")
  private String qrBaseUrl;
  
  @Value("${ticket.qr.version}")
  private String qrVersion;

  public String create(String token) {
    return qrBaseUrl + "?v=" + qrVersion + "&token=" + token;
  }
}

22-24: 토큰 파라미터 검증을 추가하는 것을 고려해보세요.

token이 null이거나 빈 문자열인 경우를 방어적으로 체크하면 더 안전합니다. TicketTokenGenerator가 항상 유효한 토큰을 생성하지만, 이 메서드가 다른 곳에서도 호출될 수 있습니다.

  public String create(String token) {
+   if (token == null || token.isBlank()) {
+     throw new IllegalArgumentException("Token은 필수입니다.");
+   }
    return QR_BASE_URL + "?v=" + QR_VERSION + "&token=" + token;
  }
src/main/java/com/sku/refit/domain/ticket/service/TicketService.java (1)

25-96: 서비스 메서드 네이밍을 컨트롤러와 일치시키는 게 좋습니다.

getMyTicketsUsedEvents()(Line 86) vs 컨트롤러의 getMyUsedEventTickets()로 표현이 달라 추후 탐색/유지보수 시 헷갈릴 수 있어요. 한쪽으로 통일 추천합니다.

src/main/java/com/sku/refit/domain/ticket/mapper/TicketMapper.java (1)

31-86: qrPayloadFactory.create(ticket.getToken()) 중복 호출은 지역 변수로 정리 추천.

현재 toDetail/toMyItem/toVerifyFound/toConsume에서 동일 표현이 반복됩니다. 토큰/페이로드를 지역 변수로 빼면 가독성과 디버깅이 좋아집니다.

   public TicketDetailResponse toDetail(Ticket ticket) {
+    final String token = ticket.getToken();
+    final String qrPayload = qrPayloadFactory.create(token);
     return TicketDetailResponse.builder()
         .ticketId(ticket.getId())
         .type(ticket.getType())
         .targetId(ticket.getTargetId())
-        .token(ticket.getToken())
-        .qrPayload(qrPayloadFactory.create(ticket.getToken()))
+        .token(token)
+        .qrPayload(qrPayload)
         .issuedAt(ticket.getCreatedAt())
         .usedAt(ticket.getUsedAt())
         .build();
   }
src/main/java/com/sku/refit/domain/ticket/dto/request/TicketRequest.java (1)

18-43: 요청 DTO에 최소한의 범위 검증(@Positive/@SiZe) 추가를 권장합니다.

targetId는 음수/0을 막고, token은 과도한 길이 입력을 제한하는 게 안전합니다.

 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
+import jakarta.validation.constraints.Size;

   public static class IssueTicketRequest {
     @NotNull private TicketType type;
-    @NotNull private Long targetId;
+    @NotNull @Positive private Long targetId;
     ...
-    private Long userId;
+    @Positive private Long userId;
   }

   public static class ConsumeTicketRequest {
-    @NotBlank private String token;
+    @NotBlank @Size(max = 256) private String token;
   }

   public static class VerifyTicketRequest {
-    @NotBlank private String token;
+    @NotBlank @Size(max = 256) private String token;
   }
src/main/java/com/sku/refit/domain/ticket/exception/TicketErrorCode.java (1)

16-27: 에러 코드 포맷을 일관되게 맞추는 것을 추천합니다.

TICKET_NOT_FOUND("TICKET0010", ...)만 자릿수가 튀어서 운영/로그 필터링 시 혼동될 수 있어요. 정책(예: 3자리/4자리)을 정하고 통일하는 게 좋습니다.

src/main/java/com/sku/refit/domain/ticket/service/TicketServiceImpl.java (3)

12-13: 와일드카드 임포트 대신 명시적 임포트 사용 권장

와일드카드 임포트(.*)는 어떤 클래스가 실제로 사용되는지 파악하기 어렵게 만들고, 향후 동일한 이름의 클래스가 추가될 경우 충돌이 발생할 수 있습니다.

-import com.sku.refit.domain.ticket.dto.request.TicketRequest.*;
-import com.sku.refit.domain.ticket.dto.response.TicketResponse.*;
+import com.sku.refit.domain.ticket.dto.request.TicketRequest.ConsumeTicketRequest;
+import com.sku.refit.domain.ticket.dto.request.TicketRequest.IssueTicketRequest;
+import com.sku.refit.domain.ticket.dto.request.TicketRequest.VerifyTicketRequest;
+import com.sku.refit.domain.ticket.dto.response.TicketResponse.ConsumeTicketResponse;
+import com.sku.refit.domain.ticket.dto.response.TicketResponse.MyTicketItemResponse;
+import com.sku.refit.domain.ticket.dto.response.TicketResponse.TicketDetailResponse;
+import com.sku.refit.domain.ticket.dto.response.TicketResponse.VerifyTicketResponse;

186-196: DTO의 Bean Validation과 중복되는 검증 로직

IssueTicketRequest@NotNull 어노테이션과 VerifyTicketRequest/ConsumeTicketRequest@NotBlank 어노테이션이 이미 컨트롤러 레벨에서 @Valid로 검증된다면, 이 수동 검증은 중복될 수 있습니다.

단, 서비스가 컨트롤러 외 다른 곳에서 직접 호출될 수 있다면 방어적 프로그래밍으로서 유지해도 좋습니다.


65-75: 반복되는 예외 처리 패턴

catch (CustomException e) { throw e; } catch (Exception e) { ... } 패턴이 여러 메서드에서 반복됩니다. 현재 구조도 동작하지만, 향후 유지보수를 위해 공통 유틸리티 메서드나 AOP를 고려해볼 수 있습니다.

예시:

private <T> T executeWithErrorHandling(Supplier<T> action, String context, TicketErrorCode errorCode) {
    try {
        return action.get();
    } catch (CustomException e) {
        throw e;
    } catch (Exception e) {
        log.error("[TICKET] {} - failed", context, e);
        throw new CustomException(errorCode);
    }
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a2139d0 and 756c2c9.

📒 Files selected for processing (14)
  • src/main/java/com/sku/refit/domain/ticket/controller/TicketController.java (1 hunks)
  • src/main/java/com/sku/refit/domain/ticket/controller/TicketControllerImpl.java (1 hunks)
  • src/main/java/com/sku/refit/domain/ticket/dto/request/TicketRequest.java (1 hunks)
  • src/main/java/com/sku/refit/domain/ticket/dto/response/TicketResponse.java (1 hunks)
  • src/main/java/com/sku/refit/domain/ticket/entity/Ticket.java (1 hunks)
  • src/main/java/com/sku/refit/domain/ticket/entity/TicketStatus.java (1 hunks)
  • src/main/java/com/sku/refit/domain/ticket/entity/TicketType.java (1 hunks)
  • src/main/java/com/sku/refit/domain/ticket/exception/TicketErrorCode.java (1 hunks)
  • src/main/java/com/sku/refit/domain/ticket/mapper/TicketMapper.java (1 hunks)
  • src/main/java/com/sku/refit/domain/ticket/repository/TicketRepository.java (1 hunks)
  • src/main/java/com/sku/refit/domain/ticket/service/TicketService.java (1 hunks)
  • src/main/java/com/sku/refit/domain/ticket/service/TicketServiceImpl.java (1 hunks)
  • src/main/java/com/sku/refit/domain/ticket/util/TicketQrPayloadFactory.java (1 hunks)
  • src/main/java/com/sku/refit/domain/ticket/util/TicketTokenGenerator.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (9)
src/main/java/com/sku/refit/domain/ticket/mapper/TicketMapper.java (4)
src/main/java/com/sku/refit/domain/ticket/util/TicketQrPayloadFactory.java (1)
  • Component (8-25)
src/main/java/com/sku/refit/domain/ticket/util/TicketTokenGenerator.java (1)
  • Component (11-21)
src/main/java/com/sku/refit/domain/ticket/dto/request/TicketRequest.java (1)
  • TicketRequest (13-44)
src/main/java/com/sku/refit/domain/ticket/dto/response/TicketResponse.java (1)
  • TicketResponse (12-72)
src/main/java/com/sku/refit/domain/ticket/controller/TicketControllerImpl.java (2)
src/main/java/com/sku/refit/domain/ticket/dto/request/TicketRequest.java (1)
  • TicketRequest (13-44)
src/main/java/com/sku/refit/domain/ticket/dto/response/TicketResponse.java (1)
  • TicketResponse (12-72)
src/main/java/com/sku/refit/domain/ticket/dto/request/TicketRequest.java (1)
src/main/java/com/sku/refit/domain/ticket/dto/response/TicketResponse.java (4)
  • Getter (14-26)
  • Getter (28-42)
  • Getter (44-56)
  • Getter (59-71)
src/main/java/com/sku/refit/domain/ticket/service/TicketServiceImpl.java (2)
src/main/java/com/sku/refit/domain/ticket/dto/request/TicketRequest.java (1)
  • TicketRequest (13-44)
src/main/java/com/sku/refit/domain/ticket/dto/response/TicketResponse.java (1)
  • TicketResponse (12-72)
src/main/java/com/sku/refit/domain/ticket/util/TicketQrPayloadFactory.java (2)
src/main/java/com/sku/refit/domain/ticket/mapper/TicketMapper.java (1)
  • Component (15-87)
src/main/java/com/sku/refit/domain/ticket/util/TicketTokenGenerator.java (1)
  • Component (11-21)
src/main/java/com/sku/refit/domain/ticket/service/TicketService.java (2)
src/main/java/com/sku/refit/domain/ticket/dto/request/TicketRequest.java (1)
  • TicketRequest (13-44)
src/main/java/com/sku/refit/domain/ticket/dto/response/TicketResponse.java (1)
  • TicketResponse (12-72)
src/main/java/com/sku/refit/domain/ticket/util/TicketTokenGenerator.java (2)
src/main/java/com/sku/refit/domain/ticket/mapper/TicketMapper.java (1)
  • Component (15-87)
src/main/java/com/sku/refit/domain/ticket/util/TicketQrPayloadFactory.java (1)
  • Component (8-25)
src/main/java/com/sku/refit/domain/ticket/dto/response/TicketResponse.java (1)
src/main/java/com/sku/refit/domain/ticket/dto/request/TicketRequest.java (3)
  • Getter (15-29)
  • Getter (31-36)
  • Getter (38-43)
src/main/java/com/sku/refit/domain/ticket/controller/TicketController.java (2)
src/main/java/com/sku/refit/domain/ticket/dto/request/TicketRequest.java (1)
  • TicketRequest (13-44)
src/main/java/com/sku/refit/domain/ticket/dto/response/TicketResponse.java (1)
  • TicketResponse (12-72)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build
🔇 Additional comments (17)
src/main/java/com/sku/refit/domain/ticket/entity/TicketType.java (1)

8-14: 티켓 타입 정의가 명확합니다.

두 가지 티켓 타입(EVENT, CLOTH)이 명확하게 정의되어 있고, Swagger 문서화를 위한 설명도 적절합니다.

src/main/java/com/sku/refit/domain/ticket/util/TicketTokenGenerator.java (1)

11-21: 안전한 토큰 생성 로직입니다.

SecureRandom을 사용하여 256비트(32바이트) 토큰을 생성하고, URL-safe Base64 인코딩을 적용한 구현이 적절합니다. 토큰 강도와 URL 호환성이 모두 확보되었습니다.

src/main/java/com/sku/refit/domain/ticket/repository/TicketRepository.java (1)

14-22: Repository 메서드 정의가 적절합니다.

쿼리 메서드명이 Spring Data JPA 규칙을 준수하고 있으며, Ticket 엔티티에 정의된 인덱스들(user_id, type+target_id, token, used_at)과 잘 매칭되어 쿼리 성능이 최적화되어 있습니다.

src/main/java/com/sku/refit/domain/ticket/dto/response/TicketResponse.java (4)

14-26: 발급 응답 DTO가 적절하게 구성되어 있습니다.

티켓 발급 시 사용자가 QR 코드를 생성/저장할 수 있도록 tokenqrPayload 모두를 포함하고 있습니다. PR 목표에 명시된 대로 사용자가 QR을 제시할 수 있도록 설계되었습니다.


28-42: 검증 응답에서 토큰을 노출하지 않는 것이 적절합니다.

관리자의 티켓 검증 시 사용되는 응답으로, validused 상태 정보만 포함하고 민감한 token 필드는 제외되어 있어 보안상 적절합니다.


44-56: 체크인 응답 구조가 명확합니다.

소비(체크인) 처리 결과를 나타내는 consumed 필드와 함께 필요한 티켓 정보만 포함하고 있습니다. 토큰을 포함하지 않은 것도 적절합니다.


58-71: 사용자 조회용 DTO가 요구사항을 잘 반영하고 있습니다.

사용자가 본인의 티켓 목록을 조회할 때 QR 코드를 다시 확인할 수 있도록 tokenqrPayload를 포함하고 있습니다. 주석에 명시된 대로 EVENT와 CLOTH 타입별 조회 정책 차이를 지원합니다.

src/main/java/com/sku/refit/domain/ticket/entity/Ticket.java (4)

19-26: 인덱스 구성이 쿼리 패턴에 최적화되어 있습니다.

정의된 4개의 인덱스가 TicketRepository의 쿼리 메서드들과 잘 매칭됩니다:

  • idx_ticket_user: 사용자별 티켓 조회
  • idx_ticket_type_target: 타입+대상 조합 조회
  • idx_ticket_token: 토큰 기반 검증 (unique 제약조건과 함께)
  • idx_ticket_used_at: 사용 이력 필터링

47-49: 토큰 필드의 길이와 제약조건이 적절합니다.

URL-safe Base64로 인코딩된 32바이트 토큰(약 43자)을 저장하기에 128자는 충분하며, unique 제약조건으로 토큰 중복을 방지하고 있습니다.


64-70: 멱등성이 보장된 소비 로직입니다.

consume() 메서드가 이미 사용된 티켓을 재처리하지 않도록 구현되어 있어, PR 목표에 명시된 "멱등 처리로 중복 처리 방지"를 달성하고 있습니다. QR 코드 재스캔 시에도 안전합니다.


72-86: 도메인 검증 로직이 명확합니다.

필수 필드들(type, targetId, userId, token)에 대한 검증을 수행하고, 위반 시 명확한 한글 메시지와 함께 IllegalStateException을 발생시킵니다. 도메인 무결성을 보장하는 좋은 방어 코드입니다.

src/main/java/com/sku/refit/domain/ticket/controller/TicketControllerImpl.java (1)

21-54: 권한 검증이 전역 보안 설정에서 구현되어 있습니다. 추가 검토 필요.

보안 설정을 확인한 결과, 권한 검증은 TicketController의 애노테이션이 아닌 SecurityConfig의 전역 규칙에서 구현되어 있습니다 (lines 128-131):

  • /admin/*ADMIN 역할 필수
  • /dev/*DEVELOPER 역할 필수
  • 그 외 /my/* → 인증된 사용자만 접근 가능

다만 다음을 확인해주세요:

  • issueTicket은 현재 [개발자] 권한이 필요한데, PR 요구사항과 맞는지 확인 필요
  • /my/events, /my/cloth는 현재 인증된 모든 사용자가 접근 가능한데, 본인 티켓만 조회하도록 제한하는 로직이 서비스 계층에 구현되어 있는지 확인 필요
src/main/java/com/sku/refit/domain/ticket/service/TicketService.java (1)

11-24: 인터페이스 분리/역할 정의가 명확합니다.

발급/검증/체크인(멱등)/조회로 책임이 잘 나뉘어 있고, “verify는 사용 처리 없음”을 Javadoc에 명확히 박아둔 점이 좋아요.

src/main/java/com/sku/refit/domain/ticket/mapper/TicketMapper.java (1)

71-74: toVerifyNotFound() 응답 최소 필드만 내려주는 방향은 적절합니다.

valid=false일 때 다른 필드를 비워두는 설계가 깔끔하고, used=false 기본값도 무난합니다.

src/main/java/com/sku/refit/domain/ticket/exception/TicketErrorCode.java (1)

16-31: 에러 케이스 분류와 HttpStatus 매핑은 무난합니다.

BAD_REQUEST / NOT_FOUND / CONFLICT / 500 계열로 구분이 잘 되어 있어요.

src/main/java/com/sku/refit/domain/ticket/service/TicketServiceImpl.java (2)

82-99: LGTM!

티켓이 없는 경우 예외를 던지지 않고 toVerifyNotFound를 반환하는 설계가 검증 API의 목적에 적합합니다.


142-180: LGTM!

EVENT 티켓은 사용된 것만, CLOTH 티켓은 전체를 조회하는 정책이 명확하게 구현되어 있습니다. 스트림 처리와 에러 핸들링도 일관성 있게 적용되었습니다.

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

♻️ Duplicate comments (1)
src/main/java/com/sku/refit/domain/ticket/service/TicketServiceImpl.java (1)

110-129: 동시 consume 경쟁 조건: findByToken() 대신 findByTokenForUpdate()로 잠금 후 사용 처리 필요
현재 구현은 동시에 두 요청이 들어오면 둘 다 alreadyUsed=false로 판단하고 둘 다 consumed=true를 반환할 수 있습니다.

     Ticket ticket =
         ticketRepository
-            .findByToken(request.getToken())
+            .findByTokenForUpdate(request.getToken())
             .orElseThrow(() -> new CustomException(TicketErrorCode.TICKET_NOT_FOUND));
🧹 Nitpick comments (3)
src/main/java/com/sku/refit/domain/ticket/mapper/TicketMapper.java (1)

26-80: QR payload 생성 중복 호출은 로컬 변수로 한 번만 계산해도 충분
동일한 ticket.getToken()으로 qrPayloadFactory.create(...)를 여러 빌더에서 반복 호출하고 있어, 가독성 차원에서 한 번만 계산해도 됩니다.

   public TicketDetailResponse toDetail(Ticket ticket) {
+    String token = ticket.getToken();
+    String qrPayload = qrPayloadFactory.create(token);
     return TicketDetailResponse.builder()
         .ticketId(ticket.getId())
         .type(ticket.getType())
         .targetId(ticket.getTargetId())
-        .token(ticket.getToken())
-        .qrPayload(qrPayloadFactory.create(ticket.getToken()))
+        .token(token)
+        .qrPayload(qrPayload)
         .issuedAt(ticket.getCreatedAt())
         .usedAt(ticket.getUsedAt())
         .build();
   }
src/main/java/com/sku/refit/domain/ticket/service/TicketServiceImpl.java (2)

57-80: 토큰 유니크 충돌/재시도 전략(및 DB 유니크 제약) 고려 필요
SecureRandom 기반이라 충돌 가능성은 매우 낮지만, 운영 안정성을 위해 Ticket.token에 유니크 제약(및 인덱스) + 저장 시 충돌 발생하면 제한된 횟수로 재발급/재시도하는 방식을 고려해볼 만합니다.


191-195: validateIssueRequest()는 현재 코드에서 호출되지 않음
이 PR 상태만 보면 미사용 private 메서드라, 실제로 필요 없으면 제거하거나 issueTicket(...)에서 사용하도록 정리해도 좋겠습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 756c2c9 and df7943c.

📒 Files selected for processing (7)
  • src/main/java/com/sku/refit/domain/ticket/controller/TicketController.java (1 hunks)
  • src/main/java/com/sku/refit/domain/ticket/controller/TicketControllerImpl.java (1 hunks)
  • src/main/java/com/sku/refit/domain/ticket/dto/request/TicketRequest.java (1 hunks)
  • src/main/java/com/sku/refit/domain/ticket/mapper/TicketMapper.java (1 hunks)
  • src/main/java/com/sku/refit/domain/ticket/repository/TicketRepository.java (1 hunks)
  • src/main/java/com/sku/refit/domain/ticket/service/TicketService.java (1 hunks)
  • src/main/java/com/sku/refit/domain/ticket/service/TicketServiceImpl.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/com/sku/refit/domain/ticket/controller/TicketController.java
  • src/main/java/com/sku/refit/domain/ticket/dto/request/TicketRequest.java
🧰 Additional context used
🧬 Code graph analysis (4)
src/main/java/com/sku/refit/domain/ticket/mapper/TicketMapper.java (3)
src/main/java/com/sku/refit/domain/ticket/util/TicketQrPayloadFactory.java (1)
  • Component (8-25)
src/main/java/com/sku/refit/domain/ticket/util/TicketTokenGenerator.java (1)
  • Component (11-21)
src/main/java/com/sku/refit/domain/ticket/dto/response/TicketResponse.java (1)
  • TicketResponse (12-72)
src/main/java/com/sku/refit/domain/ticket/service/TicketService.java (2)
src/main/java/com/sku/refit/domain/ticket/dto/request/TicketRequest.java (1)
  • TicketRequest (13-38)
src/main/java/com/sku/refit/domain/ticket/dto/response/TicketResponse.java (1)
  • TicketResponse (12-72)
src/main/java/com/sku/refit/domain/ticket/controller/TicketControllerImpl.java (2)
src/main/java/com/sku/refit/domain/ticket/dto/request/TicketRequest.java (1)
  • TicketRequest (13-38)
src/main/java/com/sku/refit/domain/ticket/dto/response/TicketResponse.java (1)
  • TicketResponse (12-72)
src/main/java/com/sku/refit/domain/ticket/service/TicketServiceImpl.java (2)
src/main/java/com/sku/refit/domain/ticket/dto/request/TicketRequest.java (1)
  • TicketRequest (13-38)
src/main/java/com/sku/refit/domain/ticket/dto/response/TicketResponse.java (1)
  • TicketResponse (12-72)
🔇 Additional comments (1)
src/main/java/com/sku/refit/domain/ticket/repository/TicketRepository.java (1)

21-31: findByTokenForUpdate() 도입은 좋은데, “consume” 경로에서 실제로 사용되는지 확인 필요
리포지토리 자체는 깔끔합니다. 다만 현재 PR 목적(체크인 멱등/중복 스캔 방지)을 달성하려면 서비스의 consume 로직이 이 메서드를 사용해야 효과가 있습니다.

@naooung naooung merged commit 6424e1a into main Dec 13, 2025
2 of 3 checks passed
@naooung naooung deleted the feature/ticket branch December 13, 2025 04:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feature 새로운 기능 요청 🟠 priority: medium 중간 우선순위

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨Feat: 티켓 관련 기능 개발

1 participant