Skip to content
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.848.0",
"@aws-sdk/s3-request-presigner": "^3.848.0",
"@portone/server-sdk": "^0.19.0",
"@prisma/client": "^6.14.0",
"@types/express-session": "^1.18.2",
"@types/passport-google-oauth20": "^2.0.16",
Expand All @@ -44,6 +43,7 @@
"passport-jwt": "^4.0.1",
"passport-kakao": "^1.0.1",
"passport-naver-v2": "^2.0.8",
"redis": "^5.11.0",
"reflect-metadata": "^0.2.2",
"save-dev": "0.0.1-security",
"socket.io": "^4.8.3",
Expand Down
84 changes: 76 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE `SettlementAccount` ADD COLUMN `birth_date` VARCHAR(10) NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-- Migrate Payment model from PortOne (merchant_uid/imp_uid) to Payple (pcd_pay_oid/pcd_pay_reqkey)
-- Note: existing Payment rows are PortOne test data only (no real transactions yet).

-- DropIndex
DROP INDEX `Payment_merchant_uid_key` ON `Payment`;
DROP INDEX `Payment_imp_uid_key` ON `Payment`;

-- AlterTable
ALTER TABLE `Payment`
DROP COLUMN `merchant_uid`,
DROP COLUMN `imp_uid`,
DROP COLUMN `cash_receipt_type`,
ADD COLUMN `pcd_pay_oid` VARCHAR(191) NOT NULL,
ADD COLUMN `pcd_pay_reqkey` VARCHAR(191) NOT NULL,
ADD COLUMN `pay_type` VARCHAR(191) NULL,
ADD COLUMN `card_name` VARCHAR(191) NULL;

-- CreateIndex
CREATE UNIQUE INDEX `Payment_pcd_pay_oid_key` ON `Payment`(`pcd_pay_oid`);
CREATE UNIQUE INDEX `Payment_pcd_pay_reqkey_key` ON `Payment`(`pcd_pay_reqkey`);
25 changes: 13 additions & 12 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -413,18 +413,18 @@ enum PaymentProvider {
}

model Payment {
payment_id Int @id @default(autoincrement())
purchase_id Int @unique
status Status
merchant_uid String @unique
created_at DateTime @default(now())
updated_at DateTime @updatedAt
imp_uid String @unique
purchase Purchase @relation(fields: [purchase_id], references: [purchase_id], onDelete: Cascade)
settlement Settlement?
// ν˜„κΈˆμ˜μˆ˜μ¦ 정보 (κ°€μƒκ³„μ’Œ/κ³„μ’Œμ΄μ²΄ μ‹œ)
cash_receipt_url String? // 영수증 쑰회 URL
cash_receipt_type String? // μ†Œλ“κ³΅μ œ(DEDUCTION), μ§€μΆœμ¦λΉ™(PROOF) λ“±
payment_id Int @id @default(autoincrement())
purchase_id Int @unique
status Status
pcd_pay_oid String @unique
pcd_pay_reqkey String @unique
pay_type String?
card_name String?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
purchase Purchase @relation(fields: [purchase_id], references: [purchase_id], onDelete: Cascade)
settlement Settlement?
cash_receipt_url String?
}

