Skip to content

Commit ec79400

Browse files
authored
Merge pull request #398 from PromptPlace/refactor/#384
Refactor/#384
2 parents 9cea3a3 + 8ded24a commit ec79400

16 files changed

+419
-285
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@
2020
"dependencies": {
2121
"@aws-sdk/client-s3": "^3.848.0",
2222
"@aws-sdk/s3-request-presigner": "^3.848.0",
23+
"@portone/server-sdk": "^0.19.0",
2324
"@prisma/client": "^6.14.0",
2425
"@types/express-session": "^1.18.2",
2526
"@types/passport-google-oauth20": "^2.0.16",
2627
"@types/passport-kakao": "^1.0.3",
2728
"axios": "^1.11.0",
2829
"bcrypt": "^6.0.0",
2930
"bcryptjs": "^3.0.2",
31+
"body-parser": "^2.2.2",
3032
"class-transformer": "^0.5.1",
3133
"class-validator": "^0.14.2",
3234
"cors": "^2.8.5",
@@ -58,6 +60,7 @@
5860
"@types/axios": "^0.14.4",
5961
"@types/bcrypt": "^6.0.0",
6062
"@types/bcryptjs": "^3.0.0",
63+
"@types/body-parser": "^1.19.6",
6164
"@types/cors": "^2.8.19",
6265
"@types/express": "^5.0.3",
6366
"@types/jsonwebtoken": "^9.0.10",

pnpm-lock.yaml

Lines changed: 28 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/purchases/controller/purchase.complete.controller.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,13 @@ import { PurchaseCompleteService } from '../services/purchase.complete.service';
55
export const PurchaseCompleteController = {
66
async completePurchase(req: Request, res: Response, next: NextFunction) {
77
try {
8-
console.log('🔥 요청 바디 확인:', req.body); // ← 여기에 로그 찍기
9-
console.log('🔥 Content-Type:', req.headers['content-type']); // ← 헤더도 확인
10-
118
const userId = (req.user as any).user_id;
129
const dto = req.body as Partial<PromptPurchaseCompleteRequestDTO>;
1310

14-
if (!dto || typeof dto.imp_uid !== 'string' || typeof dto.merchant_uid !== 'string') {
11+
if (!dto || typeof dto.paymentId !== 'string') {
1512
return res.status(400).json({
1613
error: 'BadRequest',
17-
message: 'imp_uid와 merchant_uid는 필수입니다.',
14+
message: 'paymentId는 필수입니다.',
1815
statusCode: 400,
1916
});
2017
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Request, Response, NextFunction } from 'express';
2+
import * as PortOne from '@portone/server-sdk';
3+
import { WebhookService } from '../services/purchase.webhook.service';
4+
5+
export const WebhookController = {
6+
async handleWebhook(req: Request, res: Response, next: NextFunction) {
7+
try {
8+
const webhookSecret = process.env.PORTONE_WEBHOOK_SECRET;
9+
if (!webhookSecret) {
10+
console.error('PORTONE_WEBHOOK_SECRET is not set');
11+
return res.status(500).send('Server Config Error');
12+
}
13+
14+
// 1. 웹훅 서명 검증
15+
const webhook = await PortOne.Webhook.verify(
16+
webhookSecret,
17+
req.body,
18+
req.headers as Record<string, string | string[] | undefined>
19+
);
20+
21+
// 2. 이벤트 타입별 처리 -> 현재는 결제 완료(Paid)만 처리
22+
if (webhook.type === 'Transaction.Paid') {
23+
const { paymentId, storeId } = webhook.data;
24+
await WebhookService.handleTransactionPaid(paymentId, storeId);
25+
} else if (webhook.type === 'Transaction.Cancelled') {
26+
console.log('[Webhook] Transaction Cancelled:', webhook.data.paymentId);
27+
}
28+
res.status(200).send('OK');
29+
} catch (err) {
30+
if (err instanceof PortOne.Webhook.WebhookVerificationError) {
31+
console.error('[Webhook] Signature Verification Failed');
32+
return res.status(400).send('Verification Failed');
33+
}
34+
console.error('[Webhook] Error:', err);
35+
res.status(500).send('Internal Server Error');
36+
}
37+
}
38+
};

src/purchases/dtos/purchase.complete.dto.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
export interface PromptPurchaseCompleteRequestDTO {
2-
imp_uid: string; // 포트원 결제 UID
3-
merchant_uid: string; // 주문번호
2+
paymentId: string;
43
}
54

65
export interface PromptPurchaseCompleteResponseDTO {

src/purchases/dtos/purchase.request.dto.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ export interface PromptPurchaseRequestDTO {
77
redirect_url: string;
88
custom_data: {
99
prompt_id: number;
10+
user_id: number;
1011
};
1112
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Prisma } from '@prisma/client';
2+
3+
type Tx = Prisma.TransactionClient;
4+
5+
export const PurchaseCompleteRepository = {
6+
createPurchaseTx(tx: Tx, data: {
7+
user_id: number;
8+
prompt_id: number;
9+
seller_id?: number;
10+
amount: number;
11+
is_free: false;
12+
}) {
13+
return tx.purchase.create({ data });
14+
},
15+
16+
createPaymentTx(tx: Tx, data: {
17+
purchase_id: number;
18+
merchant_uid: string;
19+
pg: 'kakaopay' | 'tosspay';
20+
status: 'Succeed' | 'Failed' | 'Pending';
21+
paymentId: string;
22+
}) {
23+
return tx.payment.create({
24+
data: {
25+
purchase: { connect: { purchase_id: data.purchase_id } },
26+
merchant_uid: data.merchant_uid,
27+
provider: data.pg,
28+
status: data.status,
29+
imp_uid: data.paymentId,
30+
},
31+
});
32+
},
33+
34+
upsertSettlementForPaymentTx(tx: Tx, input: {
35+
sellerId: number;
36+
paymentId: number;
37+
amount: number;
38+
fee: number;
39+
status: 'Succeed' | 'Failed' | 'Pending';
40+
}) {
41+
return tx.settlement.upsert({
42+
where: { payment_id: input.paymentId },
43+
create: {
44+
user_id: input.sellerId,
45+
payment_id: input.paymentId,
46+
amount: input.amount,
47+
fee: input.fee,
48+
status: input.status,
49+
},
50+
update: {
51+
amount: input.amount,
52+
fee: input.fee,
53+
status: input.status,
54+
},
55+
});
56+
}
57+
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import prisma from '../../config/prisma';
2+
3+
export const PurchaseRepository = {
4+
findSucceededByUser(userId: number) {
5+
return prisma.purchase.findMany({
6+
where: {
7+
user_id: userId,
8+
payment: { is: { status: 'Succeed'}},
9+
},
10+
include: {
11+
prompt: {
12+
select: {
13+
prompt_id: true,
14+
title: true,
15+
user: { select: { nickname: true}},
16+
},
17+
},
18+
payment: {
19+
select: {
20+
provider: true,
21+
},
22+
},
23+
},
24+
orderBy: { created_at: 'desc'},
25+
})
26+
}
27+
};

0 commit comments

Comments
 (0)