Skip to content

Conversation

@sjk4618
Copy link
Member

@sjk4618 sjk4618 commented Sep 26, 2025

🔥Pull requests

⛳️ 작업한 브랜치

👷 작업한 내용

  • 도어용 유저 티켓 검증 api

🚨 참고 사항

@sjk4618 sjk4618 self-assigned this Sep 26, 2025
@coderabbitai
Copy link

coderabbitai bot commented Sep 26, 2025

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • 현장 검증용 티켓 확인 API 추가(POST /api/tickets/confirm). 잘못된 코드, 취소, 사용 완료 티켓에 대한 명확한 응답 제공.
  • Improvements
    • 티켓 사용 처리 시 사용 시각 기록으로 이력 정확성 향상.
    • 사용자 구매 티켓 정보 응답 구조 정비로 일관된 표시와 정렬 개선.
  • Refactor
    • 응답 DTO 명칭 및 패키지 구조 정리로 응답 체계 일원화.
  • Chores
    • 티켓 상태·날짜·코드 검증을 위한 예외 유형 추가 및 오류 코드 확장.

Walkthrough

POST /api/tickets/confirm 엔드포인트와 요청 DTO(TicketConfirmRequest)를 추가하고, 티켓 검증/사용 처리 로직(TicketService.confirmTicket)과 관련 예외/에러코드/레포지토리·리트리버 변경을 도입했습니다. 일부 응답 DTO(UserBuyTicketInfoResponse, EventTicketInfoResponse)의 패키지/이름이 변경되었습니다.

Changes

Cohort / File(s) Summary
Controller
src/.../ticket/api/controller/TicketController.java
새 엔드포인트 POST /api/tickets/confirm 추가: @RequestBody @Valid TicketConfirmRequest 수신 후 서비스의 confirmTicket 호출 및 성공 응답 반환.
Request DTO
src/.../ticket/api/dto/req/TicketConfirmRequest.java
record TicketConfirmRequest(@NotBlank String ticketCode, @NotBlank String checkCode) 추가.
Response DTOs (패키지/이름 변경)
src/.../ticket/api/dto/res/EventTicketInfoResponse.java, src/.../ticket/api/dto/res/UserBuyTicketInfoResponse.java
EventTicketInfoResponse 패키지 이동 및 UserBuyTicketInfoUserBuyTicketInfoResponse로 타입명·패키지 변경.
Service
src/.../ticket/api/service/TicketService.java
confirmTicket(String ticketCode, String checkCode) 추가(상태/날짜/체크코드 검증 → USED로 상태 변경 및 저장). getUserBuyTicketInfo 반환 타입을 UserBuyTicketInfoResponse로 변경. 예외 매핑 및 검증 헬퍼 추가. TicketUpdater 의존성 사용.
Retriever / Repository / Type retriever
src/.../ticket/core/component/TicketRetriever.java, src/.../ticket/core/repository/TicketRepository.java, src/.../tickettype/core/component/TicketTypeRetriever.java
findTicketEntityByTicketCode(String) 추가, TicketRepository.findByTicketCode(...) 추가(Optional 반환). TicketTypeRetriever.findTicketTypeById(long) 추가(도메인 객체 반환).
Domain Entity
src/.../ticket/core/domain/entity/TicketEntity.java
티켓 상태를 USED로 변경할 때 usedTimeLocalDateTime.now()로 설정하도록 수정.
API 예외 클래스
src/.../ticket/api/exception/ConflictTicketException.java, .../DateTicketException.java, .../IllegalTicketException.java
TicketApiException을 확장하는 예외 3종 추가(각 생성자는 ErrorCode를 받음).
Error Codes
src/.../global/response/code/ErrorCode.java
에러 코드 추가: BAD_REQUEST_TICKET_CHECK_CODE_ERROR, BAD_REQUEST_CANCELED_TICKET, CONFLICT_ALREADY_USED_TICKET.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client as 도어스캐너
  participant C as TicketController
  participant S as TicketService
  participant R as TicketRetriever
  participant Repo as TicketRepository
  participant U as TicketUpdater
  participant E as TicketEntity

  Client->>C: POST /api/tickets/confirm\n{ticketCode, checkCode}
  C->>S: confirmTicket(ticketCode, checkCode)
  S->>R: findTicketEntityByTicketCode(ticketCode)
  R->>Repo: findByTicketCode(ticketCode)
  Repo-->>R: Optional<TicketEntity>
  R-->>S: TicketEntity | throw TicketNotFoundException
  S->>S: verifyTicketStatus/verifyTicketDate\nverifyTicketCheckCode
  alt 검증 성공
    S->>E: updateTicketStatus(USED) (set usedTime)
    S->>U: persist status 변경
    U-->>S: OK
    S-->>C: 성공 응답
    C-->>Client: 200 OK
  else 체크코드 불일치
    S-->>C: throw BAD_REQUEST_TICKET_CHECK_CODE_ERROR
    C-->>Client: 400
  else 이미 사용/취소됨
    S-->>C: throw CONFLICT_ALREADY_USED_TICKET / BAD_REQUEST_CANCELED_TICKET
    C-->>Client: 409 / 400
  else 날짜/유효성 오류
    S-->>C: throw DateTicketException
    C-->>Client: 400
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning PR에 EventTicketInfoResponse 및 UserBuyTicketInfoResponse 패키지 변경 및 이름 수정과 getUserBuyTicketInfo 반환 타입 변경 같은 기존 API 리팩토링이 포함되어 있어 도어 티켓 검증 API 구현 요구사항과 직접 관련이 없는 변경이 포함되었습니다. 리팩토링된 DTO 이동 및 API 서명 변경 부분은 별도 PR로 분리하거나 구현 범위를 도어 티켓 검증 기능으로만 제한해 주세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 23.08% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed 제목이 주요 변경 사항인 도어용 유저 티켓 검증 API 구현을 명확하게 요약하고 있어 한눈에 핵심을 파악할 수 있습니다.
Linked Issues Check ✅ Passed 이 PR은 TicketController의 confirmTicket 엔드포인트, DTO, 서비스 로직, 예외 매핑 및 에러코드 추가를 통해 이슈 #135에서 요구한 도어용 유저 티켓 검증 API 구현 목표를 충족합니다.
Description Check ✅ Passed 제공된 설명은 도어용 유저 티켓 검증 API 구현이라는 변경 사항과 직접적으로 연관되어 있으며 관련 없는 내용을 포함하지 않아 적절합니다.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#135

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

