diff --git a/README.md b/README.md
index 9802e91..7129e1b 100644
--- a/README.md
+++ b/README.md
@@ -1,79 +1,28 @@
-# proj-acquirex - Card Acquisition Approval System
+# 카드 매입 승인 경로 Spring Boot 3 전환
-## Spring Boot 3 Migration
+## 전환 범위
-### Migration Summary
+| 항목 | 기존 | 전환 후 |
+|------|------|----------|
+| 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 |
-| Component | Before (SB 2.x) | After (SB 3.x) |
-|-----------|-----------------|----------------|
-| Java Version | 8/11 | 17 |
-| EE Namespace | `javax.*` | `jakarta.*` |
-| Validation | javax.validation | jakarta.validation |
-| Spring Boot | 2.7.x | 3.2.5 |
-
-### Migration Scope
-
-- **CardAcquisitionApprovalService**: Core approval business logic
-- **CardAcquisitionApprovalController**: REST API endpoint (`/api/v1/approvals`)
-- **ApprovalRequest/ApprovalResponse**: DTOs with Jakarta Validation
-- **Domain entities**: CardAcquisitionApproval, ApprovalStatus
-
-### Key Changes
-
-1. **Package Migration**: All `javax.*` imports replaced with `jakarta.*`
-2. **Validation Annotations**: `@NotNull`, `@NotBlank`, `@DecimalMin` use `jakarta.validation`
-3. **Java 17 Features**: Switch expressions, records-ready structure
-
-### Verification Procedures
+## 검증 절차
```bash
-# Build and test
-mvn clean verify
-
-# Run application
-mvn spring-boot:run
-
-# Test endpoint
-curl -X POST http://localhost:8080/api/v1/approvals \
- -H "Content-Type: application/json" \
- -d '{"requestId":"REQ-001","cardNumber":"4111111111111111","merchantId":"MERCHANT-001","amount":100.00}'
+./mvnw clean verify
```
-### Test Coverage (15 tests)
+## API 엔드포인트
-| Category | Tests | Description |
-|----------|-------|-------------|
-| Service: processApproval | 3 | Approval, pending, rejection logic |
-| Service: validateRequest | 2 | Card number, merchant ID validation |
-| Service: determineApprovalStatus | 3 | Status determination by amount |
-| Service: maskCardNumber | 3 | Card number masking |
-| Controller | 4 | HTTP 200/202/400 responses |
-
-### API Specification
-
-**POST /api/v1/approvals**
-
-Request:
-```json
-{"requestId":"REQ-001","cardNumber":"4111111111111111","merchantId":"MERCHANT-001","amount":100.00}
-```
-
-Response (200 OK):
-```json
-{"id":1,"requestId":"REQ-001","status":"APPROVED","amount":100.00,"processedAt":"...","message":"Card acquisition approved successfully"}
-```
-
-### Dependencies
-
-| Dependency | Version | Purpose |
-|------------|---------|---------|
-| spring-boot-starter-web | 3.2.5 | REST API |
-| spring-boot-starter-validation | 3.2.5 | Jakarta Validation |
-| spring-boot-starter-test | 3.2.5 | JUnit 5, Mockito |
-
-### Build Status
-
-- Java 17 required
-- Spring Boot 3.2.5
-- Jakarta EE 9+ compatible
-- All 15 tests passing
+| 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 6b86229..c6cf793 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,7 +1,7 @@
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
@@ -14,15 +14,11 @@
com.acquirex
proj-acquirex
1.0.0-SNAPSHOT
- jar
proj-acquirex
- Card Acquisition Approval System - Spring Boot 3 Migration
+ 카드 매입 승인 경로 - Spring Boot 3 전환
17
- 17
- 17
- UTF-8
@@ -30,10 +26,19 @@
org.springframework.boot
spring-boot-starter-web
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
org.springframework.boot
spring-boot-starter-validation
+
+ com.h2database
+ h2
+ runtime
+
org.springframework.boot
spring-boot-starter-test
@@ -47,11 +52,6 @@
org.springframework.boot
spring-boot-maven-plugin
-
- 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
index 2fe1b0c..cbf43e9 100644
--- a/src/main/java/com/acquirex/AcquirexApplication.java
+++ b/src/main/java/com/acquirex/AcquirexApplication.java
@@ -3,13 +3,9 @@ package com.acquirex;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
-/**
- * Card Acquisition Approval System - Spring Boot 3 Application.
- * Migrated from Spring Boot 2.x (javax) to Spring Boot 3.x (jakarta).
- */
@SpringBootApplication
public class AcquirexApplication {
public static void main(String[] args) {
SpringApplication.run(AcquirexApplication.class, args);
}
-}
\ No newline at end of file
+}
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/approval/controller/CardAcquisitionApprovalController.java b/src/main/java/com/acquirex/approval/controller/CardAcquisitionApprovalController.java
deleted file mode 100644
index b5bed44..0000000
--- a/src/main/java/com/acquirex/approval/controller/CardAcquisitionApprovalController.java
+++ /dev/null
@@ -1,87 +0,0 @@
-package com.acquirex.approval.controller;
-
-import com.acquirex.approval.domain.ApprovalStatus;
-import com.acquirex.approval.dto.ApprovalRequest;
-import com.acquirex.approval.dto.ApprovalResponse;
-import com.acquirex.approval.service.CardAcquisitionApprovalService;
-import jakarta.validation.Valid;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.MethodArgumentNotValidException;
-import org.springframework.web.bind.annotation.*;
-
-import java.time.LocalDateTime;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * REST controller for card acquisition approval endpoints.
- * Migrated to Spring Boot 3 with Jakarta EE 9+ (jakarta.*) annotations.
- */
-@RestController
-@RequestMapping("/api/v1/approvals")
-public class CardAcquisitionApprovalController {
-
- private static final Logger log = LoggerFactory.getLogger(CardAcquisitionApprovalController.class);
- private final CardAcquisitionApprovalService approvalService;
-
- public CardAcquisitionApprovalController(CardAcquisitionApprovalService approvalService) {
- this.approvalService = approvalService;
- }
-
- /**
- * Process a card acquisition approval request.
- * @param request the approval request with validation
- * @return the approval response
- */
- @PostMapping
- public ResponseEntity processApproval(@Valid @RequestBody ApprovalRequest request) {
- log.info("Received approval request: {}", request.getRequestId());
- ApprovalResponse response = approvalService.processApproval(request);
- return ResponseEntity.status(mapToHttpStatus(response.getStatus())).body(response);
- }
-
- /**
- * Handle validation errors from @Valid annotations.
- */
- @ExceptionHandler(MethodArgumentNotValidException.class)
- public ResponseEntity