Skip to content

Commit e7e266b

Browse files
authored
Merge pull request #404 from PromptPlace/fix/#403
Fix/#403
2 parents 908b0f7 + 63cdb3e commit e7e266b

File tree

13 files changed

+190
-120
lines changed

13 files changed

+190
-120
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
Warnings:
3+
4+
- The values [kakaopay,tosspay] on the enum `Payment_provider` will be removed. If these variants are still used in the database, this will fail.
5+
6+
*/
7+
-- AlterTable
8+
ALTER TABLE `Payment` ADD COLUMN `cash_receipt_type` VARCHAR(191) NULL,
9+
ADD COLUMN `cash_receipt_url` VARCHAR(191) NULL,
10+
ADD COLUMN `method` ENUM('CARD', 'VIRTUAL_ACCOUNT', 'TRANSFER', 'MOBILE', 'EASY_PAY') NOT NULL DEFAULT 'CARD',
11+
MODIFY `provider` ENUM('TOSSPAYMENTS', 'KAKAOPAY', 'NAVERPAY', 'TOSSPAY', 'SAMSUNGPAY', 'APPLEPAY', 'LPAY', 'PAYCO', 'SSG', 'PINPAY') NOT NULL;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE `Payment` ALTER COLUMN `method` DROP DEFAULT;

prisma/schema.prisma

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -392,17 +392,44 @@ model Purchase {
392392
@@index([user_id], map: "Purchase_user_id_fkey")
393393
}
394394

395+
// 결제 수단
396+
enum PaymentMethod {
397+
CARD // 카드
398+
VIRTUAL_ACCOUNT // 가상계좌
399+
TRANSFER // 계좌이체
400+
MOBILE // 휴대폰
401+
EASY_PAY // 간편결제
402+
}
403+
404+
// 구체적인 PG사 또는 간편결제사
405+
enum PaymentProvider {
406+
TOSSPAYMENTS // 일반 토스 PG (카드, 가상계좌 등)
407+
KAKAOPAY // 카카오페이
408+
NAVERPAY // 네이버페이
409+
TOSSPAY // 토스페이
410+
SAMSUNGPAY // 삼성페이
411+
APPLEPAY // 애플페이
412+
LPAY // 엘페이
413+
PAYCO // 페이코
414+
SSG // SSG페이
415+
PINPAY // 핀페이
416+
}
417+
395418
model Payment {
396419
payment_id Int @id @default(autoincrement())
397420
purchase_id Int @unique
398421
status Status
399-
provider Payment_provider
422+
method PaymentMethod
423+
provider PaymentProvider
400424
merchant_uid String @unique
401425
created_at DateTime @default(now())
402426
updated_at DateTime @updatedAt
403427
imp_uid String @unique
404428
purchase Purchase @relation(fields: [purchase_id], references: [purchase_id], onDelete: Cascade)
405429
settlement Settlement?
430+
// 현금영수증 정보 (가상계좌/계좌이체 시)
431+
cash_receipt_url String? // 영수증 조회 URL
432+
cash_receipt_type String? // 소득공제(DEDUCTION), 지출증빙(PROOF) 등
406433
}
407434