🧹 Nitpick comments (2)
src/main/java/com/permitseoul/permitserver/domain/ticket/api/controller/TicketController.java (1)

43-43: 메서드 이름을 역할에 맞게 정리하면 가독성이 올라갑니다

POST /api/tickets/confirm 엔드포인트가 GET용 getUserBuyTicketInfo와 동일한 이름을 쓰고 있어 읽는 사람이 쉽게 혼동합니다. confirmTicket 등 동작을 설명하는 이름으로 바꿔두면 유지보수 시 의도가 훨씬 분명해집니다.

src/main/java/com/permitseoul/permitserver/domain/tickettype/core/component/TicketTypeRetriever.java (1)

24-27: 엔티티 재사용으로 조회 중복 제거 권장
이미 존재하는 findTicketTypeEntityById를 활용하면 중복 쿼리를 줄이고 유지보수성이 향상됩니다.

다음과 같이 내부 메서드를 재사용하면 깔끔합니다:

     @Transactional(readOnly = true)
     public TicketType findTicketTypeById(final long ticketTypeId) {
-        return TicketType.fromEntity(ticketTypeRepository.findById(ticketTypeId).orElseThrow(TicketTypeNotfoundException::new));
+        return TicketType.fromEntity(findTicketTypeEntityById(ticketTypeId));
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c5fdbb5 and 1f496da.

📒 Files selected for processing (13)
  • src/main/java/com/permitseoul/permitserver/domain/ticket/api/controller/TicketController.java (3 hunks)
  • src/main/java/com/permitseoul/permitserver/domain/ticket/api/dto/req/TicketConfirmRequest.java (1 hunks)
  • src/main/java/com/permitseoul/permitserver/domain/ticket/api/dto/res/EventTicketInfoResponse.java (1 hunks)
  • src/main/java/com/permitseoul/permitserver/domain/ticket/api/dto/res/UserBuyTicketInfoResponse.java (1 hunks)
  • src/main/java/com/permitseoul/permitserver/domain/ticket/api/exception/ConflictTicketException.java (1 hunks)
  • src/main/java/com/permitseoul/permitserver/domain/ticket/api/exception/DateTicketException.java (1 hunks)
  • src/main/java/com/permitseoul/permitserver/domain/ticket/api/exception/IllegalTicketException.java (1 hunks)
  • src/main/java/com/permitseoul/permitserver/domain/ticket/api/service/TicketService.java (6 hunks)
  • src/main/java/com/permitseoul/permitserver/domain/ticket/core/component/TicketRetriever.java (1 hunks)
  • src/main/java/com/permitseoul/permitserver/domain/ticket/core/domain/entity/TicketEntity.java (1 hunks)
  • src/main/java/com/permitseoul/permitserver/domain/ticket/core/repository/TicketRepository.java (1 hunks)
  • src/main/java/com/permitseoul/permitserver/domain/tickettype/core/component/TicketTypeRetriever.java (1 hunks)
  • src/main/java/com/permitseoul/permitserver/global/response/code/ErrorCode.java (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/main/java/com/permitseoul/permitserver/domain/tickettype/core/component/TicketTypeRetriever.java (1)
src/main/java/com/permitseoul/permitserver/domain/tickettype/core/exception/TicketTypeNotfoundException.java (1)
  • TicketTypeNotfoundException (3-4)
src/main/java/com/permitseoul/permitserver/domain/ticket/core/component/TicketRetriever.java (1)
src/main/java/com/permitseoul/permitserver/domain/ticket/core/exception/TicketNotFoundException.java (1)
  • TicketNotFoundException (3-4)
src/main/java/com/permitseoul/permitserver/domain/ticket/api/controller/TicketController.java (1)
src/main/java/com/permitseoul/permitserver/global/response/ApiResponseUtil.java (1)
  • ApiResponseUtil (7-29)
src/main/java/com/permitseoul/permitserver/domain/ticket/api/exception/ConflictTicketException.java (1)
src/main/java/com/permitseoul/permitserver/domain/reservation/api/exception/ConflictReservationException.java (1)
  • ConflictReservationException (5-9)
src/main/java/com/permitseoul/permitserver/domain/ticket/api/service/TicketService.java (7)
src/main/java/com/permitseoul/permitserver/domain/event/core/exception/EventNotfoundException.java (1)
  • EventNotfoundException (3-4)
src/main/java/com/permitseoul/permitserver/domain/ticket/api/exception/ConflictTicketException.java (1)
  • ConflictTicketException (5-9)
src/main/java/com/permitseoul/permitserver/domain/ticket/api/exception/DateTicketException.java (1)
  • DateTicketException (5-9)
src/main/java/com/permitseoul/permitserver/domain/ticket/api/exception/IllegalTicketException.java (1)
  • IllegalTicketException (5-9)
src/main/java/com/permitseoul/permitserver/domain/ticket/api/exception/NotFoundTicketException.java (1)
  • NotFoundTicketException (5-9)
src/main/java/com/permitseoul/permitserver/domain/ticket/core/exception/TicketNotFoundException.java (1)
  • TicketNotFoundException (3-4)
src/main/java/com/permitseoul/permitserver/domain/ticket/api/TicketExceptionHandler.java (1)
  • Order (12-19)
🔇 Additional comments (10)
src/main/java/com/permitseoul/permitserver/domain/ticket/core/repository/TicketRepository.java (1)

16-17: 새 티켓 코드 조회 메서드 확인 완료
티켓 코드 기반 확인 흐름에 꼭 필요한 리포지토리 쿼리가 추가되어 일관성이 좋습니다.

src/main/java/com/permitseoul/permitserver/domain/ticket/api/dto/res/EventTicketInfoResponse.java (1)

1-1: 패키지 재배치 적절
응답 DTO를 .res 패키지로 모으는 방향과 잘 맞습니다.

src/main/java/com/permitseoul/permitserver/domain/ticket/core/component/TicketRetriever.java (1)

19-22: 티켓 코드 검색 로직 적합
티켓 코드가 존재하지 않을 때 즉시 예외를 던져 API 오류 시그널과 일관됩니다.

src/main/java/com/permitseoul/permitserver/domain/ticket/api/dto/req/TicketConfirmRequest.java (1)

5-11: 요청 DTO 구성 적절
필수 필드에 @NotBlank를 적용해 유효성 검증 흐름을 확보한 점이 좋습니다.

src/main/java/com/permitseoul/permitserver/domain/ticket/api/exception/DateTicketException.java (1)

5-9: 날짜 검증 예외 정의 확인
도어 검증 시 날짜 오류를 명확히 전파할 수 있도록 예외가 잘 분리되었습니다.

src/main/java/com/permitseoul/permitserver/domain/ticket/api/exception/ConflictTicketException.java (1)

5-9: 상태 충돌 예외 정의 확인
이미 사용된 티켓 충돌 시그널을 위한 예외 계층이 깔끔하게 추가되었습니다.

src/main/java/com/permitseoul/permitserver/domain/ticket/api/exception/IllegalTicketException.java (1)

5-8: 예외 계층 구조 일관성 유지

TicketApiException을 그대로 상속해 ErrorCode만 위임하도록 한 구조가 기존 패턴과 잘 맞습니다. 이후 예외 매핑 시 추가 조정 없이 재사용 가능해 보여요.

src/main/java/com/permitseoul/permitserver/domain/ticket/api/dto/res/UserBuyTicketInfoResponse.java (1)

6-29: 응답 DTO 패키지/네이밍 정리 확인

응답용 DTO를 res 패키지로 이동하고 명확히 Response로 명명해 의도가 분명해졌습니다. 중첩 레코드/enum 구조도 이전과 동일하게 유지되어 하위 호환성 문제가 없어 보입니다.

src/main/java/com/permitseoul/permitserver/global/response/code/ErrorCode.java (2)

28-29: 도어 검증용 400 에러 코드 추가 적절

체크 코드 검증 실패를 별도 ErrorCode로 분리해 컨트롤러에서 명확히 매핑할 수 있게 된 점 좋습니다. 기존 400번대와 충돌도 없네요.


102-103: 이미 사용된 티켓 대응 409 코드 확인

이미 사용/취소된 티켓 시나리오를 40906으로 분리해 API 응답을 세분화한 구성 문제없습니다. 메시지도 문맥에 맞습니다.

Comment on lines 112 to 126
@Transactional
public void confirmTicket(final String ticketCode, final String checkCodeFromTicket) {
try {
final TicketEntity ticketEntity = ticketRetriever.findTicketEntityByTicketCode(ticketCode);
verifyTicketStatus(ticketEntity.getStatus());

final TicketType ticketType = ticketTypeRetriever.findTicketTypeById(ticketEntity.getTicketTypeId());
verifyTicketDate(ticketType.getTicketStartAt(), ticketType.getTicketEndAt());

final Event event = eventRetriever.findEventById(ticketEntity.getEventId());
verifyTicketCheckCode(event.getTicketCheckCode(), checkCodeFromTicket);


ticketUpdater.updateTicketStatus(ticketEntity, TicketStatus.USED);
} catch (TicketNotFoundException e) {
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

문 상태 검증 API에 치명적인 경쟁 조건 존재

confirmTicket에서 티켓을 조회해 상태를 검사한 뒤, 별도의 잠금 없이 ticketUpdater.updateTicketStatus로 상태를 USED로 변경하고 있습니다. 서로 다른 트랜잭션에서 동시에 동일 티켓을 스캔하면 두 요청 모두 조회 시점에 RESERVED 상태를 보게 되고, 동시에 verifyTicketStatus를 통과하여 모두 성공해 버립니다. 결국 같은 티켓이 여러 번 사용 완료 처리되는 TOCTOU 문제가 생기며, 이는 티켓 중복 사용을 방지하려는 이 API의 핵심 목적을 무너뜨립니다.

티켓 엔터티를 비관적 락으로 조회하거나, update 단계에서 현재 상태를 조건으로 삼아 한 번만 성공하도록 방어 로직을 추가하는 등 동시성 제어가 필요합니다.

🤖 Prompt for AI Agents
In
src/main/java/com/permitseoul/permitserver/domain/ticket/api/service/TicketService.java
around lines 112 to 126, the confirmTicket method has a TOCTOU race: it reads
the ticket status then updates without concurrency control. Fix by ensuring
atomic status transition — either retrieve the TicketEntity with a pessimistic
lock inside the @Transactional method (e.g., use repository/findById with
PESSIMISTIC_WRITE) before verifying status, or make the update operation
conditional (execute an update that sets status=USED only where id=? and
status=RESERVED and check the affected-row count, throwing an exception if 0).
Ensure the method remains transactional and remove the separate non-atomic
read+write sequence so only one concurrent caller can successfully mark the
ticket USED.

Comment on lines 60 to 63
public void updateTicketStatus(final TicketStatus status) {
this.status = status;
this.usedTime = 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

상태 업데이트 시 usedTime이 항상 갱신되고 있습니다
updateTicketStatus는 USED 외의 다른 상태 전환(예: RESERVED, CANCELLED)에도 사용되는데, 현재 구현은 어떤 상태로 바꾸더라도 usedTime을 지금 시각으로 덮어씁니다. 그 결과 실제로 사용되지 않은 티켓도 사용된 것처럼 기록되어 도어 검증/통계 로직에 치명적인 오동작이 발생합니다. 반드시 USED일 때만 사용 시각을 남기고, 그 외 상태에서는 usedTime을 초기화하거나 유지하도록 분기해 주세요.

     public void updateTicketStatus(final TicketStatus status) {
         this.status = status;
-        this.usedTime = LocalDateTime.now();
+        if (status == TicketStatus.USED) {
+            this.usedTime = LocalDateTime.now();
+        } else {
+            this.usedTime = null;
+        }
     }
🤖 Prompt for AI Agents
In
src/main/java/com/permitseoul/permitserver/domain/ticket/core/domain/entity/TicketEntity.java
around lines 60-63, change updateTicketStatus so that usedTime is set to
LocalDateTime.now() only when the new status equals TicketStatus.USED, and for
any other status clear usedTime (set to null) or leave it unchanged per domain
rule; implement an if/else branch to perform this conditional assignment so
non-used state transitions do not record a usage timestamp.

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

🧹 Nitpick comments (1)
src/main/java/com/permitseoul/permitserver/domain/ticket/api/controller/TicketController.java (1)

43-48: 엔드포인트 목적에 맞는 메서드명으로 정리했으면 합니다

Line 44에서 /confirm 검증용 POST 엔드포인트가 기존 조회용 메서드와 동일한 getUserBuyTicketInfo 이름을 재사용하고 있어 가독성이 떨어집니다. 후속 유지보수를 위해 confirmTicket 등 역할을 드러내는 메서드명으로 분리하는 편이 좋겠습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f496da and f311acb.

📒 Files selected for processing (4)
  • src/main/java/com/permitseoul/permitserver/domain/ticket/api/controller/TicketController.java (3 hunks)
  • src/main/java/com/permitseoul/permitserver/domain/ticket/api/service/TicketService.java (6 hunks)
  • src/main/java/com/permitseoul/permitserver/domain/ticket/core/domain/entity/TicketEntity.java (1 hunks)
  • src/main/java/com/permitseoul/permitserver/global/response/code/ErrorCode.java (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/com/permitseoul/permitserver/domain/ticket/core/domain/entity/TicketEntity.java
  • src/main/java/com/permitseoul/permitserver/global/response/code/ErrorCode.java
🧰 Additional context used
🧬 Code graph analysis (2)
src/main/java/com/permitseoul/permitserver/domain/ticket/api/service/TicketService.java (7)
src/main/java/com/permitseoul/permitserver/domain/event/core/exception/EventNotfoundException.java (1)
  • EventNotfoundException (3-4)
src/main/java/com/permitseoul/permitserver/domain/ticket/api/exception/ConflictTicketException.java (1)
  • ConflictTicketException (5-9)
src/main/java/com/permitseoul/permitserver/domain/ticket/api/exception/DateTicketException.java (1)
  • DateTicketException (5-9)
src/main/java/com/permitseoul/permitserver/domain/ticket/api/exception/IllegalTicketException.java (1)
  • IllegalTicketException (5-9)
src/main/java/com/permitseoul/permitserver/domain/ticket/api/exception/NotFoundTicketException.java (1)
  • NotFoundTicketException (5-9)
src/main/java/com/permitseoul/permitserver/domain/ticket/core/exception/TicketNotFoundException.java (1)
  • TicketNotFoundException (3-4)
src/main/java/com/permitseoul/permitserver/domain/ticket/api/TicketExceptionHandler.java (1)
  • Order (12-19)
src/main/java/com/permitseoul/permitserver/domain/ticket/api/controller/TicketController.java (1)
src/main/java/com/permitseoul/permitserver/global/response/ApiResponseUtil.java (1)
  • ApiResponseUtil (7-29)
🔇 Additional comments (1)
src/main/java/com/permitseoul/permitserver/domain/ticket/api/service/TicketService.java (1)

112-125: 동시 요청 시 같은 티켓이 여러 번 사용 처리됩니다

Line 124 기준, findTicketEntityByTicketCode로 상태를 읽은 뒤 별도 동기화 없이 ticketUpdater.updateTicketStatus를 호출하고 있어 두 트랜잭션이 동시에 RESERVED 상태를 보고 모두 USED로 전이시키는 TOCTOU 경쟁 조건이 남아 있습니다. 이는 도어 검증 API의 핵심 목적인 중복 사용 방지를 무력화하므로, 조회 단계에서 비관적 락을 걸거나 업데이트를 WHERE status = :expected 조건과 함께 실행해 한 번만 성공하도록 보완해 주세요.

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.

feat: 도어용 유저 티켓 검증 API 구현

2 participants