Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package app.payment.core.adapter.inbound

import app.payment.core.domain.Payment
import app.payment.core.domain.PaymentMethod
import app.payment.core.domain.PaymentStatus
import java.math.BigDecimal
import java.time.OffsetDateTime

data class GetPaymentResponse(
val id: Long,
val orderId: String,
val userId: Long,
val amount: BigDecimal,
val method: PaymentMethod,
val status: PaymentStatus,
val transactionId: String?,
val paidAt: OffsetDateTime?,
val createdAt: OffsetDateTime,
val updatedAt: OffsetDateTime
) {
companion object {
fun from(payment: Payment): GetPaymentResponse {
return GetPaymentResponse(
id = payment.id,
orderId = payment.orderId,
userId = payment.userId,
amount = payment.amount,
method = payment.method,
status = payment.status,
transactionId = payment.transactionId,
paidAt = payment.paidAt,
createdAt = payment.createdAt,
updatedAt = payment.updatedAt
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package app.payment.core.adapter.inbound

import app.payment.core.domain.Payment

data class GetPaymentsResponse(
val payments: List<GetPaymentResponse>,
val count: Int
) {
companion object {
fun from(payments: List<Payment>): GetPaymentsResponse {
return GetPaymentsResponse(
payments = payments.map { GetPaymentResponse.from(it) },
count = payments.size
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package app.payment.core.adapter.inbound

import app.payment.core.application.port.inbound.PaymentUseCase
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/api/payments")
class PaymentController(
private val paymentUseCase: PaymentUseCase
) {

@GetMapping("/{id}")
fun getPayment(@PathVariable id: Long): GetPaymentResponse {
val payment = paymentUseCase.getPayment(id)
return GetPaymentResponse.from(payment)
}
Comment on lines +12 to +16
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

404 에러 처리를 확인하세요.

존재하지 않는 결제 ID에 대한 요청 시 적절한 HTTP 상태 코드(404 Not Found)가 반환되는지 확인이 필요합니다. paymentUseCase.getPayment(id)에서 예외가 발생할 때 이를 처리하는 글로벌 exception handler가 구현되어 있는지 검증하세요.

다음 스크립트로 exception handler 구현을 확인하세요:


🏁 Script executed:

#!/bin/bash
# 글로벌 exception handler 또는 ControllerAdvice 확인
rg -nP --type=kt -C3 '@(RestControllerAdvice|ControllerAdvice|ExceptionHandler)'

Length of output: 26


🏁 Script executed:

#!/bin/bash
# 글로벌 exception handler 검색 - 코틀린 파일에서
rg -nP '(RestControllerAdvice|ControllerAdvice|ExceptionHandler)' --type-list | head -20

Length of output: 389


🏁 Script executed:

#!/bin/bash
# 코틀린 파일 확장자로 exception handler 검색
rg -n '(RestControllerAdvice|ControllerAdvice|ExceptionHandler)' -g '*.kt'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# PaymentUseCase 인터페이스와 구현 검색
rg -n 'fun getPayment' -g '*.kt' -A 5 -B 2

Length of output: 2406


🏁 Script executed:

#!/bin/bash
# Exception 클래스 정의 검색 (PaymentNotFound, NotFound 등)
rg -n '(Exception|class.*Exception|EntityNotFoundException)' -g '*.kt' -B 1 | head -50

Length of output: 2968


글로벌 exception handler를 구현하여 404 응답을 처리하세요.

검증 결과, 존재하지 않는 결제 ID 요청 시 404 에러가 제대로 처리되지 않고 있습니다.

  • 현재 paymentUseCase.getPayment(id) (PaymentService.kt:15-17)는 결제를 찾지 못할 때 NoSuchElementException을 발생시킵니다.
  • 코드베이스에 @RestControllerAdvice, @ControllerAdvice, @ExceptionHandler가 구현되어 있지 않습니다.
  • 컨트롤러에서 예외를 처리하지 않으면 500 에러가 반환됩니다.

@RestControllerAdvice를 통해 NoSuchElementException을 HTTP 404 상태 코드로 매핑하는 글로벌 exception handler를 구현하세요.

🤖 Prompt for AI Agents
In
payment/payment-core/adapter/src/main/kotlin/app/payment/core/adapter/inbound/PaymentController.kt
around lines 12-16: requests for non-existent payment currently cause a
NoSuchElementException in the service and result in a 500 because there is no
global exception handler; add a new Kotlin class (e.g.,
GlobalExceptionHandler.kt) in the same inbound package (or a common config
package) annotated with @RestControllerAdvice and implement an
@ExceptionHandler(NoSuchElementException::class) method that returns a
ResponseEntity with HTTP 404 (NOT_FOUND) and a minimal error body or message;
ensure the handler logs the exception and maps NoSuchElementException
consistently to 404 so the controller no longer returns 500 for missing
payments.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package app.payment.core.adapter.outbound

import app.payment.core.domain.Payment
import app.payment.core.domain.PaymentMethod
import app.payment.core.domain.PaymentStatus
import app.payment.core.domain.PgProvider
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.Table
import java.math.BigDecimal
import java.time.OffsetDateTime

@Entity
@Table(name = "payments")
class PaymentJpaEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,

@Column(name = "order_id", nullable = false, unique = true)
val orderId: String,

@Column(name = "user_id", nullable = false)
val userId: Long,

@Column(nullable = false, precision = 19, scale = 2)
val amount: BigDecimal,

@Enumerated(EnumType.STRING)
@Column(nullable = false)
val method: PaymentMethod,

@Enumerated(EnumType.STRING)
@Column(nullable = false)
val pgProvider: PgProvider,

@Enumerated(EnumType.STRING)
@Column(nullable = false)
val status: PaymentStatus,

@Column(name = "transaction_id")
val transactionId: String? = null,

@Column(name = "pg_transaction_id")
val pgTransactionId: String? = null,

@Column(name = "paid_at")
val paidAt: OffsetDateTime? = null,

@Column(name = "created_at", nullable = false)
val createdAt: OffsetDateTime = OffsetDateTime.now(),

@Column(name = "updated_at", nullable = false)
val updatedAt: OffsetDateTime = OffsetDateTime.now()
) {
fun toDomain(): Payment {
return Payment(
id = id,
orderId = orderId,
userId = userId,
amount = amount,
method = method,
pgProvider = pgProvider,
status = status,
transactionId = transactionId,
pgTransactionId = pgTransactionId,
paidAt = paidAt,
createdAt = createdAt,
updatedAt = updatedAt
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package app.payment.core.adapter.outbound

import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query

interface PaymentJpaRepository : JpaRepository<PaymentJpaEntity, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package app.payment.core.adapter.outbound

import app.payment.core.application.port.outbound.PaymentReader
import app.payment.core.domain.Payment
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Component

@Component
class PaymentReaderImpl(
private val paymentJpaRepository: PaymentJpaRepository
) : PaymentReader {

override fun findById(id: Long): Payment? {
return paymentJpaRepository.findByIdOrNull(id)?.toDomain()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package app.payment.core.application.port.inbound

import app.payment.core.domain.Payment

interface PaymentUseCase {
fun getPayment(paymentId: Long): Payment
}
Comment on lines +5 to +7
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

약간 네이밍의 문제같긴 한데, PaymentUseCase/PaymentService는 여기에 관련 기능을 몰아서 넣을지 궁금합니다. 만약 아니라면 Find 등의 행위가 나타나면 좋을것같아요/ 저번 Voucher에 제가 이부분에 대해 언급을 안했나 보네요..

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package app.payment.core.application.port.outbound

import app.payment.core.domain.Payment

interface PaymentReader {
fun findById(id: Long): Payment?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package app.payment.core.application.service

import app.payment.core.application.port.inbound.PaymentUseCase
import app.payment.core.application.port.outbound.PaymentReader
import app.payment.core.domain.Payment
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
@Transactional(readOnly = true)
class PaymentService(
private val paymentReader: PaymentReader
) : PaymentUseCase {

override fun getPayment(paymentId: Long): Payment {
return paymentReader.findById(paymentId)
?: throw NoSuchElementException("Payment not found with id: $paymentId")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package app.payment.core.domain

import java.math.BigDecimal
import java.time.OffsetDateTime

class Payment(
val id: Long,
val orderId: String,
val userId: Long,
val amount: BigDecimal,
val method: PaymentMethod,
val pgProvider: PgProvider,
val status: PaymentStatus,
val transactionId: String?,
val pgTransactionId: String?,
val paidAt: OffsetDateTime?,
val createdAt: OffsetDateTime,
val updatedAt: OffsetDateTime
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package app.payment.core.domain

enum class PaymentMethod {
CREDIT_CARD,
BANK_TRANSFER,
VIRTUAL_ACCOUNT,
MOBILE,
EASY_PAY,
GIFT_CERTIFICATE,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package app.payment.core.domain

enum class PaymentStatus {
READY,
PAY_PENDING,
PAID,
PARTIAL_CANCELLED,
CANCELLED,
FAILED,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package app.payment.core.domain

enum class PgProvider {
TOSS_PAYMENTS,
KAKAO_PAY,
}