408435
model Settlement {
@@ -588,11 +615,6 @@ enum NotificationType {
588615
ADMIN_MESSAGE
589616
}
590617

591-
enum Payment_provider {
592-
kakaopay
593-
tosspay
594-
}
595-
596618
enum userStatus {
597619
active
598620
banned

src/purchases/dtos/purchase.dto.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import { PaymentProvider } from "@prisma/client";
2+
13
export interface PurchaseHistoryItemDTO {
24
prompt_id: number;
35
title: string;
46
price: number;
57
seller_nickname: string;
6-
pg: 'kakaopay' | 'tosspay';
8+
pg: PaymentProvider | null;
79
}
810

911
export interface PurchaseHistoryResponseDTO {
Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,3 @@
11
export interface PromptPurchaseRequestDTO {
22
prompt_id: number;
3-
pg: 'kakaopay' | 'tosspay'; // 결제 수단
4-
merchant_uid: string; // 고유 주문번호
5-
amount: number;
6-
buyer_name: string;
7-
redirect_url: string;
8-
custom_data: {
9-
prompt_id: number;
10-
user_id: number;
11-
};
123
}

src/purchases/repositories/purchase.complete.repository.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { Prisma } from '@prisma/client';
1+
import { Prisma, PaymentMethod, PaymentProvider, Status } from '@prisma/client';
22

33
type Tx = Prisma.TransactionClient;
44

55
export const PurchaseCompleteRepository = {
6-
createPurchaseTx(tx: Tx, data: {
6+
createPurchaseTx(tx: Tx, data: {
77
user_id: number;
88
prompt_id: number;
99
seller_id?: number;
@@ -16,17 +16,23 @@ export const PurchaseCompleteRepository = {
1616
createPaymentTx(tx: Tx, data: {
1717
purchase_id: number;
1818
merchant_uid: string;
19-
pg: 'kakaopay' | 'tosspay';
20-
status: 'Succeed' | 'Failed' | 'Pending';
19+
method: PaymentMethod;
20+
provider: PaymentProvider;
21+
status: Status;
2122
paymentId: string;
23+
cash_receipt_url?: string | null;
24+
cash_receipt_type?: string | null;
2225
}) {
2326
return tx.payment.create({
2427
data: {
2528
purchase: { connect: { purchase_id: data.purchase_id } },
2629
merchant_uid: data.merchant_uid,
27-
provider: data.pg,
28-
status: data.status,
2930
imp_uid: data.paymentId,
31+
method: data.method,
32+
provider: data.provider,
33+
status: data.status,
34+
cash_receipt_url: data.cash_receipt_url,
35+
cash_receipt_type: data.cash_receipt_type,
3036
},
3137
});
3238
},
@@ -36,7 +42,7 @@ export const PurchaseCompleteRepository = {
3642
paymentId: number;
3743
amount: number;
3844
fee: number;
39-
status: 'Succeed' | 'Failed' | 'Pending';
45+
status: Status;
4046
}) {
4147
return tx.settlement.upsert({
4248
where: { payment_id: input.paymentId },

src/purchases/routes/purchase.request.route.ts

Lines changed: 27 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ const router = Router();
1717
* @swagger
1818
* /api/prompts/purchases/requests:
1919
* post:
20-
* summary: 결제 요청 생성
21-
* description: 결제 시작을 위한 요청을 생성합니다.
20+
* summary: 결제 요청 생성 (사전 검증)
21+
* description: 프론트엔드에서 결제창을 띄우기 전, 주문 번호 생성 및 사전 검증을 수행합니다.
2222
* tags: [Purchase]
2323
* security:
2424
* - jwt: []
@@ -28,55 +28,48 @@ const router = Router();
2828
* application/json:
2929
* schema:
3030
* type: object
31+
* required:
32+
* - prompt_id
33+
* - merchant_uid
34+
* - amount
35+
* - buyer_name
36+
* - redirect_url
3137
* properties:
3238
* prompt_id:
3339
* type: integer
34-
* pg:
35-
* type: string
36-
* enum: [kakaopay, tosspayments]
3740
* merchant_uid:
3841
* type: string
42+
* description: 가맹점 주문 번호
3943
* amount:
4044
* type: integer
45+
* description: 결제 예정 금액
4146
* buyer_name:
4247
* type: string
4348
* redirect_url:
4449
* type: string
4550
* responses:
4651
* 200:
47-
* description: 결제 요청 생성 성공
52+
* description: 요청 성공
4853
* content:
4954
* application/json:
5055
* schema:
5156
* type: object
5257
* properties:
5358
* message:
5459
* type: string
55-
* payment_gateway:
56-
* type: string
5760
* merchant_uid:
5861
* type: string
59-
* redirect_url:
60-
* type: string
6162
* statusCode:
6263
* type: integer
63-
* 400:
64-
* description: 잘못된 요청
65-
* 401:
66-
* description: 인증 실패
67-
* 404:
68-
* description: 리소스 없음
69-
* 409:
70-
* description: 중복/상태 충돌
7164
*/
7265
router.post('/requests', authenticateJwt, PurchaseRequestController.requestPurchase);
7366

7467
/**
7568
* @swagger
7669
* /api/prompts/purchases/complete:
7770
* post:
78-
* summary: 결제 완료 처리(Webhook/리다이렉트 후 서버 검증)
79-
* description: 포트원 imp_uid 기반으로 서버에서 결제 검증 후 구매/결제/정산을 기록합니다.
71+
* summary: 결제 완료 처리 (검증 및 저장)
72+
* description: 포트원 결제 완료 후, paymentId를 서버로 보내 검증하고 구매를 확정합니다.
8073
* tags: [Purchase]
8174
* security:
8275
* - jwt: []
@@ -86,14 +79,18 @@ router.post('/requests', authenticateJwt, PurchaseRequestController.requestPurch
8679
* application/json:
8780
* schema:
8881
* type: object
82+
* required:
83+
* - paymentId
8984
* properties:
90-
* imp_uid:
85+
* paymentId:
9186
* type: string
87+
* description: 포트원 V2 결제 ID
9288
* merchant_uid:
9389
* type: string
90+
* description: 가맹점 주문 번호
9491
* responses:
9592
* 200:
96-
* description: 결제 완료 처리 성공
93+
* description: 결제 성공 및 저장 완료
9794
* content:
9895
* application/json:
9996
* schema:
@@ -106,54 +103,23 @@ router.post('/requests', authenticateJwt, PurchaseRequestController.requestPurch
106103
* enum: [Succeed, Failed, Pending]
107104
* purchase_id:
108105
* type: integer
109-
* nullable: true
110106
* statusCode:
111107
* type: integer
112-
* 400:
113-
* description: 검증 실패/유효하지 않은 요청
114-
* 401:
115-
* description: 인증 실패
116-
* 404:
117-
* description: 리소스 없음
118-
* 409:
119-
* description: 충돌
120-
* 500:
121-
* description: 서버 오류
122108
*/
123109
router.post('/complete', authenticateJwt, PurchaseCompleteController.completePurchase);
124110

125111
/**
126112
* @swagger
127113
* /api/prompts/purchases:
128114
* get:
129-
* summary: 결제 내역 조회
130-
* description: 인증된 사용자의 결제(구매) 내역을 조회합니다.
115+
* summary: 결제 내역 조회
116+
* description: 인증된 사용자의 결제 내역을 최신순으로 조회합니다.
131117
* tags: [Purchase]
132118
* security:
133119
* - jwt: []
134-
* parameters:
135-
* - in: query
136-
* name: page
137-
* schema:
138-
* type: integer
139-
* required: false
140-
* description: 페이지 번호 (옵션)
141-
* - in: query
142-
* name: pageSize
143-
* schema:
144-
* type: integer
145-
* required: false
146-
* description: 페이지 크기 (옵션)
147-
* - in: query
148-
* name: status
149-
* schema:
150-
* type: string
151-
* enum: [Succeed, Failed, Pending]
152-
* required: false
153-
* description: 결제 상태 필터 (옵션)
154120
* responses:
155121
* 200:
156-
* description: 결제 내역 조회 성공
122+
* description: 조회 성공
157123
* content:
158124
* application/json:
159125
* schema:
@@ -172,21 +138,18 @@ router.post('/complete', authenticateJwt, PurchaseCompleteController.completePur
172138
* type: string
173139
* price:
174140
* type: integer
141+
* seller_nickname:
142+
* type: string
175143
* purchased_at:
176144
* type: string
177145
* format: date-time
178-
* seller_nickname:
179-
* type: string
180-
* nullable: true
181146
* pg:
182147
* type: string
183-
* enum: [kakaopay, tosspay, null]
148+
* description: 결제 제공자 (DB Enum)
149+
* enum: [TOSSPAYMENTS, KAKAOPAY, TOSSPAY, NAVERPAY, SAMSUNGPAY, APPLEPAY, LPAY, PAYCO, SSG, PINPAY]
150+
* nullable: true
184151
* statusCode:
185152
* type: integer
186-
* 401:
187-
* description: 인증 실패
188-
* 500:
189-
* description: 서버 오류
190153
*/
191154
router.get('/', authenticateJwt, PurchaseHistoryController.list);
192155

src/purchases/services/purchase.complete.service.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { PurchaseCompleteRepository } from '../repositories/purchase.complete.re
44
import { AppError } from '../../errors/AppError';
55
import prisma from '../../config/prisma';
66
import { fetchAndVerifyPortonePayment } from '../utils/portone';
7-
import { mapPgProvider } from '../utils/payment.util';
87

98
export const PurchaseCompleteService = {
109
async completePurchase(userId: number, dto: PromptPurchaseCompleteRequestDTO): Promise<PromptPurchaseCompleteResponseDTO> {
@@ -32,9 +31,6 @@ export const PurchaseCompleteService = {
3231
throw new AppError('이미 구매한 프롬프트입니다.', 409, 'AlreadyPurchased');
3332
}
3433

35-
// 5. 트랜잭션 처리
36-
const pgProvider = mapPgProvider(verifiedPayment.method_provider);
37-
3834
const { purchase_id } = await prisma.$transaction(async (tx) => {
3935
// 구매 기록 생성
4036
const purchase = await PurchaseCompleteRepository.createPurchaseTx(tx, {
@@ -49,9 +45,12 @@ export const PurchaseCompleteService = {
4945
const payment = await PurchaseCompleteRepository.createPaymentTx(tx, {
5046
purchase_id: purchase.purchase_id,
5147
merchant_uid: paymentId,
52-
pg: pgProvider,
48+
paymentId: paymentId,
5349
status: 'Succeed',
54-
paymentId: paymentId
50+
method: verifiedPayment.method,
51+
provider: verifiedPayment.provider,
52+
cash_receipt_url: verifiedPayment.cashReceipt?.url,
53+
cash_receipt_type: verifiedPayment.cashReceipt?.type,
5554
});
5655

5756
// 정산 데이터 생성

src/purchases/services/purchase.request.service.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { v4 as uuidv4 } from 'uuid';
12
import { PromptPurchaseRequestDTO } from '../dtos/purchase.request.dto';
23
import { PurchaseRequestRepository } from '../repositories/purchase.request.repository';
34
import { AppError } from '../../errors/AppError';
@@ -12,16 +13,18 @@ export const PurchaseRequestService = {
1213
const existing = await PurchaseRequestRepository.findExistingPurchase(userId, dto.prompt_id);
1314
if (existing) throw new AppError('이미 구매한 프롬프트입니다.', 409, 'AlreadyPurchased');
1415

16+
const paymentId = `payment-${uuidv4()}`;
17+
1518
return {
16-
message: '결제 요청이 정상 처리되었습니다.',
17-
payment_gateway: dto.pg,
18-
merchant_uid: dto.merchant_uid,
19-
redirect_url: dto.redirect_url, // 클라이언트가 넘긴 값 사용
20-
custom_data: {
19+
message: '주문서가 생성되었습니다.',
20+
statusCode: 200,
21+
merchant_uid: paymentId,
22+
amount: prompt.price,
23+
prompt_title: prompt.title,
24+
custom_data: {
2125
prompt_id: dto.prompt_id,
2226
user_id: userId,
2327
},
24-
statusCode: 200,
2528
};
2629
},
2730
};

0 commit comments

Comments
 (0)