model Settlement {
Expand Down Expand Up @@ -460,6 +460,7 @@ model SettlementAccount {
account_number String @db.VarChar(30)
account_holder String @db.VarChar(100)
seller_type SellerType @default(INDIVIDUAL)
birth_date String? @db.VarChar(10)
status ApprovalStatus @default(APPROVED) // κ°œμΈμ€ μ¦‰μ‹œ 승인, μ‚¬μ—…μžλŠ” PENDING으둜 생성
is_active Boolean @default(true)
representative_name String? @db.VarChar(100)
Expand Down
25 changes: 25 additions & 0 deletions src/config/redis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createClient } from 'redis';

const redisClient = createClient({
url: process.env.REDIS_URL,
});

redisClient.on('connect', () => {
console.log('Redis connected successfully!');
});

redisClient.on('error', (err) => {
console.error('Redis connection error:', err);
});

const connectRedis = async () => {
try {
await redisClient.connect();
} catch (error) {
console.error('Redis μ—°κ²° μ‹€νŒ¨, μ„œλ²„λ₯Ό λ‹€μ‹œ ν™•μΈν•΄μ£Όμ„Έμš”.', error);
}
};

connectRedis();

export default redisClient;
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import membersRouter from "./members/routes/member.route"; // members λΌμš°ν„°
import promptRoutes from "./prompts/routes/prompt.route"; // ν”„λ‘¬ν”„νŠΈ κ΄€λ ¨ λΌμš°ν„°
import ReviewRouter from "./reviews/routes/review.route";
import purchaseRouter from "./purchases/routes/purchase.route";
import purchaseWebhookRouter from "./purchases/routes/purchase.webhook.route";
import settlementRouter from "./settlements/routes/settlement.route";
import withdrawalRouter from "./withdrawals/routes/withdrawal.route";
import promptDownloadRouter from "./prompts/routes/prompt.download.route";
Expand Down Expand Up @@ -123,6 +124,9 @@ app.use("/api/reviews", ReviewRouter);
// ν”„λ‘¬ν”„νŠΈ 검색 API
app.use("/api/prompts", promptRoutes);

// νŽ˜μ΄ν”Œ PCD_RST_URL μ„œλ²„ 콜백 (urlencoded/json 자체 νŒŒμ‹±)
app.use("/api/prompts/purchases", purchaseWebhookRouter);

// ν”„λ‘¬ν”„νŠΈ 결제 λΌμš°ν„°
app.use(
"/api/prompts/purchases",
Expand Down
11 changes: 8 additions & 3 deletions src/purchases/controller/purchase.complete.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ export const PurchaseCompleteController = {
const userId = (req.user as any).user_id;
const dto = req.body as Partial<PurchaseCompleteRequestDTO>;

if (!dto || typeof dto.paymentId !== 'string') {
if (
!dto ||
typeof dto.PCD_PAY_OID !== 'string' ||
typeof dto.PCD_PAY_REQKEY !== 'string' ||
typeof dto.PCD_AUTH_KEY !== 'string'
) {
return res.status(400).json({
error: 'BadRequest',
message: 'paymentIdλŠ” ν•„μˆ˜μž…λ‹ˆλ‹€.',
message: 'PCD_PAY_OID, PCD_PAY_REQKEY, PCD_AUTH_KEYλŠ” ν•„μˆ˜μž…λ‹ˆλ‹€.',
statusCode: 400,
});
}
Expand All @@ -22,4 +27,4 @@ export const PurchaseCompleteController = {
next(err);
}
},
};
};
36 changes: 12 additions & 24 deletions src/purchases/controller/purchase.webhook.controller.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,26 @@
import { Request, Response, NextFunction } from 'express';
import * as PortOne from '@portone/server-sdk';
import { WebhookService } from '../services/purchase.webhook.service';
import { PayplePaymentResult } from '../utils/payple';

export const WebhookController = {
async handleWebhook(req: Request, res: Response, next: NextFunction) {
try {
const webhookSecret = process.env.PORTONE_WEBHOOK_SECRET;
if (!webhookSecret) {
console.error('PORTONE_WEBHOOK_SECRET is not set');
return res.status(500).send('Server Config Error');
}
const result = req.body as Partial<PayplePaymentResult>;

// 1. μ›Ήν›… μ„œλͺ… 검증
const webhook = await PortOne.Webhook.verify(
webhookSecret,
req.body,
req.headers as Record<string, string | string[] | undefined>
);
if (!result || typeof result.PCD_PAY_RST !== 'string') {
return res.status(400).send('Invalid payload');
}

// 2. 이벀트 νƒ€μž…λ³„ 처리 -> ν˜„μž¬λŠ” 결제 μ™„λ£Œ(Paid)만 처리
if (webhook.type === 'Transaction.Paid') {
const { paymentId, storeId } = webhook.data;
await WebhookService.handleTransactionPaid(paymentId, storeId);
} else if (webhook.type === 'Transaction.Cancelled') {
console.log('[Webhook] Transaction Cancelled:', webhook.data.paymentId);
if (result.PCD_PAY_RST !== 'success') {
console.log('[Webhook] Non-success result:', result.PCD_PAY_CODE, result.PCD_PAY_MSG);
return res.status(200).send('OK');
}

await WebhookService.handlePaypleResult(result as PayplePaymentResult);
res.status(200).send('OK');
} catch (err) {
if (err instanceof PortOne.Webhook.WebhookVerificationError) {
console.error('[Webhook] Signature Verification Failed');
return res.status(400).send('Verification Failed');
}
console.error('[Webhook] Error:', err);
res.status(500).send('Internal Server Error');
}
}
};
},
};
8 changes: 4 additions & 4 deletions src/purchases/dtos/purchase.complete.dto.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
export interface PurchaseCompleteRequestDTO {
paymentId: string;
}
import { PayplePaymentResult } from '../utils/payple';

export type PurchaseCompleteRequestDTO = PayplePaymentResult;

export interface PurchaseCompleteResponseDTO {
message: string;
status: 'Succeed' | 'Failed' | 'Pending';
purchase_id?: number;
statusCode: number;
}
}
Loading
Loading