-
Notifications
You must be signed in to change notification settings - Fork 1
Feature/ticket #10
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
Merged
Merged
Feature/ticket #10
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
f906640
:sparkles: Feat: 티켓 관련 API
naooung 756c2c9
:recycle: Refactor: swagger 설명 일부 수정
naooung c0438f5
:recycle: Refactor: 동시 티켓 소비에 따른 동시성 이슈 해결 + 메서드 정리
naooung df7943c
:recycle: Refactor: spotless 적용
naooung 943406e
:recycle: Refactor: javadoc 수정
naooung File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
46 changes: 46 additions & 0 deletions
46
src/main/java/com/sku/refit/domain/ticket/controller/TicketController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| /* | ||
| * Copyright (c) SKU 다시입을Lab | ||
| */ | ||
| package com.sku.refit.domain.ticket.controller; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| import jakarta.validation.Valid; | ||
|
|
||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.*; | ||
|
|
||
| import com.sku.refit.domain.ticket.dto.request.TicketRequest.*; | ||
| import com.sku.refit.domain.ticket.dto.response.TicketResponse.*; | ||
| import com.sku.refit.global.response.BaseResponse; | ||
|
|
||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
|
|
||
| @Tag(name = "티켓", description = "티켓 관련 API") | ||
| @RequestMapping("/api/tickets") | ||
| public interface TicketController { | ||
|
|
||
| @PostMapping("/dev/issue") | ||
| @Operation(summary = "[개발자] 티켓 발급", description = "EVENT/CLOTH 티켓을 발급하고 token을 반환합니다.") | ||
| ResponseEntity<BaseResponse<TicketDetailResponse>> issueTicket( | ||
| @RequestBody @Valid IssueTicketRequest request); | ||
|
|
||
| @PostMapping("/admin/verify") | ||
| @Operation(summary = "[관리자] 티켓 검증", description = "token으로 티켓 유효/사용 여부를 확인합니다. (사용 처리 X)") | ||
| ResponseEntity<BaseResponse<VerifyTicketResponse>> verifyTicket( | ||
| @RequestBody @Valid VerifyTicketRequest request); | ||
|
|
||
| @PostMapping("/admin/consume") | ||
| @Operation(summary = "[관리자] 티켓 사용 처리", description = "token으로 티켓을 사용 처리합니다. (멱등 사용 처리)") | ||
| ResponseEntity<BaseResponse<ConsumeTicketResponse>> consumeTicket( | ||
| @RequestBody @Valid ConsumeTicketRequest request); | ||
|
|
||
naooung marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| @GetMapping("/my/events") | ||
| @Operation(summary = "참가한 행사 조회", description = "사용자가 사용한 EVENT 티켓만 조회합니다.") | ||
| ResponseEntity<BaseResponse<List<MyTicketItemResponse>>> getMyUsedEventTickets(); | ||
|
|
||
| @GetMapping("/my/cloth") | ||
| @Operation(summary = "교환 내역 조회", description = "사용자가 받았던 CLOTH 티켓을 모두 조회합니다.") | ||
| ResponseEntity<BaseResponse<List<MyTicketItemResponse>>> getMyClothTickets(); | ||
| } | ||
57 changes: 57 additions & 0 deletions
57
src/main/java/com/sku/refit/domain/ticket/controller/TicketControllerImpl.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| /* | ||
| * Copyright (c) SKU 다시입을Lab | ||
| */ | ||
| package com.sku.refit.domain.ticket.controller; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| import jakarta.validation.Valid; | ||
|
|
||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.RequestBody; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| import com.sku.refit.domain.ticket.dto.request.TicketRequest.*; | ||
| import com.sku.refit.domain.ticket.dto.response.TicketResponse.*; | ||
| import com.sku.refit.domain.ticket.service.TicketService; | ||
| import com.sku.refit.global.response.BaseResponse; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @RestController | ||
| @RequiredArgsConstructor | ||
| public class TicketControllerImpl implements TicketController { | ||
|
|
||
| private final TicketService ticketService; | ||
|
|
||
| @Override | ||
| public ResponseEntity<BaseResponse<TicketDetailResponse>> issueTicket( | ||
| @RequestBody @Valid IssueTicketRequest request) { | ||
| return ResponseEntity.ok( | ||
| BaseResponse.success( | ||
| ticketService.issueTicket( | ||
| request.getType(), request.getTargetId(), request.getUserId()))); | ||
| } | ||
naooung marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| @Override | ||
| public ResponseEntity<BaseResponse<VerifyTicketResponse>> verifyTicket( | ||
| @RequestBody @Valid VerifyTicketRequest request) { | ||
| return ResponseEntity.ok(BaseResponse.success(ticketService.verifyTicket(request))); | ||
| } | ||
|
|
||
| @Override | ||
| public ResponseEntity<BaseResponse<ConsumeTicketResponse>> consumeTicket( | ||
| @RequestBody @Valid ConsumeTicketRequest request) { | ||
| return ResponseEntity.ok(BaseResponse.success(ticketService.consumeTicket(request))); | ||
| } | ||
|
|
||
| @Override | ||
| public ResponseEntity<BaseResponse<List<MyTicketItemResponse>>> getMyUsedEventTickets() { | ||
| return ResponseEntity.ok(BaseResponse.success(ticketService.getMyTicketsUsedEvents())); | ||
| } | ||
|
|
||
| @Override | ||
| public ResponseEntity<BaseResponse<List<MyTicketItemResponse>>> getMyClothTickets() { | ||
| return ResponseEntity.ok(BaseResponse.success(ticketService.getMyClothTickets())); | ||
| } | ||
| } | ||
38 changes: 38 additions & 0 deletions
38
src/main/java/com/sku/refit/domain/ticket/dto/request/TicketRequest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| /* | ||
| * Copyright (c) SKU 다시입을Lab | ||
| */ | ||
| package com.sku.refit.domain.ticket.dto.request; | ||
|
|
||
| import jakarta.validation.constraints.NotBlank; | ||
| import jakarta.validation.constraints.NotNull; | ||
|
|
||
| import com.sku.refit.domain.ticket.entity.TicketType; | ||
|
|
||
| import lombok.*; | ||
|
|
||
| public class TicketRequest { | ||
|
|
||
| @Getter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public static class IssueTicketRequest { | ||
| @NotNull private TicketType type; | ||
| @NotNull private Long targetId; | ||
|
|
||
| private Long userId; | ||
| } | ||
|
|
||
| @Getter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public static class ConsumeTicketRequest { | ||
| @NotBlank private String token; | ||
| } | ||
|
|
||
| @Getter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public static class VerifyTicketRequest { | ||
| @NotBlank private String token; | ||
| } | ||
| } |
72 changes: 72 additions & 0 deletions
72
src/main/java/com/sku/refit/domain/ticket/dto/response/TicketResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| /* | ||
| * Copyright (c) SKU 다시입을Lab | ||
| */ | ||
| package com.sku.refit.domain.ticket.dto.response; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| import com.sku.refit.domain.ticket.entity.TicketType; | ||
|
|
||
| import lombok.*; | ||
|
|
||
| public class TicketResponse { | ||
|
|
||
| @Getter | ||
| @Builder | ||
| public static class TicketDetailResponse { | ||
| private Long ticketId; | ||
| private TicketType type; | ||
| private Long targetId; | ||
|
|
||
| private String token; | ||
| private String qrPayload; | ||
|
|
||
| private LocalDateTime issuedAt; | ||
| private LocalDateTime usedAt; | ||
| } | ||
|
|
||
| @Getter | ||
| @Builder | ||
| public static class VerifyTicketResponse { | ||
| private boolean valid; | ||
| private boolean used; | ||
|
|
||
| private Long ticketId; | ||
| private TicketType type; | ||
| private Long targetId; | ||
|
|
||
| private String qrPayload; | ||
|
|
||
| private LocalDateTime issuedAt; | ||
| private LocalDateTime usedAt; | ||
| } | ||
|
|
||
| @Getter | ||
| @Builder | ||
| public static class ConsumeTicketResponse { | ||
| private boolean consumed; | ||
|
|
||
| private Long ticketId; | ||
| private TicketType type; | ||
| private Long targetId; | ||
|
|
||
| private String qrPayload; | ||
|
|
||
| private LocalDateTime usedAt; | ||
| } | ||
|
|
||
| /** 사용자 조회용: - EVENT: "사용한 행사"만 조회(usedAt != null) - CLOTH: "받은 티켓" 전체 조회(usedAt 상관 없음) */ | ||
| @Getter | ||
| @Builder | ||
| public static class MyTicketItemResponse { | ||
| private Long ticketId; | ||
| private TicketType type; | ||
| private Long targetId; | ||
|
|
||
| private String qrPayload; | ||
| private String token; | ||
|
|
||
| private LocalDateTime issuedAt; | ||
| private LocalDateTime usedAt; | ||
| } | ||
| } |
87 changes: 87 additions & 0 deletions
87
src/main/java/com/sku/refit/domain/ticket/entity/Ticket.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| /* | ||
| * Copyright (c) SKU 다시입을Lab | ||
| */ | ||
| package com.sku.refit.domain.ticket.entity; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| import jakarta.persistence.*; | ||
|
|
||
| import com.sku.refit.global.common.BaseTimeEntity; | ||
|
|
||
| import lombok.*; | ||
|
|
||
| @Entity | ||
| @Getter | ||
| @Builder | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @AllArgsConstructor | ||
| @Table( | ||
| name = "ticket", | ||
| indexes = { | ||
| @Index(name = "idx_ticket_user", columnList = "user_id"), | ||
| @Index(name = "idx_ticket_type_target", columnList = "type,target_id"), | ||
| @Index(name = "idx_ticket_token", columnList = "token"), | ||
| @Index(name = "idx_ticket_used_at", columnList = "used_at") | ||
| }) | ||
| public class Ticket extends BaseTimeEntity { | ||
|
|
||
| /** PK (외부 노출 금지) */ | ||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| /** 티켓 종류 (EVENT / CLOTH) */ | ||
| @Enumerated(EnumType.STRING) | ||
| @Column(nullable = false, length = 20) | ||
| private TicketType type; | ||
|
|
||
| /** 티켓 대상 ID - EVENT → eventId - CLOTH → clothId */ | ||
| @Column(name = "target_id", nullable = false) | ||
| private Long targetId; | ||
|
|
||
| /** 티켓 소유 사용자 */ | ||
| @Column(name = "user_id", nullable = false) | ||
| private Long userId; | ||
|
|
||
| /** QR에 들어가는 검증용 토큰 - 외부 노출 OK - 의미 없는 랜덤 값 - unique 필수 */ | ||
| @Column(nullable = false, unique = true, length = 128) | ||
| private String token; | ||
|
|
||
| /** 사용 시각 (null = 미사용) */ | ||
| @Column(name = "used_at") | ||
| private LocalDateTime usedAt; | ||
|
|
||
| /* ========================= | ||
| * Domain Logic | ||
| * ========================= */ | ||
|
|
||
| /** 사용 여부 */ | ||
| public boolean isUsed() { | ||
| return usedAt != null; | ||
| } | ||
|
|
||
| /** 티켓 사용 처리 (멱등) */ | ||
| public void consume(LocalDateTime now) { | ||
| if (this.usedAt != null) { | ||
| return; | ||
| } | ||
| this.usedAt = now; | ||
| } | ||
|
|
||
| /** 기본 도메인 유효성 */ | ||
| public void validate() { | ||
| if (type == null) { | ||
| throw new IllegalStateException("Ticket type은 필수입니다."); | ||
| } | ||
| if (targetId == null) { | ||
| throw new IllegalStateException("Ticket targetId는 필수입니다."); | ||
| } | ||
| if (userId == null) { | ||
| throw new IllegalStateException("Ticket userId는 필수입니다."); | ||
| } | ||
| if (token == null || token.isBlank()) { | ||
| throw new IllegalStateException("Ticket token은 필수입니다."); | ||
| } | ||
| } | ||
| } |
14 changes: 14 additions & 0 deletions
14
src/main/java/com/sku/refit/domain/ticket/entity/TicketType.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| /* | ||
| * Copyright (c) SKU 다시입을Lab | ||
| */ | ||
| package com.sku.refit.domain.ticket.entity; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
|
|
||
| public enum TicketType { | ||
| @Schema(description = "행사 체크인") | ||
| EVENT, | ||
|
|
||
| @Schema(description = "의류 관련 티켓") | ||
| CLOTH | ||
| } |
32 changes: 32 additions & 0 deletions
32
src/main/java/com/sku/refit/domain/ticket/exception/TicketErrorCode.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| /* | ||
| * Copyright (c) SKU 다시입을Lab | ||
| */ | ||
| package com.sku.refit.domain.ticket.exception; | ||
|
|
||
| import org.springframework.http.HttpStatus; | ||
|
|
||
| import com.sku.refit.global.exception.model.BaseErrorCode; | ||
|
|
||
| import lombok.Getter; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Getter | ||
| @RequiredArgsConstructor | ||
| public enum TicketErrorCode implements BaseErrorCode { | ||
| TICKET_BAD_REQUEST("TICKET001", "요청 값이 올바르지 않습니다.", HttpStatus.BAD_REQUEST), | ||
| TICKET_TOKEN_REQUIRED("TICKET002", "티켓 토큰이 필요합니다.", HttpStatus.BAD_REQUEST), | ||
|
|
||
| TICKET_NOT_FOUND("TICKET0010", "티켓을 찾을 수 없습니다.", HttpStatus.NOT_FOUND), | ||
| TICKET_ALREADY_USED("TICKET020", "이미 사용된 티켓입니다.", HttpStatus.CONFLICT), | ||
|
|
||
| TICKET_TOKEN_GENERATION_FAILED( | ||
| "TICKET030", "티켓 토큰 생성에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), | ||
| TICKET_ISSUE_FAILED("TICKET031", "티켓 발급에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), | ||
| TICKET_VERIFY_FAILED("TICKET032", "티켓 검증에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), | ||
| TICKET_CONSUME_FAILED("TICKET033", "티켓 사용 처리에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), | ||
| TICKET_MY_LIST_FAILED("TICKET034", "내 티켓 목록 조회에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR); | ||
|
|
||
| private final String code; | ||
| private final String message; | ||
| private final HttpStatus status; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.