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 src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
6 changes: 3 additions & 3 deletions src/purchases/controller/purchase.complete.controller.ts
Original file line number Diff line number Diff line change
@@ -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<PromptPurchaseCompleteRequestDTO>;
const dto = req.body as Partial<PurchaseCompleteRequestDTO>;

if (!dto || typeof dto.paymentId !== 'string') {
return res.status(400).json({
Expand All @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/purchases/dtos/purchase.complete.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
16 changes: 15 additions & 1 deletion src/purchases/dtos/purchase.request.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
};
}
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -17,8 +17,8 @@ const router = Router();
* @swagger
* /api/prompts/purchases/requests:
* post:
* summary: 결제 μš”μ²­ 생성 (사전 검증)
* description: ν”„λ‘ νŠΈμ—”λ“œμ—μ„œ κ²°μ œμ°½μ„ λ„μš°κΈ° μ „, μ£Όλ¬Έ 번호 생성 및 사전 검증을 μˆ˜ν–‰ν•©λ‹ˆλ‹€.
* summary: 결제 μš”μ²­ 생성 (μ£Όλ¬Έμ„œ λ°œν–‰)
* description: ν”„λ‘ νŠΈμ—”λ“œμ—μ„œ ν¬νŠΈμ› V2 κ²°μ œμ°½μ„ λ„μš°κΈ° μœ„ν•΄ ν•„μš”ν•œ μ£Όλ¬Έ 번호(paymentId)와 결제 정보λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.
* tags: [Purchase]
* security:
* - jwt: []
Expand All @@ -30,83 +30,57 @@ 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:
* type: object
* 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
Expand Down Expand Up @@ -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;
4 changes: 2 additions & 2 deletions src/purchases/services/purchase.complete.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
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';
import prisma from '../../config/prisma';
import { fetchAndVerifyPortonePayment } from '../utils/portone';

export const PurchaseCompleteService = {
async completePurchase(userId: number, dto: PromptPurchaseCompleteRequestDTO): Promise<PromptPurchaseCompleteResponseDTO> {
async completePurchase(userId: number, dto: PurchaseCompleteRequestDTO): Promise<PurchaseCompleteResponseDTO> {
const { paymentId } = dto;

// 1. ν¬νŠΈμ› 쑰회 (검증 μ „ 단계)
Expand Down
15 changes: 9 additions & 6 deletions src/purchases/services/purchase.request.service.ts
Original file line number Diff line number Diff line change
@@ -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<PurchaseRequestResponseDTO> {
const prompt = await PurchaseRequestRepository.findPromptWithSeller(dto.prompt_id);
if (!prompt) throw new AppError('ν”„λ‘¬ν”„νŠΈλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.', 404, 'NotFound');

Expand All @@ -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,
},
Expand Down