From 008e722887338be5d1cac9ab6933c46724d8dde8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=82=98=EA=B2=BD?= <1030n@naver.com> Date: Fri, 19 Dec 2025 09:13:09 +0900 Subject: [PATCH 1/3] =?UTF-8?q?:sparkles:=20Feat:=20=ED=8B=B0=EC=BC=93=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=C2=B7=EA=B2=80=EC=A6=9D=C2=B7=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ticket/controller/TicketController.java | 35 +++++++ .../controller/TicketControllerImpl.java | 38 ++++++++ .../ticket/dto/request/TicketRequest.java | 29 ++++++ .../dto/response/CompleteMissionResponse.java | 17 ++++ .../ticket/dto/response/TicketResponse.java | 45 +++++++++ .../insaroad/domain/ticket/entity/Ticket.java | 87 +++++++++++++++++ .../ticket/exception/TicketErrorCode.java | 33 +++++++ .../domain/ticket/mapper/TicketMapper.java | 64 +++++++++++++ .../ticket/repository/TicketRepository.java | 27 ++++++ .../domain/ticket/service/TicketService.java | 40 ++++++++ .../ticket/service/TicketServiceImpl.java | 96 +++++++++++++++++++ .../ticket/util/TicketQrPayloadFactory.java | 17 ++++ .../ticket/util/TicketTokenGenerator.java | 21 ++++ 13 files changed, 549 insertions(+) create mode 100644 src/main/java/com/pbl/insaroad/domain/ticket/controller/TicketController.java create mode 100644 src/main/java/com/pbl/insaroad/domain/ticket/controller/TicketControllerImpl.java create mode 100644 src/main/java/com/pbl/insaroad/domain/ticket/dto/request/TicketRequest.java create mode 100644 src/main/java/com/pbl/insaroad/domain/ticket/dto/response/CompleteMissionResponse.java create mode 100644 src/main/java/com/pbl/insaroad/domain/ticket/dto/response/TicketResponse.java create mode 100644 src/main/java/com/pbl/insaroad/domain/ticket/entity/Ticket.java create mode 100644 src/main/java/com/pbl/insaroad/domain/ticket/exception/TicketErrorCode.java create mode 100644 src/main/java/com/pbl/insaroad/domain/ticket/mapper/TicketMapper.java create mode 100644 src/main/java/com/pbl/insaroad/domain/ticket/repository/TicketRepository.java create mode 100644 src/main/java/com/pbl/insaroad/domain/ticket/service/TicketService.java create mode 100644 src/main/java/com/pbl/insaroad/domain/ticket/service/TicketServiceImpl.java create mode 100644 src/main/java/com/pbl/insaroad/domain/ticket/util/TicketQrPayloadFactory.java create mode 100644 src/main/java/com/pbl/insaroad/domain/ticket/util/TicketTokenGenerator.java diff --git a/src/main/java/com/pbl/insaroad/domain/ticket/controller/TicketController.java b/src/main/java/com/pbl/insaroad/domain/ticket/controller/TicketController.java new file mode 100644 index 0000000..bea3a81 --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/ticket/controller/TicketController.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.ticket.controller; + +import jakarta.validation.Valid; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +import com.pbl.insaroad.domain.ticket.dto.request.TicketRequest.ConsumeTicketRequest; +import com.pbl.insaroad.domain.ticket.dto.request.TicketRequest.VerifyTicketRequest; +import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.ConsumeTicketResponse; +import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.VerifyTicketResponse; +import com.pbl.insaroad.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("/admin/verify") + @Operation(summary = "[관리자] 교환권 검증", description = "token으로 교환권 유효/사용 여부를 확인합니다. (사용 처리 X)") + ResponseEntity> verifyTicket( + @RequestBody @Valid VerifyTicketRequest request); + + @PostMapping("/admin/consume") + @Operation(summary = "[관리자] 교환권 사용 처리", description = "token으로 교환권을 사용 처리합니다. (멱등 보장)") + ResponseEntity> consumeTicket( + @RequestBody @Valid ConsumeTicketRequest request); +} diff --git a/src/main/java/com/pbl/insaroad/domain/ticket/controller/TicketControllerImpl.java b/src/main/java/com/pbl/insaroad/domain/ticket/controller/TicketControllerImpl.java new file mode 100644 index 0000000..c8b6c89 --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/ticket/controller/TicketControllerImpl.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.ticket.controller; + +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.pbl.insaroad.domain.ticket.dto.request.TicketRequest.ConsumeTicketRequest; +import com.pbl.insaroad.domain.ticket.dto.request.TicketRequest.VerifyTicketRequest; +import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.ConsumeTicketResponse; +import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.VerifyTicketResponse; +import com.pbl.insaroad.domain.ticket.service.TicketService; +import com.pbl.insaroad.global.response.BaseResponse; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +public class TicketControllerImpl implements TicketController { + + private final TicketService ticketService; + + @Override + public ResponseEntity> verifyTicket( + @RequestBody @Valid VerifyTicketRequest request) { + return ResponseEntity.ok(BaseResponse.success(ticketService.verifyTicket(request))); + } + + @Override + public ResponseEntity> consumeTicket( + @RequestBody @Valid ConsumeTicketRequest request) { + return ResponseEntity.ok(BaseResponse.success(ticketService.consumeTicket(request))); + } +} diff --git a/src/main/java/com/pbl/insaroad/domain/ticket/dto/request/TicketRequest.java b/src/main/java/com/pbl/insaroad/domain/ticket/dto/request/TicketRequest.java new file mode 100644 index 0000000..98a7eb1 --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/ticket/dto/request/TicketRequest.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.ticket.dto.request; + +import jakarta.validation.constraints.NotBlank; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +public class TicketRequest { + + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class VerifyTicketRequest { + + @NotBlank private String token; + } + + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class ConsumeTicketRequest { + + @NotBlank private String token; + } +} diff --git a/src/main/java/com/pbl/insaroad/domain/ticket/dto/response/CompleteMissionResponse.java b/src/main/java/com/pbl/insaroad/domain/ticket/dto/response/CompleteMissionResponse.java new file mode 100644 index 0000000..3175b1e --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/ticket/dto/response/CompleteMissionResponse.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.ticket.dto.response; + +import java.time.LocalDateTime; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class CompleteMissionResponse { + + private String exchangeUrl; + private LocalDateTime issuedAt; +} diff --git a/src/main/java/com/pbl/insaroad/domain/ticket/dto/response/TicketResponse.java b/src/main/java/com/pbl/insaroad/domain/ticket/dto/response/TicketResponse.java new file mode 100644 index 0000000..4c5d76c --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/ticket/dto/response/TicketResponse.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.ticket.dto.response; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import lombok.Builder; +import lombok.Getter; + +public class TicketResponse { + + @Getter + @Builder + public static class VerifyTicketResponse { + + private boolean valid; + private boolean used; + + private Long ticketId; + private Long targetId; + + private String qrPayload; + + private LocalDateTime issuedAt; + private LocalDateTime usedAt; + private LocalDate expiresAt; + } + + @Getter + @Builder + public static class ConsumeTicketResponse { + + private boolean consumed; + + private Long ticketId; + private Long targetId; + + private String qrPayload; + + private LocalDateTime usedAt; + private LocalDate expiresAt; + } +} diff --git a/src/main/java/com/pbl/insaroad/domain/ticket/entity/Ticket.java b/src/main/java/com/pbl/insaroad/domain/ticket/entity/Ticket.java new file mode 100644 index 0000000..d5d5ef9 --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/ticket/entity/Ticket.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.ticket.entity; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; + +import com.pbl.insaroad.global.common.BaseTimeEntity; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Table( + name = "ticket", + indexes = { + @Index(name = "idx_ticket_user", columnList = "user_id"), + @Index(name = "idx_ticket_token", columnList = "token"), + @Index(name = "idx_ticket_used_at", columnList = "used_at"), + @Index(name = "idx_ticket_expires_at", columnList = "expires_at") + }) +public class Ticket extends BaseTimeEntity { + + /** PK (외부 노출 금지) */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** 티켓 소유 사용자 */ + @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; + + /** 만료일 (null = 만료 X) */ + @Column(name = "expires_at") + private LocalDate expiresAt; + + /* ========================= + * Domain Logic + * ========================= */ + + /** 사용 여부 */ + public boolean isUsed() { + return usedAt != null; + } + + /** 만료 여부 (미사용 + expiresAt 존재 + 오늘 날짜 초과) */ + public boolean isExpired(LocalDate today) { + return usedAt == null && expiresAt != null && today.isAfter(expiresAt); + } + + public void consume(LocalDateTime now) { + if (this.usedAt != null) { + return; + } + + // 날짜 기준 만료 체크 + if (isExpired(now.toLocalDate())) { + throw new IllegalStateException("만료된 티켓은 사용할 수 없습니다."); + } + + this.usedAt = now; + } +} diff --git a/src/main/java/com/pbl/insaroad/domain/ticket/exception/TicketErrorCode.java b/src/main/java/com/pbl/insaroad/domain/ticket/exception/TicketErrorCode.java new file mode 100644 index 0000000..326cbca --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/ticket/exception/TicketErrorCode.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.ticket.exception; + +import org.springframework.http.HttpStatus; + +import com.pbl.insaroad.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_EXPIRED("TICKET021", "만료된 티켓입니다.", HttpStatus.GONE), + + 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; +} diff --git a/src/main/java/com/pbl/insaroad/domain/ticket/mapper/TicketMapper.java b/src/main/java/com/pbl/insaroad/domain/ticket/mapper/TicketMapper.java new file mode 100644 index 0000000..78012eb --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/ticket/mapper/TicketMapper.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.ticket.mapper; + +import java.time.LocalDate; + +import org.springframework.stereotype.Component; + +import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.ConsumeTicketResponse; +import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.VerifyTicketResponse; +import com.pbl.insaroad.domain.ticket.entity.Ticket; +import com.pbl.insaroad.domain.ticket.util.TicketQrPayloadFactory; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class TicketMapper { + + private final TicketQrPayloadFactory qrPayloadFactory; + + public Ticket toEntity(Long userId, String token, LocalDate expiresAt) { + return Ticket.builder().userId(userId).token(token).expiresAt(expiresAt).build(); + } + + public VerifyTicketResponse toVerifyFound(Ticket ticket) { + return VerifyTicketResponse.builder() + .valid(true) + .used(ticket.isUsed()) + .ticketId(ticket.getId()) + .qrPayload(qrPayloadFactory.create(ticket.getToken())) + .issuedAt(ticket.getCreatedAt()) + .usedAt(ticket.getUsedAt()) + .expiresAt(ticket.getExpiresAt()) + .build(); + } + + public VerifyTicketResponse toVerifyExpired(Ticket ticket) { + return VerifyTicketResponse.builder() + .valid(false) + .used(false) + .ticketId(ticket.getId()) + .qrPayload(qrPayloadFactory.create(ticket.getToken())) + .issuedAt(ticket.getCreatedAt()) + .usedAt(ticket.getUsedAt()) + .expiresAt(ticket.getExpiresAt()) + .build(); + } + + public VerifyTicketResponse toVerifyNotFound() { + return VerifyTicketResponse.builder().valid(false).used(false).build(); + } + + public ConsumeTicketResponse toConsume(Ticket ticket, boolean consumed) { + return ConsumeTicketResponse.builder() + .consumed(consumed) + .ticketId(ticket.getId()) + .qrPayload(qrPayloadFactory.create(ticket.getToken())) + .usedAt(ticket.getUsedAt()) + .expiresAt(ticket.getExpiresAt()) + .build(); + } +} diff --git a/src/main/java/com/pbl/insaroad/domain/ticket/repository/TicketRepository.java b/src/main/java/com/pbl/insaroad/domain/ticket/repository/TicketRepository.java new file mode 100644 index 0000000..4fa1b48 --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/ticket/repository/TicketRepository.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.ticket.repository; + +import java.util.Optional; + +import jakarta.persistence.LockModeType; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import com.pbl.insaroad.domain.ticket.entity.Ticket; + +public interface TicketRepository extends JpaRepository { + + Optional findByToken(String token); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("select t from Ticket t where t.token = :token") + Optional findByTokenForUpdate(@Param("token") String token); + + /** 사용자당 1장의 교환권만 허용 (멱등) */ + Optional findTop1ByUserIdOrderByCreatedAtDesc(Long userId); +} diff --git a/src/main/java/com/pbl/insaroad/domain/ticket/service/TicketService.java b/src/main/java/com/pbl/insaroad/domain/ticket/service/TicketService.java new file mode 100644 index 0000000..d95a028 --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/ticket/service/TicketService.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.ticket.service; + +import com.pbl.insaroad.domain.ticket.dto.request.TicketRequest.ConsumeTicketRequest; +import com.pbl.insaroad.domain.ticket.dto.request.TicketRequest.VerifyTicketRequest; +import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.ConsumeTicketResponse; +import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.VerifyTicketResponse; + +public interface TicketService { + + /** + * 티켓을 검증합니다. (사용 처리 없음) + * + *

주어진 {@code token} 에 해당하는 티켓이 존재하는지, 그리고 이미 사용된 티켓인지 여부를 확인합니다. + * + *

이 메서드는 티켓을 실제로 사용 처리하지 않으며, QR 스캔 시 사전 검증 용도로 사용됩니다. + * + * @param request 티켓 검증 요청 정보 (token) + * @return 티켓 유효성 및 사용 여부 응답 + */ + VerifyTicketResponse verifyTicket(VerifyTicketRequest request); + + /* ========================= + * Consume (Check-in) + * ========================= */ + + /** + * 티켓을 사용 처리합니다. (체크인) + * + *

해당 메서드는 멱등성을 보장하며, 이미 사용된 티켓에 대해 다시 호출되더라도 예외 없이 동일한 상태를 반환합니다. + * + *

일반적으로 관리자 또는 키오스크에서 QR 스캔 후 실제 입장/수거/처리 시점에 호출됩니다. + * + * @param request 티켓 사용 요청 정보 (token) + * @return 티켓 사용 처리 결과 응답 + */ + ConsumeTicketResponse consumeTicket(ConsumeTicketRequest request); +} diff --git a/src/main/java/com/pbl/insaroad/domain/ticket/service/TicketServiceImpl.java b/src/main/java/com/pbl/insaroad/domain/ticket/service/TicketServiceImpl.java new file mode 100644 index 0000000..6ebae8e --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/ticket/service/TicketServiceImpl.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.ticket.service; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.pbl.insaroad.domain.ticket.dto.request.TicketRequest.ConsumeTicketRequest; +import com.pbl.insaroad.domain.ticket.dto.request.TicketRequest.VerifyTicketRequest; +import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.ConsumeTicketResponse; +import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.VerifyTicketResponse; +import com.pbl.insaroad.domain.ticket.entity.Ticket; +import com.pbl.insaroad.domain.ticket.exception.TicketErrorCode; +import com.pbl.insaroad.domain.ticket.mapper.TicketMapper; +import com.pbl.insaroad.domain.ticket.repository.TicketRepository; +import com.pbl.insaroad.domain.ticket.util.TicketTokenGenerator; +import com.pbl.insaroad.domain.user.entity.User; +import com.pbl.insaroad.domain.user.repository.UserRepository; +import com.pbl.insaroad.global.exception.CustomException; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@RequiredArgsConstructor +@Slf4j +@Transactional(readOnly = true) +public class TicketServiceImpl implements TicketService { + + private final TicketRepository ticketRepository; + private final UserRepository userRepository; + + private final TicketMapper ticketMapper; + private final TicketTokenGenerator tokenGenerator; + + @Override + public VerifyTicketResponse verifyTicket(VerifyTicketRequest request) { + validateToken(request.getToken()); + LocalDate today = LocalDate.now(); + + return ticketRepository + .findByToken(request.getToken()) + .map( + ticket -> + ticket.isExpired(today) + ? ticketMapper.toVerifyExpired(ticket) + : ticketMapper.toVerifyFound(ticket)) + .orElseGet(ticketMapper::toVerifyNotFound); + } + + @Transactional + public ConsumeTicketResponse consumeTicket(ConsumeTicketRequest request) { + validateToken(request.getToken()); + + Ticket ticket = + ticketRepository + .findByTokenForUpdate(request.getToken()) + .orElseThrow(() -> new CustomException(TicketErrorCode.TICKET_NOT_FOUND)); + + boolean alreadyUsed = ticket.isUsed(); + if (!alreadyUsed) { + ticket.consume(LocalDateTime.now()); + + User user = userRepository.findById(ticket.getUserId()).orElse(null); + if (user != null && !user.isRewardReceived()) { + user.receiveReward(); + } + } + + return ticketMapper.toConsume(ticket, !alreadyUsed); + } + + private void validateToken(String token) { + if (token == null || token.isBlank()) { + throw new CustomException(TicketErrorCode.TICKET_TOKEN_REQUIRED); + } + } + + private Ticket issueNewTicket(Long userId) { + try { + String token = tokenGenerator.generate(); + + Ticket ticket = ticketMapper.toEntity(userId, token, LocalDate.now().plusDays(7)); + + return ticketRepository.save(ticket); + + } catch (Exception e) { + log.error("[MISSION] ticket issue failed userId={}", userId, e); + throw e; + } + } +} diff --git a/src/main/java/com/pbl/insaroad/domain/ticket/util/TicketQrPayloadFactory.java b/src/main/java/com/pbl/insaroad/domain/ticket/util/TicketQrPayloadFactory.java new file mode 100644 index 0000000..a51ce9f --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/ticket/util/TicketQrPayloadFactory.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.ticket.util; + +import org.springframework.stereotype.Component; + +@Component +public class TicketQrPayloadFactory { + + private static final String QR_BASE_URL = "https://api.danchu.site/ticket"; + private static final String QR_VERSION = "1"; + + public String create(String token) { + return QR_BASE_URL + "?v=" + QR_VERSION + "&token=" + token; + } +} diff --git a/src/main/java/com/pbl/insaroad/domain/ticket/util/TicketTokenGenerator.java b/src/main/java/com/pbl/insaroad/domain/ticket/util/TicketTokenGenerator.java new file mode 100644 index 0000000..73ee36b --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/ticket/util/TicketTokenGenerator.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.ticket.util; + +import java.security.SecureRandom; +import java.util.Base64; + +import org.springframework.stereotype.Component; + +@Component +public class TicketTokenGenerator { + + private static final SecureRandom RANDOM = new SecureRandom(); + + public String generate() { + byte[] bytes = new byte[32]; + RANDOM.nextBytes(bytes); + return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); + } +} From da770bbda10f988695a071a2627d710fa362ae98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=82=98=EA=B2=BD?= <1030n@naver.com> Date: Fri, 19 Dec 2025 10:45:07 +0900 Subject: [PATCH 2/3] =?UTF-8?q?:sparkles:=20Feat:=20=EA=B2=8C=EC=9E=84=20?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=C2=B7=EC=A2=85=EB=A3=8C=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EC=A0=84=EB=B0=98=EC=A0=81=EC=9D=B8=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../game/controller/GameController.java | 48 ++++++++ .../game/controller/GameControllerImpl.java | 39 +++++++ .../domain/game/dto/request/GameRequest.java | 35 ++++++ .../game/dto/response/GameResponse.java | 59 ++++++++++ .../domain/game/exception/GameErrorCode.java | 26 +++++ .../controller/LocationController.java | 34 ++++++ .../controller/LocationControllerImpl.java | 37 ++++++ .../location/dto/request/LocationRequest.java | 28 +++++ .../dto/response/LocationResponse.java | 51 +++++++++ .../domain/location/entity/Location.java | 50 ++++++++ .../location/exception/LocationErrorCode.java | 23 ++++ .../repository/LocationRepository.java | 28 +++++ .../location/service/LocationService.java | 14 +++ .../location/service/LocationServiceImpl.java | 55 +++++++++ .../ticket/controller/TicketController.java | 14 ++- .../controller/TicketControllerImpl.java | 9 ++ .../ticket/dto/response/TicketResponse.java | 30 +++++ .../domain/ticket/mapper/TicketMapper.java | 16 +++ .../ticket/repository/TicketRepository.java | 19 ++- .../domain/ticket/service/TicketService.java | 8 ++ .../ticket/service/TicketServiceImpl.java | 41 +++++-- .../pbl/insaroad/domain/user/entity/User.java | 38 ++++-- .../user/repository/UserRepository.java | 3 + .../domain/user/service/UserService.java | 108 ++++++++++++++++++ 24 files changed, 792 insertions(+), 21 deletions(-) create mode 100644 src/main/java/com/pbl/insaroad/domain/game/controller/GameController.java create mode 100644 src/main/java/com/pbl/insaroad/domain/game/controller/GameControllerImpl.java create mode 100644 src/main/java/com/pbl/insaroad/domain/game/dto/request/GameRequest.java create mode 100644 src/main/java/com/pbl/insaroad/domain/game/dto/response/GameResponse.java create mode 100644 src/main/java/com/pbl/insaroad/domain/game/exception/GameErrorCode.java create mode 100644 src/main/java/com/pbl/insaroad/domain/location/controller/LocationController.java create mode 100644 src/main/java/com/pbl/insaroad/domain/location/controller/LocationControllerImpl.java create mode 100644 src/main/java/com/pbl/insaroad/domain/location/dto/request/LocationRequest.java create mode 100644 src/main/java/com/pbl/insaroad/domain/location/dto/response/LocationResponse.java create mode 100644 src/main/java/com/pbl/insaroad/domain/location/entity/Location.java create mode 100644 src/main/java/com/pbl/insaroad/domain/location/exception/LocationErrorCode.java create mode 100644 src/main/java/com/pbl/insaroad/domain/location/repository/LocationRepository.java create mode 100644 src/main/java/com/pbl/insaroad/domain/location/service/LocationService.java create mode 100644 src/main/java/com/pbl/insaroad/domain/location/service/LocationServiceImpl.java diff --git a/src/main/java/com/pbl/insaroad/domain/game/controller/GameController.java b/src/main/java/com/pbl/insaroad/domain/game/controller/GameController.java new file mode 100644 index 0000000..1ae97c8 --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/game/controller/GameController.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.game.controller; + +import jakarta.validation.Valid; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +import com.pbl.insaroad.domain.game.dto.request.GameRequest.CompleteRequest; +import com.pbl.insaroad.domain.game.dto.request.GameRequest.UnvisitedRequest; +import com.pbl.insaroad.domain.game.dto.response.GameResponse.GameProgressResponse; +import com.pbl.insaroad.domain.game.dto.response.GameResponse.UnvisitedResponse; +import com.pbl.insaroad.global.response.BaseResponse; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "Game", description = "게임 관련 API") +@RequestMapping("/api/games") +public interface GameController { + + @PostMapping("/unvisited") + @Operation( + summary = "미방문 Location 전체 조회", + description = "사용자 기준으로 아직 방문하지 않은 Location 목록 전체를 반환합니다.") + ResponseEntity> unvisited( + @RequestBody @Valid UnvisitedRequest request); + + @PostMapping("/complete") + @Operation( + summary = "게임 진행/완료 처리", + description = + """ + 현재 Location 방문을 처리합니다. + + - 미션이 아직 남아있는 경우: + 사용자 방문 이력을 갱신하고, 아직 방문하지 않은 Location 목록을 반환합니다. + + - 이번 방문으로 모든 Location을 방문한 경우: + 미션을 완료 처리하고, 교환권 발급 정보를 함께 반환합니다. + """) + ResponseEntity> complete( + @RequestBody @Valid CompleteRequest request); +} diff --git a/src/main/java/com/pbl/insaroad/domain/game/controller/GameControllerImpl.java b/src/main/java/com/pbl/insaroad/domain/game/controller/GameControllerImpl.java new file mode 100644 index 0000000..3e3eabb --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/game/controller/GameControllerImpl.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.game.controller; + +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.pbl.insaroad.domain.game.dto.request.GameRequest.CompleteRequest; +import com.pbl.insaroad.domain.game.dto.request.GameRequest.UnvisitedRequest; +import com.pbl.insaroad.domain.game.dto.response.GameResponse.GameProgressResponse; +import com.pbl.insaroad.domain.game.dto.response.GameResponse.UnvisitedResponse; +import com.pbl.insaroad.domain.user.service.UserService; +import com.pbl.insaroad.global.response.BaseResponse; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +public class GameControllerImpl implements GameController { + + private final UserService userService; + + @Override + public ResponseEntity> unvisited( + @RequestBody @Valid UnvisitedRequest request) { + return ResponseEntity.ok( + BaseResponse.success(userService.getUnvisitedLocations(request.getUserCode()))); + } + + @Override + public ResponseEntity> complete( + @RequestBody @Valid CompleteRequest request) { + return ResponseEntity.ok(BaseResponse.success(userService.completeGame(request))); + } +} diff --git a/src/main/java/com/pbl/insaroad/domain/game/dto/request/GameRequest.java b/src/main/java/com/pbl/insaroad/domain/game/dto/request/GameRequest.java new file mode 100644 index 0000000..257838d --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/game/dto/request/GameRequest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.game.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** Game 관련 요청 DTO 모음 */ +public class GameRequest { + + /** 미방문 Location 전체 조회 요청 */ + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class UnvisitedRequest { + + @NotBlank private String userCode; + } + + /** 게임 1회 완료 처리 요청 - 첫 시작이면 userCode = null 허용 */ + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class CompleteRequest { + + private String userCode; + + @NotNull private Long currentLocationId; + } +} diff --git a/src/main/java/com/pbl/insaroad/domain/game/dto/response/GameResponse.java b/src/main/java/com/pbl/insaroad/domain/game/dto/response/GameResponse.java new file mode 100644 index 0000000..ae06bad --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/game/dto/response/GameResponse.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.game.dto.response; + +import java.time.LocalDateTime; +import java.util.List; + +import com.pbl.insaroad.domain.location.dto.LocationResponse; + +import lombok.Builder; +import lombok.Getter; + +/** Game 관련 응답 DTO 모음 */ +public class GameResponse { + + /** 미방문 Location 전체 조회 응답 */ + @Getter + @Builder + public static class UnvisitedResponse { + + private List unvisitedLocations; + } + + /** 게임 1회 완료 처리 응답 - 사용자 코드 - 아직 방문하지 않은 Location 전체 */ + @Getter + @Builder + public static class CompleteResponse { + + private String userCode; + private List unvisitedLocations; + } + + /** 전체 미션 완료 응답 (교환권 발급) */ + @Getter + @Builder + public static class FinishResponse { + + /** QR로 사용할 교환권 URL */ + private String exchangeUrl; + + /** 교환권 발급 시각 */ + private LocalDateTime issuedAt; + } + + @Getter + @Builder + public static class GameProgressResponse { + + /** 이번 complete 처리로 전체 미션이 완료되었는지 여부 */ + private boolean completed; + + /** 미완료 시 채움 */ + private CompleteResponse complete; + + /** 완료 시 채움 (교환권 발급 정보) */ + private FinishResponse finish; + } +} diff --git a/src/main/java/com/pbl/insaroad/domain/game/exception/GameErrorCode.java b/src/main/java/com/pbl/insaroad/domain/game/exception/GameErrorCode.java new file mode 100644 index 0000000..ffb30c0 --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/game/exception/GameErrorCode.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.game.exception; + +import org.springframework.http.HttpStatus; + +import com.pbl.insaroad.global.exception.model.BaseErrorCode; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum GameErrorCode implements BaseErrorCode { + USER_CODE_REQUIRED("GAME001", "사용자 번호가 필요합니다.", HttpStatus.BAD_REQUEST), + USER_NOT_FOUND("GAME002", "사용자를 찾을 수 없습니다.", HttpStatus.NOT_FOUND), + + LOCATION_NOT_FOUND("GAME010", "위치 정보를 찾을 수 없습니다.", HttpStatus.NOT_FOUND), + + NOT_ALL_LOCATIONS_VISITED("GAME020", "아직 방문하지 않은 위치가 남아있습니다.", HttpStatus.CONFLICT); + + private final String code; + private final String message; + private final HttpStatus status; +} diff --git a/src/main/java/com/pbl/insaroad/domain/location/controller/LocationController.java b/src/main/java/com/pbl/insaroad/domain/location/controller/LocationController.java new file mode 100644 index 0000000..f27667d --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/location/controller/LocationController.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.location.controller; + +import jakarta.validation.Valid; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +import com.pbl.insaroad.domain.location.dto.LocationResponse; +import com.pbl.insaroad.domain.location.dto.request.LocationRequest; +import com.pbl.insaroad.global.response.BaseResponse; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "Location", description = "Location 관련 API") +@RequestMapping("/api/locations") +public interface LocationController { + + @PostMapping + @Operation(summary = "Location 생성", description = "Location을 생성합니다.") + ResponseEntity> create( + @RequestBody @Valid LocationRequest.CreateLocationRequest request); + + @DeleteMapping("/{id}") + @Operation(summary = "Location 삭제", description = "Location을 삭제합니다.") + ResponseEntity> deleteById(@PathVariable("id") Long locationId); +} diff --git a/src/main/java/com/pbl/insaroad/domain/location/controller/LocationControllerImpl.java b/src/main/java/com/pbl/insaroad/domain/location/controller/LocationControllerImpl.java new file mode 100644 index 0000000..e28c0d5 --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/location/controller/LocationControllerImpl.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.location.controller; + +import jakarta.validation.Valid; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import com.pbl.insaroad.domain.location.dto.LocationResponse; +import com.pbl.insaroad.domain.location.dto.request.LocationRequest.CreateLocationRequest; +import com.pbl.insaroad.domain.location.service.LocationService; +import com.pbl.insaroad.global.response.BaseResponse; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +public class LocationControllerImpl implements LocationController { + + private final LocationService locationService; + + @Override + public ResponseEntity> create( + @RequestBody @Valid CreateLocationRequest request) { + return ResponseEntity.ok(BaseResponse.success(locationService.create(request))); + } + + @Override + public ResponseEntity> deleteById(@PathVariable("id") Long locationId) { + locationService.deleteById(locationId); + return ResponseEntity.ok(BaseResponse.success("Location이 성공적으로 삭제되었습니다.", null)); + } +} diff --git a/src/main/java/com/pbl/insaroad/domain/location/dto/request/LocationRequest.java b/src/main/java/com/pbl/insaroad/domain/location/dto/request/LocationRequest.java new file mode 100644 index 0000000..dae398e --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/location/dto/request/LocationRequest.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.location.dto.request; + +import jakarta.validation.constraints.NotBlank; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +public class LocationRequest { + + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class CreateLocationRequest { + + @NotBlank private String name; + + private String description; + private String address; + private String imageUrl; + + private Double latitude; + private Double longitude; + } +} diff --git a/src/main/java/com/pbl/insaroad/domain/location/dto/response/LocationResponse.java b/src/main/java/com/pbl/insaroad/domain/location/dto/response/LocationResponse.java new file mode 100644 index 0000000..ddd71e9 --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/location/dto/response/LocationResponse.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.location.dto; + +import com.pbl.insaroad.domain.location.entity.Location; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class LocationResponse { + + private Long id; + private String name; + private String description; + private String address; + private String imageUrl; + + private Double latitude; + private Double longitude; + + /** 카카오맵 링크(위경도 기반) - 위경도가 없으면 null */ + private String kakaoMapUrl; + + public static LocationResponse from(Location location) { + Double lat = location.getLatitude(); + Double lng = location.getLongitude(); + + return LocationResponse.builder() + .id(location.getId()) + .name(location.getName()) + .description(location.getDescription()) + .address(location.getAddress()) + .imageUrl(location.getImageUrl()) + .latitude(lat) + .longitude(lng) + .kakaoMapUrl(buildKakaoMapUrl(lat, lng)) + .build(); + } + + /** 카카오맵: 좌표로 지도 열기 - 모바일/웹 모두 동작하는 "map.kakao.com" 방식 - 좌표가 없으면 null */ + private static String buildKakaoMapUrl(Double latitude, Double longitude) { + if (latitude == null || longitude == null) { + return null; + } + // 예: https://map.kakao.com/link/map/37.5665,126.9780 + return "https://map.kakao.com/link/map/" + latitude + "," + longitude; + } +} diff --git a/src/main/java/com/pbl/insaroad/domain/location/entity/Location.java b/src/main/java/com/pbl/insaroad/domain/location/entity/Location.java new file mode 100644 index 0000000..a534440 --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/location/entity/Location.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.location.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Table(name = "locations") +public class Location { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "name", nullable = false, length = 100) + private String name; + + @Column(name = "description", length = 500) + private String description; + + @Column(name = "address", length = 200) + private String address; + + @Column(name = "image_url", length = 500) + private String imageUrl; + + /** 위도 (latitude) - 예: 37.5665 */ + @Column(name = "latitude") + private Double latitude; + + /** 경도 (longitude) - 예: 126.9780 */ + @Column(name = "longitude") + private Double longitude; +} diff --git a/src/main/java/com/pbl/insaroad/domain/location/exception/LocationErrorCode.java b/src/main/java/com/pbl/insaroad/domain/location/exception/LocationErrorCode.java new file mode 100644 index 0000000..8dda633 --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/location/exception/LocationErrorCode.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.location.exception; + +import org.springframework.http.HttpStatus; + +import com.pbl.insaroad.global.exception.model.BaseErrorCode; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum LocationErrorCode implements BaseErrorCode { + LOCATION_BAD_REQUEST("LOC001", "요청 값이 올바르지 않습니다.", HttpStatus.BAD_REQUEST), + LOCATION_NOT_FOUND("LOC002", "위치 정보를 찾을 수 없습니다.", HttpStatus.NOT_FOUND), + LOCATION_NUMBER_DUPLICATED("LOC003", "이미 존재하는 위치 번호입니다.", HttpStatus.CONFLICT); + + private final String code; + private final String message; + private final HttpStatus status; +} diff --git a/src/main/java/com/pbl/insaroad/domain/location/repository/LocationRepository.java b/src/main/java/com/pbl/insaroad/domain/location/repository/LocationRepository.java new file mode 100644 index 0000000..7fb8257 --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/location/repository/LocationRepository.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.location.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import com.pbl.insaroad.domain.location.entity.Location; + +public interface LocationRepository extends JpaRepository { + + /** 특정 사용자(userId)가 방문하지 않은 Location 전체 조회 - visitedLocationIds가 비어있어도 안전 */ + @Query( + """ + select l + from Location l + where l.id not in ( + select v + from User u + join u.visitedLocationIds v + where u.id = :userId + ) + """) + List findAllUnvisitedByUserId(Long userId); +} diff --git a/src/main/java/com/pbl/insaroad/domain/location/service/LocationService.java b/src/main/java/com/pbl/insaroad/domain/location/service/LocationService.java new file mode 100644 index 0000000..812f09b --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/location/service/LocationService.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.location.service; + +import com.pbl.insaroad.domain.location.dto.LocationResponse; +import com.pbl.insaroad.domain.location.dto.request.LocationRequest.CreateLocationRequest; + +public interface LocationService { + + LocationResponse create(CreateLocationRequest request); + + void deleteById(Long locationId); +} diff --git a/src/main/java/com/pbl/insaroad/domain/location/service/LocationServiceImpl.java b/src/main/java/com/pbl/insaroad/domain/location/service/LocationServiceImpl.java new file mode 100644 index 0000000..3288ca7 --- /dev/null +++ b/src/main/java/com/pbl/insaroad/domain/location/service/LocationServiceImpl.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) SKU PBL Team4 + */ +package com.pbl.insaroad.domain.location.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.pbl.insaroad.domain.location.dto.LocationResponse; +import com.pbl.insaroad.domain.location.dto.request.LocationRequest.CreateLocationRequest; +import com.pbl.insaroad.domain.location.entity.Location; +import com.pbl.insaroad.domain.location.exception.LocationErrorCode; +import com.pbl.insaroad.domain.location.repository.LocationRepository; +import com.pbl.insaroad.global.exception.CustomException; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class LocationServiceImpl implements LocationService { + + private final LocationRepository locationRepository; + + @Override + @Transactional + public LocationResponse create(CreateLocationRequest request) { + if (request.getName() == null || request.getName().isBlank()) { + throw new CustomException(LocationErrorCode.LOCATION_BAD_REQUEST); + } + + Location saved = + locationRepository.save( + Location.builder() + .name(request.getName()) + .description(request.getDescription()) + .address(request.getAddress()) + .imageUrl(request.getImageUrl()) + .latitude(request.getLatitude()) + .longitude(request.getLongitude()) + .build()); + + return LocationResponse.from(saved); + } + + @Override + @Transactional + public void deleteById(Long locationId) { + Location location = + locationRepository + .findById(locationId) + .orElseThrow(() -> new CustomException(LocationErrorCode.LOCATION_NOT_FOUND)); + locationRepository.delete(location); + } +} diff --git a/src/main/java/com/pbl/insaroad/domain/ticket/controller/TicketController.java b/src/main/java/com/pbl/insaroad/domain/ticket/controller/TicketController.java index bea3a81..bdc3074 100644 --- a/src/main/java/com/pbl/insaroad/domain/ticket/controller/TicketController.java +++ b/src/main/java/com/pbl/insaroad/domain/ticket/controller/TicketController.java @@ -3,9 +3,13 @@ */ package com.pbl.insaroad.domain.ticket.controller; +import java.util.List; + import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -13,13 +17,14 @@ import com.pbl.insaroad.domain.ticket.dto.request.TicketRequest.ConsumeTicketRequest; import com.pbl.insaroad.domain.ticket.dto.request.TicketRequest.VerifyTicketRequest; import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.ConsumeTicketResponse; +import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.TicketItemResponse; import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.VerifyTicketResponse; import com.pbl.insaroad.global.response.BaseResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -@Tag(name = "티켓", description = "교환권(티켓) 검증/사용 처리 API") +@Tag(name = "Ticket", description = "교환권 관련 API") @RequestMapping("/api/tickets") public interface TicketController { @@ -32,4 +37,11 @@ ResponseEntity> verifyTicket( @Operation(summary = "[관리자] 교환권 사용 처리", description = "token으로 교환권을 사용 처리합니다. (멱등 보장)") ResponseEntity> consumeTicket( @RequestBody @Valid ConsumeTicketRequest request); + + @GetMapping("/users/{userCode}") + @Operation( + summary = "사용자 코드로 티켓 조회", + description = "userCode로 사용자를 찾고, 해당 사용자의 티켓 목록(최신순)을 반환합니다.") + ResponseEntity>> getTicketsByUserCode( + @PathVariable String userCode); } diff --git a/src/main/java/com/pbl/insaroad/domain/ticket/controller/TicketControllerImpl.java b/src/main/java/com/pbl/insaroad/domain/ticket/controller/TicketControllerImpl.java index c8b6c89..b94be9f 100644 --- a/src/main/java/com/pbl/insaroad/domain/ticket/controller/TicketControllerImpl.java +++ b/src/main/java/com/pbl/insaroad/domain/ticket/controller/TicketControllerImpl.java @@ -3,6 +3,8 @@ */ package com.pbl.insaroad.domain.ticket.controller; +import java.util.List; + import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; @@ -12,6 +14,7 @@ import com.pbl.insaroad.domain.ticket.dto.request.TicketRequest.ConsumeTicketRequest; import com.pbl.insaroad.domain.ticket.dto.request.TicketRequest.VerifyTicketRequest; import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.ConsumeTicketResponse; +import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.TicketItemResponse; import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.VerifyTicketResponse; import com.pbl.insaroad.domain.ticket.service.TicketService; import com.pbl.insaroad.global.response.BaseResponse; @@ -35,4 +38,10 @@ public ResponseEntity> consumeTicket( @RequestBody @Valid ConsumeTicketRequest request) { return ResponseEntity.ok(BaseResponse.success(ticketService.consumeTicket(request))); } + + @Override + public ResponseEntity>> getTicketsByUserCode( + String userCode) { + return ResponseEntity.ok(BaseResponse.success(ticketService.getTicketsByUserCode(userCode))); + } } diff --git a/src/main/java/com/pbl/insaroad/domain/ticket/dto/response/TicketResponse.java b/src/main/java/com/pbl/insaroad/domain/ticket/dto/response/TicketResponse.java index 4c5d76c..c81c5b8 100644 --- a/src/main/java/com/pbl/insaroad/domain/ticket/dto/response/TicketResponse.java +++ b/src/main/java/com/pbl/insaroad/domain/ticket/dto/response/TicketResponse.java @@ -42,4 +42,34 @@ public static class ConsumeTicketResponse { private LocalDateTime usedAt; private LocalDate expiresAt; } + + @Getter + @Builder + public static class TicketDetailResponse { + + private Long ticketId; + + private String token; + private String qrPayload; + + private LocalDateTime issuedAt; + private LocalDateTime usedAt; + private LocalDate expiresAt; + } + + @Getter + @Builder + public static class TicketItemResponse { + + private Long ticketId; + + private String token; + private String qrPayload; + + private LocalDateTime issuedAt; + private LocalDateTime usedAt; + private LocalDate expiresAt; + + private boolean expired; + } } diff --git a/src/main/java/com/pbl/insaroad/domain/ticket/mapper/TicketMapper.java b/src/main/java/com/pbl/insaroad/domain/ticket/mapper/TicketMapper.java index 78012eb..b7cfe46 100644 --- a/src/main/java/com/pbl/insaroad/domain/ticket/mapper/TicketMapper.java +++ b/src/main/java/com/pbl/insaroad/domain/ticket/mapper/TicketMapper.java @@ -8,6 +8,7 @@ import org.springframework.stereotype.Component; import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.ConsumeTicketResponse; +import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.TicketItemResponse; import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.VerifyTicketResponse; import com.pbl.insaroad.domain.ticket.entity.Ticket; import com.pbl.insaroad.domain.ticket.util.TicketQrPayloadFactory; @@ -61,4 +62,19 @@ public ConsumeTicketResponse toConsume(Ticket ticket, boolean consumed) { .expiresAt(ticket.getExpiresAt()) .build(); } + + public TicketItemResponse toItemResponse(Ticket ticket) { + + boolean expired = + ticket.getExpiresAt() != null && ticket.getExpiresAt().isBefore(LocalDate.now()); + + return TicketItemResponse.builder() + .ticketId(ticket.getId()) + .token(ticket.getToken()) + .qrPayload(qrPayloadFactory.create(ticket.getToken())) + .issuedAt(ticket.getCreatedAt()) + .usedAt(ticket.getUsedAt()) + .expiresAt(ticket.getExpiresAt()) + .build(); + } } diff --git a/src/main/java/com/pbl/insaroad/domain/ticket/repository/TicketRepository.java b/src/main/java/com/pbl/insaroad/domain/ticket/repository/TicketRepository.java index 4fa1b48..563856a 100644 --- a/src/main/java/com/pbl/insaroad/domain/ticket/repository/TicketRepository.java +++ b/src/main/java/com/pbl/insaroad/domain/ticket/repository/TicketRepository.java @@ -3,10 +3,13 @@ */ package com.pbl.insaroad.domain.ticket.repository; +import java.time.LocalDate; +import java.util.List; import java.util.Optional; import jakarta.persistence.LockModeType; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; @@ -22,6 +25,20 @@ public interface TicketRepository extends JpaRepository { @Query("select t from Ticket t where t.token = :token") Optional findByTokenForUpdate(@Param("token") String token); - /** 사용자당 1장의 교환권만 허용 (멱등) */ + List findAllByUserIdOrderByCreatedAtDesc(Long userId); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query( + """ + select t + from Ticket t + where t.userId = :userId + and t.usedAt is null + and (t.expiresAt is null or t.expiresAt >= :today) + order by t.createdAt desc + """) + List findLatestValidTicketForUpdate( + @Param("userId") Long userId, @Param("today") LocalDate today, Pageable pageable); + Optional findTop1ByUserIdOrderByCreatedAtDesc(Long userId); } diff --git a/src/main/java/com/pbl/insaroad/domain/ticket/service/TicketService.java b/src/main/java/com/pbl/insaroad/domain/ticket/service/TicketService.java index d95a028..40a9d50 100644 --- a/src/main/java/com/pbl/insaroad/domain/ticket/service/TicketService.java +++ b/src/main/java/com/pbl/insaroad/domain/ticket/service/TicketService.java @@ -3,10 +3,14 @@ */ package com.pbl.insaroad.domain.ticket.service; +import java.util.List; + import com.pbl.insaroad.domain.ticket.dto.request.TicketRequest.ConsumeTicketRequest; import com.pbl.insaroad.domain.ticket.dto.request.TicketRequest.VerifyTicketRequest; import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.ConsumeTicketResponse; +import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.TicketItemResponse; import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.VerifyTicketResponse; +import com.pbl.insaroad.domain.ticket.entity.Ticket; public interface TicketService { @@ -37,4 +41,8 @@ public interface TicketService { * @return 티켓 사용 처리 결과 응답 */ ConsumeTicketResponse consumeTicket(ConsumeTicketRequest request); + + Ticket issueNewTicket(Long userId); + + List getTicketsByUserCode(String userCode); } diff --git a/src/main/java/com/pbl/insaroad/domain/ticket/service/TicketServiceImpl.java b/src/main/java/com/pbl/insaroad/domain/ticket/service/TicketServiceImpl.java index 6ebae8e..7a78979 100644 --- a/src/main/java/com/pbl/insaroad/domain/ticket/service/TicketServiceImpl.java +++ b/src/main/java/com/pbl/insaroad/domain/ticket/service/TicketServiceImpl.java @@ -5,13 +5,17 @@ import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.List; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.pbl.insaroad.domain.game.exception.GameErrorCode; import com.pbl.insaroad.domain.ticket.dto.request.TicketRequest.ConsumeTicketRequest; import com.pbl.insaroad.domain.ticket.dto.request.TicketRequest.VerifyTicketRequest; import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.ConsumeTicketResponse; +import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.TicketItemResponse; import com.pbl.insaroad.domain.ticket.dto.response.TicketResponse.VerifyTicketResponse; import com.pbl.insaroad.domain.ticket.entity.Ticket; import com.pbl.insaroad.domain.ticket.exception.TicketErrorCode; @@ -80,17 +84,38 @@ private void validateToken(String token) { } } - private Ticket issueNewTicket(Long userId) { - try { - String token = tokenGenerator.generate(); + @Override + public Ticket issueNewTicket(Long userId) { - Ticket ticket = ticketMapper.toEntity(userId, token, LocalDate.now().plusDays(7)); + List latestValid = + ticketRepository.findLatestValidTicketForUpdate( + userId, LocalDate.now(), PageRequest.of(0, 1)); - return ticketRepository.save(ticket); + if (!latestValid.isEmpty()) { + return latestValid.getFirst(); + } - } catch (Exception e) { - log.error("[MISSION] ticket issue failed userId={}", userId, e); - throw e; + String token = tokenGenerator.generate(); + Ticket ticket = ticketMapper.toEntity(userId, token, LocalDate.now().plusDays(7)); + return ticketRepository.save(ticket); + } + + @Override + public List getTicketsByUserCode(String userCode) { + User user = getUserByCodeOrThrow(userCode); + + return ticketRepository.findAllByUserIdOrderByCreatedAtDesc(user.getId()).stream() + .map(ticketMapper::toItemResponse) + .toList(); + } + + private User getUserByCodeOrThrow(String userCode) { + if (userCode == null || userCode.isBlank()) { + throw new CustomException(GameErrorCode.USER_CODE_REQUIRED); } + + return userRepository + .findByCode(userCode) + .orElseThrow(() -> new CustomException(GameErrorCode.USER_NOT_FOUND)); } } diff --git a/src/main/java/com/pbl/insaroad/domain/user/entity/User.java b/src/main/java/com/pbl/insaroad/domain/user/entity/User.java index b2cae7b..a3c2f7a 100644 --- a/src/main/java/com/pbl/insaroad/domain/user/entity/User.java +++ b/src/main/java/com/pbl/insaroad/domain/user/entity/User.java @@ -3,11 +3,17 @@ */ package com.pbl.insaroad.domain.user.entity; +import java.util.ArrayList; +import java.util.List; + +import jakarta.persistence.CollectionTable; import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; import jakarta.persistence.Table; import lombok.AllArgsConstructor; @@ -27,30 +33,28 @@ public class User { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - /** 랜덤 고유 3자리 번호 (예: 123) 중복 방지를 원하면 unique = true 설정 */ + /** 랜덤 고유 3자리 번호 (예: 123) */ @Column(name = "code", length = 3, nullable = false, unique = true) private String code; - /** 현재 미션 진행 스테이지 기본값 = 1 */ + /** 방문한 Location ID 리스트 - 연관관계 없이 단순 ID(Long) 목록 저장 - 방문 순서 보존 */ @Builder.Default - @Column(name = "stage", nullable = false) - private int stage = 1; + @ElementCollection + @CollectionTable(name = "user_visited_location_ids", joinColumns = @JoinColumn(name = "user_id")) + @Column(name = "location_id", nullable = false) + private List visitedLocationIds = new ArrayList<>(); - /** 완주 여부 (기본값 = false) */ + /** 완주 여부 */ @Builder.Default @Column(name = "is_completed", nullable = false) private boolean completed = false; - /** 상품 수령 여부 (기본값 = false) */ + /** 상품 수령 여부 */ @Builder.Default @Column(name = "is_reward_received", nullable = false) private boolean rewardReceived = false; - /* ====== 동작 메서드 ====== */ - - public void nextStage() { - this.stage += 1; - } + /* ====== Domain Logic ====== */ public void completeMission() { this.completed = true; @@ -59,4 +63,16 @@ public void completeMission() { public void receiveReward() { this.rewardReceived = true; } + + /** 방문 LocationId 추가 - 이미 방문한 곳이면 무시(중복 방지) */ + public void addVisitedLocationId(Long locationId) { + if (locationId == null) { + return; + } + + // 방문 순서를 유지하면서 "중복 방문"은 막는 정책 + if (!visitedLocationIds.contains(locationId)) { + visitedLocationIds.add(locationId); + } + } } diff --git a/src/main/java/com/pbl/insaroad/domain/user/repository/UserRepository.java b/src/main/java/com/pbl/insaroad/domain/user/repository/UserRepository.java index acc9696..ae76f85 100644 --- a/src/main/java/com/pbl/insaroad/domain/user/repository/UserRepository.java +++ b/src/main/java/com/pbl/insaroad/domain/user/repository/UserRepository.java @@ -3,6 +3,7 @@ */ package com.pbl.insaroad.domain.user.repository; +import java.util.Optional; import java.util.Set; import org.springframework.data.jpa.repository.JpaRepository; @@ -14,4 +15,6 @@ public interface UserRepository extends JpaRepository { @Query("select u.code from User u") Set findAllCodes(); + + Optional findByCode(String code); } diff --git a/src/main/java/com/pbl/insaroad/domain/user/service/UserService.java b/src/main/java/com/pbl/insaroad/domain/user/service/UserService.java index a4b5d74..1da4b68 100644 --- a/src/main/java/com/pbl/insaroad/domain/user/service/UserService.java +++ b/src/main/java/com/pbl/insaroad/domain/user/service/UserService.java @@ -12,6 +12,18 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.pbl.insaroad.domain.game.dto.request.GameRequest.CompleteRequest; +import com.pbl.insaroad.domain.game.dto.response.GameResponse.CompleteResponse; +import com.pbl.insaroad.domain.game.dto.response.GameResponse.FinishResponse; +import com.pbl.insaroad.domain.game.dto.response.GameResponse.GameProgressResponse; +import com.pbl.insaroad.domain.game.dto.response.GameResponse.UnvisitedResponse; +import com.pbl.insaroad.domain.game.exception.GameErrorCode; +import com.pbl.insaroad.domain.location.dto.LocationResponse; +import com.pbl.insaroad.domain.location.entity.Location; +import com.pbl.insaroad.domain.location.repository.LocationRepository; +import com.pbl.insaroad.domain.ticket.entity.Ticket; +import com.pbl.insaroad.domain.ticket.service.TicketService; +import com.pbl.insaroad.domain.ticket.util.TicketQrPayloadFactory; import com.pbl.insaroad.domain.user.entity.User; import com.pbl.insaroad.domain.user.exception.UserErrorCode; import com.pbl.insaroad.domain.user.mapper.UserMapper; @@ -30,6 +42,11 @@ public class UserService { private final UserRepository userRepository; private final UserMapper userMapper; + private final LocationRepository locationRepository; + + private final TicketService ticketService; + private final TicketQrPayloadFactory qrPayloadFactory; + public User createUser() { String code = generateUniqueCode(); @@ -59,4 +76,95 @@ public String generateUniqueCode() { Collections.shuffle(all); return all.getFirst(); } + + public UnvisitedResponse getUnvisitedLocations(String userCode) { + User user = getUserByCodeOrThrow(userCode); + + List unvisited = findUnvisitedLocations(user); + + return UnvisitedResponse.builder() + .unvisitedLocations(unvisited.stream().map(LocationResponse::from).toList()) + .build(); + } + + /* ========================= + * 게임 완료 처리 + * - 첫 시작이면 사용자 생성 + * - 현재 locationId 방문 처리 + * - 응답: userCode + 미방문 Location 전체 + * - 이번 요청으로 전체 미션 완료됨 → finish 로직 수행 + * ========================= */ + @Transactional + public GameProgressResponse completeGame(CompleteRequest request) { + + // 현재 위치 존재 확인 + Location current = + locationRepository + .findById(request.getCurrentLocationId()) + .orElseThrow(() -> new CustomException(GameErrorCode.LOCATION_NOT_FOUND)); + + // 첫 시작이면 생성 + User user = + (request.getUserCode() == null || request.getUserCode().isBlank()) + ? createUser() + : getUserByCodeOrThrow(request.getUserCode()); + + // 방문 처리 (중복이면 무시) + user.addVisitedLocationId(current.getId()); + + // 남은 곳 전체 조회 + List unvisited = locationRepository.findAllUnvisitedByUserId(user.getId()); + + // 미션 완료가 아니라면: CompleteResponse 반환 + if (!unvisited.isEmpty()) { + CompleteResponse complete = + CompleteResponse.builder() + .userCode(user.getCode()) + .unvisitedLocations(unvisited.stream().map(LocationResponse::from).toList()) + .build(); + + return GameProgressResponse.builder() + .completed(false) + .complete(complete) + .finish(null) + .build(); + } + + // 이번 요청으로 전체 미션 완료됨 → finish 로직 수행 + if (!user.isCompleted()) { + user.completeMission(); + } + + Ticket ticket = ticketService.issueNewTicket(user.getId()); + + FinishResponse finish = + FinishResponse.builder() + .exchangeUrl(qrPayloadFactory.create(ticket.getToken())) + .issuedAt(ticket.getCreatedAt()) + .build(); + + return GameProgressResponse.builder().completed(true).complete(null).finish(finish).build(); + } + + /* ========================= + * Private Helpers + * ========================= */ + + private User getUserByCodeOrThrow(String userCode) { + if (userCode == null || userCode.isBlank()) { + throw new CustomException(GameErrorCode.USER_CODE_REQUIRED); + } + return userRepository + .findByCode(userCode) + .orElseThrow(() -> new CustomException(GameErrorCode.USER_NOT_FOUND)); + } + + private List findUnvisitedLocations(User user) { + List visitedIds = user.getVisitedLocationIds(); + + if (visitedIds == null || visitedIds.isEmpty()) { + return locationRepository.findAll(); + } + return locationRepository.findAllUnvisitedByUserId(user.getId()); + } } From c42103e27dda2c46a3a6598e771cd1bf3a4c31c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=82=98=EA=B2=BD?= <1030n@naver.com> Date: Fri, 19 Dec 2025 10:45:23 +0900 Subject: [PATCH 3/3] =?UTF-8?q?:recycle:=20Refactor:=20spotless=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../animalmission/controller/AnimalMissionController.java | 3 +++ .../com/pbl/insaroad/domain/animalmission/data/Crane.java | 3 +++ .../com/pbl/insaroad/domain/animalmission/data/Haetae.java | 3 +++ .../com/pbl/insaroad/domain/animalmission/data/Magpie.java | 3 +++ .../com/pbl/insaroad/domain/animalmission/data/Tiger.java | 3 +++ .../com/pbl/insaroad/domain/animalmission/data/Turtle.java | 3 +++ .../animalmission/dto/request/AnimalMissionSubmitRequest.java | 3 +++ .../animalmission/dto/response/DescriptionResponse.java | 3 +++ .../domain/animalmission/dto/response/ResultTextSegment.java | 3 +++ .../pbl/insaroad/domain/animalmission/entity/AnimalType.java | 3 +++ .../animalmission/exception/AnimalMissionErrorCode.java | 4 ++++ .../domain/animalmission/service/AnimalMissionService.java | 3 +++ 12 files changed, 37 insertions(+) diff --git a/src/main/java/com/pbl/insaroad/domain/animalmission/controller/AnimalMissionController.java b/src/main/java/com/pbl/insaroad/domain/animalmission/controller/AnimalMissionController.java index b283200..abc9572 100644 --- a/src/main/java/com/pbl/insaroad/domain/animalmission/controller/AnimalMissionController.java +++ b/src/main/java/com/pbl/insaroad/domain/animalmission/controller/AnimalMissionController.java @@ -1,3 +1,6 @@ +/* + * Copyright (c) SKU PBL Team4 + */ package com.pbl.insaroad.domain.animalmission.controller; import jakarta.validation.Valid; diff --git a/src/main/java/com/pbl/insaroad/domain/animalmission/data/Crane.java b/src/main/java/com/pbl/insaroad/domain/animalmission/data/Crane.java index d284a4e..9c3061b 100644 --- a/src/main/java/com/pbl/insaroad/domain/animalmission/data/Crane.java +++ b/src/main/java/com/pbl/insaroad/domain/animalmission/data/Crane.java @@ -1,3 +1,6 @@ +/* + * Copyright (c) SKU PBL Team4 + */ package com.pbl.insaroad.domain.animalmission.data; import java.util.Arrays; diff --git a/src/main/java/com/pbl/insaroad/domain/animalmission/data/Haetae.java b/src/main/java/com/pbl/insaroad/domain/animalmission/data/Haetae.java index 5c98137..5edd0f6 100644 --- a/src/main/java/com/pbl/insaroad/domain/animalmission/data/Haetae.java +++ b/src/main/java/com/pbl/insaroad/domain/animalmission/data/Haetae.java @@ -1,3 +1,6 @@ +/* + * Copyright (c) SKU PBL Team4 + */ package com.pbl.insaroad.domain.animalmission.data; import java.util.Arrays; diff --git a/src/main/java/com/pbl/insaroad/domain/animalmission/data/Magpie.java b/src/main/java/com/pbl/insaroad/domain/animalmission/data/Magpie.java index 02ba9f7..830d5bb 100644 --- a/src/main/java/com/pbl/insaroad/domain/animalmission/data/Magpie.java +++ b/src/main/java/com/pbl/insaroad/domain/animalmission/data/Magpie.java @@ -1,3 +1,6 @@ +/* + * Copyright (c) SKU PBL Team4 + */ package com.pbl.insaroad.domain.animalmission.data; import java.util.Arrays; diff --git a/src/main/java/com/pbl/insaroad/domain/animalmission/data/Tiger.java b/src/main/java/com/pbl/insaroad/domain/animalmission/data/Tiger.java index 32a7da5..ac51eb6 100644 --- a/src/main/java/com/pbl/insaroad/domain/animalmission/data/Tiger.java +++ b/src/main/java/com/pbl/insaroad/domain/animalmission/data/Tiger.java @@ -1,3 +1,6 @@ +/* + * Copyright (c) SKU PBL Team4 + */ package com.pbl.insaroad.domain.animalmission.data; import java.util.Arrays; diff --git a/src/main/java/com/pbl/insaroad/domain/animalmission/data/Turtle.java b/src/main/java/com/pbl/insaroad/domain/animalmission/data/Turtle.java index c12cd08..cc99b5f 100644 --- a/src/main/java/com/pbl/insaroad/domain/animalmission/data/Turtle.java +++ b/src/main/java/com/pbl/insaroad/domain/animalmission/data/Turtle.java @@ -1,3 +1,6 @@ +/* + * Copyright (c) SKU PBL Team4 + */ package com.pbl.insaroad.domain.animalmission.data; import java.util.Arrays; diff --git a/src/main/java/com/pbl/insaroad/domain/animalmission/dto/request/AnimalMissionSubmitRequest.java b/src/main/java/com/pbl/insaroad/domain/animalmission/dto/request/AnimalMissionSubmitRequest.java index 914314f..90b8c92 100644 --- a/src/main/java/com/pbl/insaroad/domain/animalmission/dto/request/AnimalMissionSubmitRequest.java +++ b/src/main/java/com/pbl/insaroad/domain/animalmission/dto/request/AnimalMissionSubmitRequest.java @@ -1,3 +1,6 @@ +/* + * Copyright (c) SKU PBL Team4 + */ package com.pbl.insaroad.domain.animalmission.dto.request; import java.util.List; diff --git a/src/main/java/com/pbl/insaroad/domain/animalmission/dto/response/DescriptionResponse.java b/src/main/java/com/pbl/insaroad/domain/animalmission/dto/response/DescriptionResponse.java index 03a1bee..a04d65a 100644 --- a/src/main/java/com/pbl/insaroad/domain/animalmission/dto/response/DescriptionResponse.java +++ b/src/main/java/com/pbl/insaroad/domain/animalmission/dto/response/DescriptionResponse.java @@ -1,3 +1,6 @@ +/* + * Copyright (c) SKU PBL Team4 + */ package com.pbl.insaroad.domain.animalmission.dto.response; import java.util.List; diff --git a/src/main/java/com/pbl/insaroad/domain/animalmission/dto/response/ResultTextSegment.java b/src/main/java/com/pbl/insaroad/domain/animalmission/dto/response/ResultTextSegment.java index ec6fba5..36945fb 100644 --- a/src/main/java/com/pbl/insaroad/domain/animalmission/dto/response/ResultTextSegment.java +++ b/src/main/java/com/pbl/insaroad/domain/animalmission/dto/response/ResultTextSegment.java @@ -1,3 +1,6 @@ +/* + * Copyright (c) SKU PBL Team4 + */ package com.pbl.insaroad.domain.animalmission.dto.response; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/com/pbl/insaroad/domain/animalmission/entity/AnimalType.java b/src/main/java/com/pbl/insaroad/domain/animalmission/entity/AnimalType.java index 297510e..60d999a 100644 --- a/src/main/java/com/pbl/insaroad/domain/animalmission/entity/AnimalType.java +++ b/src/main/java/com/pbl/insaroad/domain/animalmission/entity/AnimalType.java @@ -1,3 +1,6 @@ +/* + * Copyright (c) SKU PBL Team4 + */ package com.pbl.insaroad.domain.animalmission.entity; import lombok.Getter; diff --git a/src/main/java/com/pbl/insaroad/domain/animalmission/exception/AnimalMissionErrorCode.java b/src/main/java/com/pbl/insaroad/domain/animalmission/exception/AnimalMissionErrorCode.java index 4906d59..4a147da 100644 --- a/src/main/java/com/pbl/insaroad/domain/animalmission/exception/AnimalMissionErrorCode.java +++ b/src/main/java/com/pbl/insaroad/domain/animalmission/exception/AnimalMissionErrorCode.java @@ -1,6 +1,10 @@ +/* + * Copyright (c) SKU PBL Team4 + */ package com.pbl.insaroad.domain.animalmission.exception; import org.springframework.http.HttpStatus; + import com.pbl.insaroad.global.exception.model.BaseErrorCode; import lombok.AllArgsConstructor; diff --git a/src/main/java/com/pbl/insaroad/domain/animalmission/service/AnimalMissionService.java b/src/main/java/com/pbl/insaroad/domain/animalmission/service/AnimalMissionService.java index 7f1cbcc..f4bd7ae 100644 --- a/src/main/java/com/pbl/insaroad/domain/animalmission/service/AnimalMissionService.java +++ b/src/main/java/com/pbl/insaroad/domain/animalmission/service/AnimalMissionService.java @@ -1,3 +1,6 @@ +/* + * Copyright (c) SKU PBL Team4 + */ package com.pbl.insaroad.domain.animalmission.service; import java.util.*;