diff --git a/README.md b/README.md
index c92662e..7129e1b 100644
--- a/README.md
+++ b/README.md
@@ -1,130 +1,28 @@
-# 카드 매입 승인 경로 - Spring Boot 3 전환
+# 카드 매입 승인 경로 Spring Boot 3 전환
-## 전환 개요
+## 전환 범위
| 항목 | 기존 | 전환 후 |
|------|------|----------|
-| Spring Boot | 2.7.x | **3.2.5** |
-| Spring Framework | 5.x | **6.1.x** |
-| Java | 11 | **17** |
-| Jakarta EE | 8 (javax.*) | **10 (jakarta.*)** |
-| Hibernate | 5.x | **6.4.x** |
-| JPA | 2.2 | **3.1** |
-| Validation | Bean Validation 2.0 | **Bean Validation 3.0** |
+| Spring Boot | 2.7.x | 3.2.x |
+| Java | 11 | 17 (LTS) |
+| javax.* 네임스페이스 | jakarta.* |
+| JPA/Hibernate | 2.x | 3.x |
+| Spring Data JPA | 2.x | 3.x |
+| H2 Database | 2.1.x | 2.2.x |
-## 주요 전환 사항
+## 검증 절차
-### 1. Jakarta EE 9+ 마이그레이션
-- `javax.*` → `jakarta.*` 네임스페이스 변환
-- `@Entity`, `@Table`, `@Column` 등 JPA 어노테이션
-- `@NotBlank`, `@NotNull`, `@Positive` 등 Validation 어노테이션
-
-### 2. Spring Framework 6.x 변경사항
-- Jakarta Servlet API 사용
-- 개선된 예외 처리 구조
-
-### 3. Spring Boot 3.x 의존성
-- `spring-boot-starter-validation` (Bean Validation 3.0 내장)
-- `spring-boot-starter-actuator` (헬스체크)
-- Jackson 2.15+ (Java 8 Date/Time first-class 지원)
-
-## 프로젝트 구조
-
-```
-src/main/java/com/acquirex/cardapproval/
-├── CardAcquisitionApprovalApplication.java # 메인 애플리케이션
-├── controller/
-│ └── AcquisitionApprovalController.java # REST API 엔드포인트
-├── domain/
-│ └── AcquisitionApproval.java # 도메인 엔티티 + Repository
-└── service/
- └── AcquisitionApprovalService.java # 비즈니스 로직 + DTO + Exception Handler
+```bash
+./mvnw clean verify
```
## API 엔드포인트
-| Method | Endpoint | 설명 |
-|--------|----------|------|
-| POST | `/api/v1/acquisitions` | 매입 승인 요청 처리 |
-| GET | `/api/v1/acquisitions/{id}` | 매입 승인 단건 조회 |
-| GET | `/api/v1/acquisitions/merchant/{merchantId}` | 가맹점별 목록 조회 |
-| GET | `/api/v1/acquisitions/period?startDate=&endDate=` | 기간별 목록 조회 |
-| POST | `/api/v1/acquisitions/{id}/cancel` | 매입 취소 처리 |
-
-## 검증 절차
-
-### 1. 빌드 검증
-```bash
-./mvnw clean compile
-```
-
-### 2. 테스트 실행
-```bash
-./mvnw test
-```
-
-### 3. 애플리케이션 실행
-```bash
-./mvnw spring-boot:run
-```
-
-### 4. API 동작 확인
-```bash
-# 매입 승인 요청
-curl -X POST http://localhost:8080/api/v1/acquisitions \
- -H "Content-Type: application/json" \
- -d '{
- "merchantId": "MERCHANT001",
- "cardNumber": "1234567890123456",
- "acquisitionAmount": 50000,
- "approvalAmount": 50000,
- "approvalNumber": "APPR123456",
- "acquisitionDatetime": "2024-01-15T10:30:00"
- }'
-
-# 매입 승인 조회
-curl http://localhost:8080/api/v1/acquisitions/1
-
-# 매입 취소
-curl -X POST http://localhost:8080/api/v1/acquisitions/1/cancel \
- -H "Content-Type: application/json" \
- -d '{"reason": "고객 요청"}'
-```
-
-### 5. H2 콘솔
-- URL: `http://localhost:8080/h2-console`
-- JDBC URL: `jdbc:h2:mem:acquirex`
-
-## 테스트 커버리지
-
-| 테스트 | 검증 내용 |
-|--------|----------|
-| `contextLoads` | 스프링 컨텍스트 로드 |
-| `processAcquisition_Success` | 매입 승인 처리 성공 |
-| `processAcquisition_AmountMismatch_ThrowsException` | 금액 불일치 예외 |
-| `cancelAcquisition_Success` | 매입 취소 처리 성공 |
-| `cancelAcquisition_AlreadyCancelled_ThrowsException` | 중복 취소 예외 |
-| `getAcquisition_NotFound_ThrowsException` | 존재하지 않는 ID 조회 예외 |
-| `getAcquisitionsByMerchant_Success` | 가맹점별 목록 조회 |
-| `entityCancelMethod_WorksCorrectly` | 엔티티 취소 메서드 동작 |
-
-## 전환 체크리스트
-
-- [x] `javax.persistence` → `jakarta.persistence` 마이그레이션
-- [x] `javax.validation` → `jakarta.validation` 마이그레이션
-- [x] Java 17 이상 요구사항 반영
-- [x] Spring Boot 3.2.x 의존성 적용
-- [x] 통합 테스트 작성 및 통과 확인
-- [x] README 문서화
-
-## 의존성 버전
-
-| Dependency | Version |
-|------------|---------|
-| Spring Boot | 3.2.5 |
-| Spring Framework | 6.1.5 |
-| Hibernate | 6.4.4 |
-| Jakarta EE | 10.0.0 |
-| Java | 17 |
-| Lombok | 1.18.30 |
-| H2 Database | 2.2.224 |
\ No newline at end of file
+| Method | Path | 설명 |
+|--------|------|------|
+| POST | /api/v1/acquisitions | 카드 매입 요청 생성 |
+| GET | /api/v1/acquisitions/{id} | 매입 요청 조회 |
+| POST | /api/v1/acquisitions/{id}/approve | 매입 승인 처리 |
+| POST | /api/v1/acquisitions/{id}/reject | 매입 거절 처리 |
+| GET | /api/v1/acquisitions | 전체 매입 요청 목록 |
diff --git a/pom.xml b/pom.xml
index a61409e..c6cf793 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,16 +12,13 @@
com.acquirex
- card-acquisition-approval
+ proj-acquirex
1.0.0-SNAPSHOT
- card-acquisition-approval
- 카드 매입 승인 경로 - Spring Boot 3 Migration
+ proj-acquirex
+ 카드 매입 승인 경로 - Spring Boot 3 전환
17
- 17
- 17
- UTF-8
@@ -37,27 +34,11 @@
org.springframework.boot
spring-boot-starter-validation
-
- org.springframework.boot
- spring-boot-starter-actuator
-
-
- jakarta.platform
- jakarta.jakartaee-bom
- 10.0.0
- pom
- import
-
com.h2database
h2
runtime
-
- org.projectlombok
- lombok
- true
-
org.springframework.boot
spring-boot-starter-test
@@ -70,20 +51,7 @@
org.springframework.boot
spring-boot-maven-plugin
-
-
-
- org.projectlombok
- lombok
-
-
-
-
-
- org.apache.maven.plugins
- maven-surefire-plugin
- 3.2.5
-
\ No newline at end of file
+
diff --git a/src/main/java/com/acquirex/AcquirexApplication.java b/src/main/java/com/acquirex/AcquirexApplication.java
new file mode 100644
index 0000000..cbf43e9
--- /dev/null
+++ b/src/main/java/com/acquirex/AcquirexApplication.java
@@ -0,0 +1,11 @@
+package com.acquirex;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class AcquirexApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(AcquirexApplication.class, args);
+ }
+}
diff --git a/src/main/java/com/acquirex/approval/CardAcquisition.java b/src/main/java/com/acquirex/approval/CardAcquisition.java
new file mode 100644
index 0000000..798e8e7
--- /dev/null
+++ b/src/main/java/com/acquirex/approval/CardAcquisition.java
@@ -0,0 +1,94 @@
+package com.acquirex.approval;
+
+import jakarta.persistence.*;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 카드 매입 도메인 엔티티.
+ * Spring Boot 3 전환: javax.persistence → jakarta.persistence
+ */
+@Entity
+@Table(name = "card_acquisitions")
+public class CardAcquisition {
+
+ public enum ApprovalStatus {
+ PENDING, APPROVED, REJECTED, CANCELLED
+ }
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(name = "card_number", nullable = false, length = 20)
+ private String cardNumber;
+
+ @Column(name = "merchant_id", nullable = false, length = 20)
+ private String merchantId;
+
+ @Column(name = "amount", nullable = false, precision = 15, scale = 2)
+ private BigDecimal amount;
+
+ @Enumerated(EnumType.STRING)
+ @Column(name = "status", nullable = false, length = 20)
+ private ApprovalStatus status = ApprovalStatus.PENDING;
+
+ @Column(name = "created_at", nullable = false)
+ private LocalDateTime createdAt;
+
+ @Column(name = "updated_at")
+ private LocalDateTime updatedAt;
+
+ @Column(name = "approved_by", length = 100)
+ private String approvedBy;
+
+ @Column(name = "rejection_reason", length = 500)
+ private String rejectionReason;
+
+ public CardAcquisition() {
+ this.createdAt = LocalDateTime.now();
+ }
+
+ public CardAcquisition(String cardNumber, String merchantId, BigDecimal amount) {
+ this();
+ this.cardNumber = cardNumber;
+ this.merchantId = merchantId;
+ this.amount = amount;
+ }
+
+ public Long getId() { return id; }
+ public void setId(Long id) { this.id = id; }
+ public String getCardNumber() { return cardNumber; }
+ public void setCardNumber(String cardNumber) { this.cardNumber = cardNumber; }
+ public String getMerchantId() { return merchantId; }
+ public void setMerchantId(String merchantId) { this.merchantId = merchantId; }
+ public BigDecimal getAmount() { return amount; }
+ public void setAmount(BigDecimal amount) { this.amount = amount; }
+ public ApprovalStatus getStatus() { return status; }
+ public void setStatus(ApprovalStatus status) { this.status = status; }
+ public LocalDateTime getCreatedAt() { return createdAt; }
+ public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+ public LocalDateTime getUpdatedAt() { return updatedAt; }
+ public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
+ public String getApprovedBy() { return approvedBy; }
+ public void setApprovedBy(String approvedBy) { this.approvedBy = approvedBy; }
+ public String getRejectionReason() { return rejectionReason; }
+ public void setRejectionReason(String rejectionReason) { this.rejectionReason = rejectionReason; }
+
+ public void approve(String approver) {
+ this.status = ApprovalStatus.APPROVED;
+ this.approvedBy = approver;
+ this.updatedAt = LocalDateTime.now();
+ }
+
+ public void reject(String reason) {
+ this.status = ApprovalStatus.REJECTED;
+ this.rejectionReason = reason;
+ this.updatedAt = LocalDateTime.now();
+ }
+
+ public void cancel() {
+ this.status = ApprovalStatus.CANCELLED;
+ this.updatedAt = LocalDateTime.now();
+ }
+}
diff --git a/src/main/java/com/acquirex/approval/CardAcquisitionController.java b/src/main/java/com/acquirex/approval/CardAcquisitionController.java
new file mode 100644
index 0000000..8d2e92b
--- /dev/null
+++ b/src/main/java/com/acquirex/approval/CardAcquisitionController.java
@@ -0,0 +1,95 @@
+package com.acquirex.approval;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 카드 매입 승인 REST 컨트롤러.
+ * Spring Boot 3 전환: javax.validation → jakarta.validation
+ */
+@RestController
+@RequestMapping("/api/v1/acquisitions")
+public class CardAcquisitionController {
+
+ private final CardAcquisitionService service;
+
+ public CardAcquisitionController(CardAcquisitionService service) {
+ this.service = service;
+ }
+
+ @PostMapping
+ public ResponseEntity createAcquisition(
+ @Valid @RequestBody CreateAcquisitionRequest request) {
+ CardAcquisition acquisition = service.createAcquisition(
+ request.cardNumber(), request.merchantId(), request.amount());
+ return ResponseEntity.status(HttpStatus.CREATED)
+ .body(CardAcquisitionResponse.from(acquisition));
+ }
+
+ @GetMapping("/{id}")
+ public ResponseEntity getAcquisition(@PathVariable Long id) {
+ return service.findById(id)
+ .map(CardAcquisitionResponse::from)
+ .map(ResponseEntity::ok)
+ .orElse(ResponseEntity.notFound().build());
+ }
+
+ @GetMapping
+ public ResponseEntity> getAllAcquisitions(
+ @RequestParam(required = false) CardAcquisition.ApprovalStatus status) {
+ List acquisitions = (status != null)
+ ? service.findByStatus(status) : service.findAll();
+ return ResponseEntity.ok(acquisitions.stream().map(CardAcquisitionResponse::from).toList());
+ }
+
+ @PostMapping("/{id}/approve")
+ public ResponseEntity approveAcquisition(
+ @PathVariable Long id, @RequestBody ApproveRequest request) {
+ return ResponseEntity.ok(CardAcquisitionResponse.from(service.approve(id, request.approver())));
+ }
+
+ @PostMapping("/{id}/reject")
+ public ResponseEntity rejectAcquisition(
+ @PathVariable Long id, @RequestBody RejectRequest request) {
+ return ResponseEntity.ok(CardAcquisitionResponse.from(service.reject(id, request.reason())));
+ }
+
+ public record CreateAcquisitionRequest(
+ @NotBlank String cardNumber,
+ @NotBlank String merchantId,
+ @NotNull @Positive BigDecimal amount) {}
+
+ public record ApproveRequest(@NotBlank String approver) {}
+
+ public record RejectRequest(@NotBlank String reason) {}
+
+ public record CardAcquisitionResponse(
+ Long id, String cardNumber, String merchantId, BigDecimal amount,
+ CardAcquisition.ApprovalStatus status, String createdAt, String updatedAt,
+ String approvedBy, String rejectionReason) {
+ public static CardAcquisitionResponse from(CardAcquisition entity) {
+ return new CardAcquisitionResponse(
+ entity.getId(),
+ maskCardNumber(entity.getCardNumber()),
+ entity.getMerchantId(),
+ entity.getAmount(),
+ entity.getStatus(),
+ entity.getCreatedAt().toString(),
+ entity.getUpdatedAt() != null ? entity.getUpdatedAt().toString() : null,
+ entity.getApprovedBy(),
+ entity.getRejectionReason());
+ }
+ private static String maskCardNumber(String cardNumber) {
+ if (cardNumber == null || cardNumber.length() < 8) return "****";
+ return cardNumber.substring(0, 4) + "****" + cardNumber.substring(cardNumber.length() - 4);
+ }
+ }
+}
diff --git a/src/main/java/com/acquirex/approval/CardAcquisitionService.java b/src/main/java/com/acquirex/approval/CardAcquisitionService.java
new file mode 100644
index 0000000..fac8b83
--- /dev/null
+++ b/src/main/java/com/acquirex/approval/CardAcquisitionService.java
@@ -0,0 +1,86 @@
+package com.acquirex.approval;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * 카드 매입 서비스 + 리포지토리.
+ * Spring Boot 3 전환: jakarta.transaction.Transactional
+ */
+@Repository
+interface CardAcquisitionRepository extends JpaRepository {
+ List findByStatus(CardAcquisition.ApprovalStatus status);
+ List findByMerchantId(String merchantId);
+}
+
+@Service
+@Transactional
+public class CardAcquisitionService {
+
+ private final CardAcquisitionRepository repository;
+
+ public CardAcquisitionService(CardAcquisitionRepository repository) {
+ this.repository = repository;
+ }
+
+ public CardAcquisition createAcquisition(String cardNumber, String merchantId, BigDecimal amount) {
+ validateAmount(amount);
+ CardAcquisition acquisition = new CardAcquisition(cardNumber, merchantId, amount);
+ return repository.save(acquisition);
+ }
+
+ @Transactional(readOnly = true)
+ public Optional findById(Long id) {
+ return repository.findById(id);
+ }
+
+ @Transactional(readOnly = true)
+ public List findAll() {
+ return repository.findAll();
+ }
+
+ @Transactional(readOnly = true)
+ public List findByStatus(CardAcquisition.ApprovalStatus status) {
+ return repository.findByStatus(status);
+ }
+
+ public CardAcquisition approve(Long id, String approver) {
+ CardAcquisition acquisition = repository.findById(id)
+ .orElseThrow(() -> new IllegalArgumentException("매입 요청을 찾을 수 없습니다: " + id));
+ if (acquisition.getStatus() != CardAcquisition.ApprovalStatus.PENDING) {
+ throw new IllegalStateException("대기 상태의 요청만 승인할 수 있습니다.");
+ }
+ acquisition.approve(approver);
+ return repository.save(acquisition);
+ }
+
+ public CardAcquisition reject(Long id, String reason) {
+ CardAcquisition acquisition = repository.findById(id)
+ .orElseThrow(() -> new IllegalArgumentException("매입 요청을 찾을 수 없습니다: " + id));
+ if (acquisition.getStatus() != CardAcquisition.ApprovalStatus.PENDING) {
+ throw new IllegalStateException("대기 상태의 요청만 거절할 수 있습니다.");
+ }
+ acquisition.reject(reason);
+ return repository.save(acquisition);
+ }
+
+ public CardAcquisition cancel(Long id) {
+ CardAcquisition acquisition = repository.findById(id)
+ .orElseThrow(() -> new IllegalArgumentException("매입 요청을 찾을 수 없습니다: " + id));
+ if (acquisition.getStatus() == CardAcquisition.ApprovalStatus.APPROVED) {
+ throw new IllegalStateException("이미 승인된 요청은 취소할 수 없습니다.");
+ }
+ acquisition.cancel();
+ return repository.save(acquisition);
+ }
+
+ private void validateAmount(BigDecimal amount) {
+ if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new IllegalArgumentException("매입 금액은 0보다 커야 합니다.");
+ }
+ }
+}
diff --git a/src/main/java/com/acquirex/cardapproval/CardAcquisitionApprovalApplication.java b/src/main/java/com/acquirex/cardapproval/CardAcquisitionApprovalApplication.java
deleted file mode 100644
index 2c661cb..0000000
--- a/src/main/java/com/acquirex/cardapproval/CardAcquisitionApprovalApplication.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.acquirex.cardapproval;
-
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-
-/**
- * 카드 매입 승인 경로 메인 애플리케이션
- * Spring Boot 3.2.5 (Spring Framework 6.1.x) - Jakarta EE 10 호환
- */
-@SpringBootApplication
-public class CardAcquisitionApprovalApplication {
- public static void main(String[] args) {
- SpringApplication.run(CardAcquisitionApprovalApplication.class, args);
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/acquirex/cardapproval/controller/AcquisitionApprovalController.java b/src/main/java/com/acquirex/cardapproval/controller/AcquisitionApprovalController.java
deleted file mode 100644
index 89fb498..0000000
--- a/src/main/java/com/acquirex/cardapproval/controller/AcquisitionApprovalController.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package com.acquirex.cardapproval.controller;
-
-import com.acquirex.cardapproval.service.AcquisitionApprovalService;
-import com.acquirex.cardapproval.service.AcquisitionApprovalService.AcquisitionApprovalRequest;
-import com.acquirex.cardapproval.service.AcquisitionApprovalService.AcquisitionApprovalResponse;
-import jakarta.validation.Valid;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.format.annotation.DateTimeFormat;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.*;
-
-import java.time.LocalDateTime;
-import java.util.List;
-import java.util.Map;
-
-/**
- * 카드 매입 승인 REST 컨트롤러
- * Spring Boot 3.x Web (Jakarta Servlet)
- */
-@RestController
-@RequestMapping("/api/v1/acquisitions")
-@RequiredArgsConstructor
-@Slf4j
-public class AcquisitionApprovalController {
-
- private final AcquisitionApprovalService service;
-
- @PostMapping
- public ResponseEntity processAcquisition(
- @Valid @RequestBody AcquisitionApprovalRequest request) {
- log.info("매입 승인 요청 수신: {}", request.getApprovalNumber());
- AcquisitionApprovalResponse response = service.processAcquisition(request);
- return ResponseEntity.status(HttpStatus.CREATED).body(response);
- }
-
- @GetMapping("/{id}")
- public ResponseEntity getAcquisition(@PathVariable Long id) {
- log.info("매입 승인 조회: ID={}", id);
- return ResponseEntity.ok(service.getAcquisition(id));
- }
-
- @GetMapping("/merchant/{merchantId}")
- public ResponseEntity> getAcquisitionsByMerchant(
- @PathVariable String merchantId) {
- log.info("가맹점별 매입 승인 목록 조회: 가맹점={}", merchantId);
- return ResponseEntity.ok(service.getAcquisitionsByMerchant(merchantId));
- }
-
- @GetMapping("/period")
- public ResponseEntity> getAcquisitionsByPeriod(
- @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate,
- @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate) {
- log.info("기간별 매입 승인 목록 조회: {} ~ {}", startDate, endDate);
- return ResponseEntity.ok(service.getAcquisitionsByPeriod(startDate, endDate));
- }
-
- @PostMapping("/{id}/cancel")
- public ResponseEntity cancelAcquisition(
- @PathVariable Long id,
- @RequestBody Map cancelRequest) {
- log.info("매입 취소 요청: ID={}", id);
- String reason = cancelRequest.getOrDefault("reason", "고객 요청");
- return ResponseEntity.ok(service.cancelAcquisition(id, reason));
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/acquirex/cardapproval/domain/AcquisitionApproval.java b/src/main/java/com/acquirex/cardapproval/domain/AcquisitionApproval.java
deleted file mode 100644
index 69ff5e5..0000000
--- a/src/main/java/com/acquirex/cardapproval/domain/AcquisitionApproval.java
+++ /dev/null
@@ -1,114 +0,0 @@
-package com.acquirex.cardapproval.domain;
-
-import jakarta.persistence.*;
-import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.NotNull;
-import jakarta.validation.constraints.Positive;
-import lombok.*;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.Query;
-import org.springframework.data.repository.query.Param;
-import org.springframework.stereotype.Repository;
-
-import java.math.BigDecimal;
-import java.time.LocalDateTime;
-import java.util.List;
-import java.util.Optional;
-
-/**
- * 카드 매입 승인 도메인 엔티티
- * Jakarta Persistence API 3.1 (JPA 3.1) - Spring Boot 3.x
- */
-@Entity
-@Table(name = "acquisition_approvals")
-@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
-public class AcquisitionApproval {
-
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
-
- @NotBlank(message = "가맹점 번호는 필수입니다")
- @Column(name = "merchant_id", nullable = false, length = 20)
- private String merchantId;
-
- @NotBlank(message = "카드번호는 필수입니다")
- @Column(name = "card_number", nullable = false, length = 19)
- private String cardNumber;
-
- @NotNull @Positive
- @Column(name = "acquisition_amount", nullable = false, precision = 15, scale = 2)
- private BigDecimal acquisitionAmount;
-
- @NotNull @Positive
- @Column(name = "approval_amount", nullable = false, precision = 15, scale = 2)
- private BigDecimal approvalAmount;
-
- @NotBlank
- @Column(name = "approval_number", nullable = false, length = 12)
- private String approvalNumber;
-
- @NotNull
- @Column(name = "acquisition_datetime", nullable = false)
- private LocalDateTime acquisitionDatetime;
-
- @NotNull
- @Enumerated(EnumType.STRING)
- @Column(name = "approval_status", nullable = false, length = 20)
- private ApprovalStatus approvalStatus;
-
- @Column(name = "cancel_yn", length = 1)
- @Builder.Default
- private String cancelYn = "N";
-
- @Column(name = "cancel_datetime")
- private LocalDateTime cancelDatetime;
-
- @Column(name = "cancel_reason", length = 200)
- private String cancelReason;
-
- @Column(name = "created_at", nullable = false, updatable = false)
- private LocalDateTime createdAt;
-
- @Column(name = "updated_at")
- private LocalDateTime updatedAt;
-
- @PrePersist
- protected void onCreate() {
- createdAt = LocalDateTime.now();
- updatedAt = LocalDateTime.now();
- }
-
- @PreUpdate
- protected void onUpdate() {
- updatedAt = LocalDateTime.now();
- }
-
- public void cancel(String reason) {
- this.cancelYn = "Y";
- this.cancelDatetime = LocalDateTime.now();
- this.cancelReason = reason;
- this.approvalStatus = ApprovalStatus.CANCELLED;
- }
-
- public enum ApprovalStatus {
- PENDING, APPROVED, REJECTED, CANCELLED, COMPLETED
- }
-}
-
-/**
- * 카드 매입 승인 레포지토리
- * Spring Data JPA (Spring Boot 3.x)
- */
-@Repository
-interface AcquisitionApprovalRepository extends JpaRepository {
-
- List findByMerchantIdAndCancelYn(String merchantId, String cancelYn);
-
- Optional findByApprovalNumber(String approvalNumber);
-
- @Query("SELECT a FROM AcquisitionApproval a WHERE a.acquisitionDatetime BETWEEN :startDate AND :endDate")
- List findByAcquisitionDatetimeBetween(
- @Param("startDate") LocalDateTime startDate,
- @Param("endDate") LocalDateTime endDate);
-}
\ No newline at end of file
diff --git a/src/main/java/com/acquirex/cardapproval/service/AcquisitionApprovalService.java b/src/main/java/com/acquirex/cardapproval/service/AcquisitionApprovalService.java
deleted file mode 100644
index edc8503..0000000
--- a/src/main/java/com/acquirex/cardapproval/service/AcquisitionApprovalService.java
+++ /dev/null
@@ -1,215 +0,0 @@
-package com.acquirex.cardapproval.service;
-
-import com.acquirex.cardapproval.domain.AcquisitionApproval;
-import com.acquirex.cardapproval.domain.AcquisitionApproval.ApprovalStatus;
-import com.acquirex.cardapproval.domain.AcquisitionApprovalRepository;
-import jakarta.validation.Valid;
-import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.NotNull;
-import jakarta.validation.constraints.Positive;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-import org.springframework.validation.FieldError;
-import org.springframework.web.bind.MethodArgumentNotValidException;
-import org.springframework.web.bind.annotation.*;
-import org.springframework.web.bind.annotation.ExceptionHandler;
-import org.springframework.web.bind.annotation.RestControllerAdvice;
-
-import java.math.BigDecimal;
-import java.time.LocalDateTime;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-/**
- * 카드 매입 승인 서비스
- * Spring Boot 3.x 트랜잭션 관리
- */
-@Service
-@RequiredArgsConstructor
-@Slf4j
-public class AcquisitionApprovalService {
-
- private final AcquisitionApprovalRepository repository;
-
- @Transactional
- public AcquisitionApprovalResponse processAcquisition(AcquisitionApprovalRequest request) {
- log.info("매입 승인 요청 처리: 가맹점={}, 금액={}", request.getMerchantId(), request.getAcquisitionAmount());
-
- repository.findByApprovalNumber(request.getApprovalNumber())
- .ifPresent(existing -> {
- throw new AcquisitionApprovalException("DUPLICATE_APPROVAL", "이미 처리된 승인번호입니다");
- });
-
- if (request.getAcquisitionAmount().compareTo(request.getApprovalAmount()) != 0) {
- throw new AcquisitionApprovalException("AMOUNT_MISMATCH", "매입 금액과 승인 금액이 일치하지 않습니다");
- }
-
- AcquisitionApproval approval = AcquisitionApproval.builder()
- .merchantId(request.getMerchantId())
- .cardNumber(request.getCardNumber())
- .acquisitionAmount(request.getAcquisitionAmount())
- .approvalAmount(request.getApprovalAmount())
- .approvalNumber(request.getApprovalNumber())
- .acquisitionDatetime(request.getAcquisitionDatetime())
- .approvalStatus(ApprovalStatus.APPROVED)
- .cancelYn("N")
- .build();
-
- AcquisitionApproval saved = repository.save(approval);
- log.info("매입 승인 완료: ID={}, 상태={}", saved.getId(), saved.getApprovalStatus());
- return mapToResponse(saved);
- }
-
- @Transactional
- public AcquisitionApprovalResponse cancelAcquisition(Long id, String reason) {
- log.info("매입 취소 요청: ID={}, 사유={}", id, reason);
-
- AcquisitionApproval approval = repository.findById(id)
- .orElseThrow(() -> new AcquisitionApprovalException("NOT_FOUND", "매입 승인건을 찾을 수 없습니다: " + id));
-
- if ("Y".equals(approval.getCancelYn())) {
- throw new AcquisitionApprovalException("ALREADY_CANCELLED", "이미 취소된 건입니다");
- }
-
- approval.cancel(reason);
- AcquisitionApproval saved = repository.save(approval);
- log.info("매입 취소 완료: ID={}", saved.getId());
- return mapToResponse(saved);
- }
-
- @Transactional(readOnly = true)
- public AcquisitionApprovalResponse getAcquisition(Long id) {
- return repository.findById(id)
- .map(this::mapToResponse)
- .orElseThrow(() -> new AcquisitionApprovalException("NOT_FOUND", "매입 승인건을 찾을 수 없습니다: " + id));
- }
-
- @Transactional(readOnly = true)
- public List getAcquisitionsByMerchant(String merchantId) {
- return repository.findByMerchantIdAndCancelYn(merchantId, "N")
- .stream().map(this::mapToResponse).collect(Collectors.toList());
- }
-
- @Transactional(readOnly = true)
- public List getAcquisitionsByPeriod(LocalDateTime startDate, LocalDateTime endDate) {
- return repository.findByAcquisitionDatetimeBetween(startDate, endDate)
- .stream().map(this::mapToResponse).collect(Collectors.toList());
- }
-
- private AcquisitionApprovalResponse mapToResponse(AcquisitionApproval approval) {
- return AcquisitionApprovalResponse.builder()
- .id(approval.getId())
- .merchantId(approval.getMerchantId())
- .cardNumber(approval.getCardNumber())
- .acquisitionAmount(approval.getAcquisitionAmount())
- .approvalAmount(approval.getApprovalAmount())
- .approvalNumber(approval.getApprovalNumber())
- .acquisitionDatetime(approval.getAcquisitionDatetime())
- .approvalStatus(approval.getApprovalStatus())
- .cancelYn(approval.getCancelYn())
- .cancelDatetime(approval.getCancelDatetime())
- .cancelReason(approval.getCancelReason())
- .createdAt(approval.getCreatedAt())
- .updatedAt(approval.getUpdatedAt())
- .build();
- }
-
- // DTOs
- @Data @NoArgsConstructor @AllArgsConstructor @Builder
- public static class AcquisitionApprovalRequest {
- @NotBlank(message = "가맹점 번호는 필수입니다")
- private String merchantId;
- @NotBlank(message = "카드번호는 필수입니다")
- private String cardNumber;
- @NotNull @Positive
- private BigDecimal acquisitionAmount;
- @NotNull @Positive
- private BigDecimal approvalAmount;
- @NotBlank
- private String approvalNumber;
- @NotNull
- private LocalDateTime acquisitionDatetime;
- }
-
- @Data @NoArgsConstructor @AllArgsConstructor @Builder
- public static class AcquisitionApprovalResponse {
- private Long id;
- private String merchantId;
- private String cardNumber;
- private BigDecimal acquisitionAmount;
- private BigDecimal approvalAmount;
- private String approvalNumber;
- private LocalDateTime acquisitionDatetime;
- private ApprovalStatus approvalStatus;
- private String cancelYn;
- private LocalDateTime cancelDatetime;
- private String cancelReason;
- private LocalDateTime createdAt;
- private LocalDateTime updatedAt;
- }
-
- // Exception
- public static class AcquisitionApprovalException extends RuntimeException {
- private final String errorCode;
- public AcquisitionApprovalException(String errorCode, String message) {
- super(message);
- this.errorCode = errorCode;
- }
- }
-
- // Global Exception Handler
- @RestControllerAdvice
- @Slf4j
- public static class GlobalExceptionHandler {
- @ExceptionHandler(AcquisitionApprovalException.class)
- public ResponseEntity