diff --git a/src/index.ts b/src/index.ts index c875035..84718a4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,7 @@ import authRouter from "./auth/routes/auth.route"; // auth 라우터 경로 수 import membersRouter from "./members/routes/member.route"; // members 라우터 import import promptRoutes from "./prompts/routes/prompt.route"; // 프롬프트 관련 라우터 import ReviewRouter from "./reviews/routes/review.route"; -import purchaseRouter from "./purchases/routes/purchase.request.route"; +import purchaseRouter from "./purchases/routes/purchase.route"; import settlementRouter from "./settlements/routes/settlement.route"; import withdrawalRouter from "./withdrawals/routes/withdrawal.route"; import accountRouter from "./accounts/routes/account.route"; diff --git a/src/purchases/controller/purchase.complete.controller.ts b/src/purchases/controller/purchase.complete.controller.ts index 24e86fd..53ad500 100644 --- a/src/purchases/controller/purchase.complete.controller.ts +++ b/src/purchases/controller/purchase.complete.controller.ts @@ -1,12 +1,12 @@ import { Request, Response, NextFunction } from 'express'; -import { PromptPurchaseCompleteRequestDTO } from '../dtos/purchase.complete.dto'; +import { PurchaseCompleteRequestDTO } from '../dtos/purchase.complete.dto'; import { PurchaseCompleteService } from '../services/purchase.complete.service'; export const PurchaseCompleteController = { async completePurchase(req: Request, res: Response, next: NextFunction) { try { const userId = (req.user as any).user_id; - const dto = req.body as Partial; + const dto = req.body as Partial; if (!dto || typeof dto.paymentId !== 'string') { return res.status(400).json({ @@ -16,7 +16,7 @@ export const PurchaseCompleteController = { }); } - const result = await PurchaseCompleteService.completePurchase(userId, dto as PromptPurchaseCompleteRequestDTO); + const result = await PurchaseCompleteService.completePurchase(userId, dto as PurchaseCompleteRequestDTO); res.status(result.statusCode).json(result); } catch (err) { next(err); diff --git a/src/purchases/dtos/purchase.complete.dto.ts b/src/purchases/dtos/purchase.complete.dto.ts index 64851e1..8169d76 100644 --- a/src/purchases/dtos/purchase.complete.dto.ts +++ b/src/purchases/dtos/purchase.complete.dto.ts @@ -1,8 +1,8 @@ -export interface PromptPurchaseCompleteRequestDTO { +export interface PurchaseCompleteRequestDTO { paymentId: string; } -export interface PromptPurchaseCompleteResponseDTO { +export interface PurchaseCompleteResponseDTO { message: string; status: 'Succeed' | 'Failed' | 'Pending'; purchase_id?: number; diff --git a/src/purchases/dtos/purchase.request.dto.ts b/src/purchases/dtos/purchase.request.dto.ts index 7e2c84b..262ec65 100644 --- a/src/purchases/dtos/purchase.request.dto.ts +++ b/src/purchases/dtos/purchase.request.dto.ts @@ -1,3 +1,17 @@ -export interface PromptPurchaseRequestDTO { +export interface PurchaseRequestDTO { prompt_id: number; +} + +export interface PurchaseRequestResponseDTO { + message: string; + statusCode: number; + storeId: string; + paymentId: string; + orderName: string; + totalAmount: number; + channelKey: string; + customData: { + prompt_id: number; + user_id: number; + }; } \ No newline at end of file diff --git a/src/purchases/routes/purchase.request.route.ts b/src/purchases/routes/purchase.route.ts similarity index 68% rename from src/purchases/routes/purchase.request.route.ts rename to src/purchases/routes/purchase.route.ts index f5e519f..05b29b7 100644 --- a/src/purchases/routes/purchase.request.route.ts +++ b/src/purchases/routes/purchase.route.ts @@ -1,8 +1,8 @@ import { Router } from 'express'; -import { PurchaseCompleteController } from '../controller/purchase.complete.controller'; import { authenticateJwt } from '../../config/passport'; import { PurchaseRequestController } from '../controller/purchase.request.controller'; import { PurchaseHistoryController } from '../controller/purchase.controller'; +import { PurchaseCompleteController } from '../controller/purchase.complete.controller'; const router = Router(); @@ -17,8 +17,8 @@ const router = Router(); * @swagger * /api/prompts/purchases/requests: * post: - * summary: 결제 요청 생성 (사전 검증) - * description: 프론트엔드에서 결제창을 띄우기 전, 주문 번호 생성 및 사전 검증을 수행합니다. + * summary: 결제 요청 생성 (주문서 발행) + * description: 프론트엔드에서 포트원 V2 결제창을 띄우기 위해 필요한 주문 번호(paymentId)와 결제 정보를 생성합니다. * tags: [Purchase] * security: * - jwt: [] @@ -30,26 +30,14 @@ const router = Router(); * type: object * required: * - prompt_id - * - merchant_uid - * - amount - * - buyer_name - * - redirect_url * properties: * prompt_id: * type: integer - * merchant_uid: - * type: string - * description: 가맹점 주문 번호 - * amount: - * type: integer - * description: 결제 예정 금액 - * buyer_name: - * type: string - * redirect_url: - * type: string + * description: 구매하려는 프롬프트의 ID + * example: 12 * responses: * 200: - * description: 요청 성공 + * description: 주문서 생성 성공 (PortOne V2 SDK 연동 데이터 반환) * content: * application/json: * schema: @@ -57,56 +45,42 @@ const router = Router(); * properties: * message: * type: string - * merchant_uid: - * type: string + * example: "주문서가 생성되었습니다." * statusCode: * type: integer - */ -router.post('/requests', authenticateJwt, PurchaseRequestController.requestPurchase); - -/** - * @swagger - * /api/prompts/purchases/complete: - * post: - * summary: 결제 완료 처리 (검증 및 저장) - * description: 포트원 결제 완료 후, paymentId를 서버로 보내 검증하고 구매를 확정합니다. - * tags: [Purchase] - * security: - * - jwt: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - paymentId - * properties: - * paymentId: - * type: string - * description: 포트원 V2 결제 ID - * merchant_uid: - * type: string - * description: 가맹점 주문 번호 - * responses: - * 200: - * description: 결제 성공 및 저장 완료 - * content: - * application/json: - * schema: - * type: object - * properties: - * message: + * example: 200 + * storeId: * type: string - * status: + * description: 포트원 상점 ID (SDK 설정용) + * example: "store-abc12345..." + * paymentId: * type: string - * enum: [Succeed, Failed, Pending] - * purchase_id: - * type: integer - * statusCode: - * type: integer + * description: 서버에서 생성한 고유 주문 번호 (구 merchant_uid) + * example: "payment-550e8400-e29b-41d4-a716-446655440000" + * orderName: + * type: string + * description: 주문명 (프롬프트 제목) + * example: "감성적인 AI 풍경화 프롬프트" + * totalAmount: + * type: number + * description: 결제 금액 (DB 기준) + * example: 5000 + * channelKey: + * type: string + * description: 포트원 채널 키 (PG사 구분용) + * example: "channel-key-uuid..." + * customData: + * type: object + * description: 결제 검증 및 웹훅 처리를 위한 메타 데이터 + * properties: + * prompt_id: + * type: integer + * example: 12 + * user_id: + * type: integer + * example: 5 */ -router.post('/complete', authenticateJwt, PurchaseCompleteController.completePurchase); +router.post('/requests', authenticateJwt, PurchaseRequestController.requestPurchase); /** * @swagger @@ -153,4 +127,45 @@ router.post('/complete', authenticateJwt, PurchaseCompleteController.completePur */ router.get('/', authenticateJwt, PurchaseHistoryController.list); +/** + * @swagger + * /api/prompts/purchases/complete: + * post: + * summary: 결제 완료 처리 (검증 및 저장) + * description: 포트원 결제 완료 후, paymentId를 서버로 보내 검증하고 구매를 확정합니다. + * tags: [Purchase] + * security: + * - jwt: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - paymentId + * properties: + * paymentId: + * type: string + * description: 포트원 V2 결제 ID + * responses: + * 200: + * description: 결제 성공 및 저장 완료 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * status: + * type: string + * enum: [Succeed, Failed, Pending] + * purchase_id: + * type: integer + * statusCode: + * type: integer + */ +router.post('/complete', authenticateJwt, PurchaseCompleteController.completePurchase); + export default router; \ No newline at end of file diff --git a/src/purchases/services/purchase.complete.service.ts b/src/purchases/services/purchase.complete.service.ts index 258eaee..f2fea80 100644 --- a/src/purchases/services/purchase.complete.service.ts +++ b/src/purchases/services/purchase.complete.service.ts @@ -1,4 +1,4 @@ -import { PromptPurchaseCompleteRequestDTO, PromptPurchaseCompleteResponseDTO } from '../dtos/purchase.complete.dto'; +import { PurchaseCompleteRequestDTO, PurchaseCompleteResponseDTO } from '../dtos/purchase.complete.dto'; import { PurchaseRequestRepository } from '../repositories/purchase.request.repository'; import { PurchaseCompleteRepository } from '../repositories/purchase.complete.repository'; import { AppError } from '../../errors/AppError'; @@ -6,7 +6,7 @@ import prisma from '../../config/prisma'; import { fetchAndVerifyPortonePayment } from '../utils/portone'; export const PurchaseCompleteService = { - async completePurchase(userId: number, dto: PromptPurchaseCompleteRequestDTO): Promise { + async completePurchase(userId: number, dto: PurchaseCompleteRequestDTO): Promise { const { paymentId } = dto; // 1. 포트원 조회 (검증 전 단계) diff --git a/src/purchases/services/purchase.request.service.ts b/src/purchases/services/purchase.request.service.ts index 9b0e7e4..2ed9249 100644 --- a/src/purchases/services/purchase.request.service.ts +++ b/src/purchases/services/purchase.request.service.ts @@ -1,10 +1,11 @@ import { v4 as uuidv4 } from 'uuid'; -import { PromptPurchaseRequestDTO } from '../dtos/purchase.request.dto'; +import { PurchaseRequestDTO } from '../dtos/purchase.request.dto'; +import { PurchaseRequestResponseDTO } from '../dtos/purchase.request.dto'; import { PurchaseRequestRepository } from '../repositories/purchase.request.repository'; import { AppError } from '../../errors/AppError'; export const PurchaseRequestService = { - async createPurchaseRequest(userId: number, dto: PromptPurchaseRequestDTO) { + async createPurchaseRequest(userId: number, dto: PurchaseRequestDTO): Promise { const prompt = await PurchaseRequestRepository.findPromptWithSeller(dto.prompt_id); if (!prompt) throw new AppError('프롬프트를 찾을 수 없습니다.', 404, 'NotFound'); @@ -18,10 +19,12 @@ export const PurchaseRequestService = { return { message: '주문서가 생성되었습니다.', statusCode: 200, - merchant_uid: paymentId, - amount: prompt.price, - prompt_title: prompt.title, - custom_data: { + storeId: process.env.PORTONE_STORE_ID || '', + paymentId: paymentId, + orderName: prompt.title, + totalAmount: prompt.price, + channelKey: process.env.PORTONE_CHANNEL_KEY || '', + customData: { prompt_id: dto.prompt_id, user_id: userId, },