Skip to content
6 changes: 4 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,9 @@ app.use("/api/prompts", promptRoutes);
// νŽ˜μ΄ν”Œ PCD_RST_URL μ„œλ²„ 콜백 (urlencoded/json 자체 νŒŒμ‹±)
app.use("/api/prompts/purchases", purchaseWebhookRouter);

// ν”„λ‘¬ν”„νŠΈ 결제 λΌμš°ν„°
app.use("/api/prompts/purchases", refundRouter);
// ν”„λ‘¬ν”„νŠΈ 결제 λΌμš°ν„° β€” purchaseRouterκ°€ 동적 :id 라우트λ₯Ό μΆ”ν›„ 좔가해도
// refundRouter 경둜(:purchaseId/refund, :purchaseId/refund-eligibility)와
// μΆ©λŒν•˜μ§€ μ•Šλ„λ‘ refundRouterλŠ” purchaseRouter 뒀에 마운트.
app.use(
"/api/prompts/purchases",
express.text({ type: "text/plain" }),
Expand All @@ -153,6 +154,7 @@ app.use(
},
purchaseRouter
);
app.use("/api/prompts/purchases", refundRouter);

// μ±„νŒ… λΌμš°ν„°
app.use("/api/chat", chatRouter);
Expand Down
128 changes: 83 additions & 45 deletions src/prompts/routes/prompt.download.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,79 +8,117 @@ const router = Router();
* @swagger
* tags:
* name: PromptDownload
* description: ν”„λ‘¬ν”„νŠΈ λ‹€μš΄λ‘œλ“œ / 상세 λ‚΄μš© 쑰회 API
*/

/**
* @swagger
* components:
* schemas:
* PromptContent:
* type: object
* properties:
* prompt_id:
* type: integer
* example: 123
* title:
* type: string
* example: "μ±—GPT둜 λ§ˆμΌ€νŒ… μžλ™ν™”ν•˜κΈ°"
* content:
* type: string
* example: "당신은 λ§ˆμΌ€νŒ… μ „λ¬Έκ°€μž…λ‹ˆλ‹€. 이 ν”„λ‘¬ν”„νŠΈλŠ”..."
* download_count:
* type: integer
* example: 42
* created_at:
* type: string
* format: date-time
* example: "2025-07-29T10:00:00Z"
* description: ν”„λ‘¬ν”„νŠΈ λ‹€μš΄λ‘œλ“œ / ꡬ맀 λͺ©λ‘ 쑰회 API
*/

/**
* @swagger
* /api/prompts/{promptId}/downloads:
* get:
* summary: ν”„λ‘¬ν”„νŠΈ 상세 λ‚΄μš© λ˜λŠ” λ‹€μš΄λ‘œλ“œ
* description: λ‘œκ·ΈμΈν•œ μ‚¬μš©μžκ°€ ν”„λ‘¬ν”„νŠΈ 상세 λ‚΄μš©μ„ ν™•μΈν•˜κ±°λ‚˜ λ‹€μš΄λ‘œλ“œν•©λ‹ˆλ‹€.
* summary: ν”„λ‘¬ν”„νŠΈ 상세 λ‚΄μš© λ‹€μš΄λ‘œλ“œ
* description: |
* λ‘œκ·ΈμΈν•œ μ‚¬μš©μžκ°€ κ΅¬λ§€ν•œ(λ˜λŠ” 무료) ν”„λ‘¬ν”„νŠΈμ˜ μ‹€μ œ 본문을 μ‘°νšŒν•©λ‹ˆλ‹€.
*
* 유료 ν”„λ‘¬ν”„νŠΈμ˜ 경우:
* - 결제 μ™„λ£Œ(`Payment.status='Succeed'`)된 본인 ꡬ맀만 ν—ˆμš©
* - ν™˜λΆˆλœ κ΅¬λ§€λŠ” 403 Refunded둜 차단
* - 첫 λ‹€μš΄λ‘œλ“œ μ‹œμ μ— `Purchase.downloaded_at`을 기둝 β€” 이후 ν™˜λΆˆ λΆˆκ°€ (#485)
*
* 무료 ν”„λ‘¬ν”„νŠΈλŠ” Purchase rowκ°€ μ—†μœΌλ©΄ μžλ™ 생성.
* tags: [PromptDownload]
* security:
* - jwt: []
* parameters:
* - in: path
* name: promptId
* required: true
* description: μ‘°νšŒν•  ν”„λ‘¬ν”„νŠΈ ID
* schema:
* type: integer
* schema: { type: integer }
* responses:
* 200:
* description: ν”„λ‘¬ν”„νŠΈ λ‚΄μš© λ°˜ν™˜ 성곡
* description: λ‹€μš΄λ‘œλ“œ 성곡
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/PromptContent'
* type: object
* properties:
* message: { type: string, example: ν”„λ‘¬ν”„νŠΈ λ‹€μš΄λ‘œλ“œ μ™„λ£Œ }
* title: { type: string, example: "μ±—GPT λ§ˆμΌ€νŒ… μžλ™ν™”" }
* prompt: { type: string, description: ν”„λ‘¬ν”„νŠΈ λ³Έλ¬Έ }
* is_free: { type: boolean }
* is_paid: { type: boolean }
* statusCode: { type: integer, example: 200 }
* 401:
* description: 인증이 ν•„μš”ν•©λ‹ˆλ‹€
* description: 둜그인 ν•„μš”
* 403:
* description: 결제 λ―Έμ™„λ£Œ λ˜λŠ” ν™˜λΆˆλ¨
* content:
* application/json:
* schema:
* type: object
* properties:
* error:
* type: string
* example: Unauthorized
* message:
* type: string
* example: 둜그인이 ν•„μš”ν•©λ‹ˆλ‹€.
* statusCode:
* type: number
* example: 401
* error: { type: string, description: "Forbidden | Refunded" }
* message: { type: string }
* statusCode: { type: integer, example: 403 }
* examples:
* notPaid:
* summary: 유료 ν”„λ‘¬ν”„νŠΈ 미결제
* value: { error: Forbidden, message: ν•΄λ‹Ή ν”„λ‘¬ν”„νŠΈλŠ” λ¬΄λ£Œκ°€ μ•„λ‹ˆλ©°, κ²°μ œκ°€ μ™„λ£Œλ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€., statusCode: 403 }
* refunded:
* summary: ν™˜λΆˆλœ ν”„λ‘¬ν”„νŠΈ μž¬λ‹€μš΄λ‘œλ“œ μ‹œλ„
* value: { error: Refunded, message: ν™˜λΆˆλœ ν”„λ‘¬ν”„νŠΈλŠ” λ‹€μ‹œ λ‹€μš΄λ‘œλ“œν•  수 μ—†μŠ΅λ‹ˆλ‹€., statusCode: 403 }
* 404:
* description: ν”„λ‘¬ν”„νŠΈλ₯Ό 찾을 수 μ—†μŒ
* description: ν”„λ‘¬ν”„νŠΈ μ—†μŒ
* 500:
* description: μ„œλ²„ 였λ₯˜
*/
router.get('/:promptId/downloads', authenticateJwt, PromptDownloadController.getPromptContent);

/**
* @swagger
* /api/prompts/downloads:
* get:
* summary: 본인이 κ΅¬λ§€ν•œ ν”„λ‘¬ν”„νŠΈ λͺ©λ‘ 쑰회
* description: λ‘œκ·ΈμΈν•œ μ‚¬μš©μžκ°€ λ‹€μš΄λ‘œλ“œ(ꡬ맀)ν•œ ν”„λ‘¬ν”„νŠΈ λͺ©λ‘μ„ μ΅œμ‹ μˆœμœΌλ‘œ λ°˜ν™˜ν•©λ‹ˆλ‹€. 무료/유료 λͺ¨λ‘ 포함.
* tags: [PromptDownload]
* security:
* - jwt: []
* responses:
* 200:
* description: λͺ©λ‘ 쑰회 성곡
* content:
* application/json:
* schema:
* type: object
* properties:
* message: { type: string, example: ν”„λ‘¬ν”„νŠΈ λ‹€μš΄λ‘œλ“œ λͺ©λ‘ 쑰회 성곡 }
* statusCode: { type: integer, example: 200 }
* data:
* type: array
* items:
* type: object
* properties:
* prompt_id: { type: integer }
* title: { type: string }
* description: { type: string, nullable: true }
* price: { type: integer }
* models: { type: array, items: { type: string } }
* imageUrls: { type: array, items: { type: string } }
* has_review: { type: boolean }
* is_recent_review: { type: boolean, description: 졜근 30일 λ‚΄ 리뷰 μž‘μ„± μ—¬λΆ€ }
* userNickname: { type: string }
* userProfileImageUrl: { type: string, nullable: true }
* userReview:
* type: object
* nullable: true
* properties:
* review_id: { type: integer }
* content: { type: string }
* rating: { type: number }
* 401:
* description: 둜그인 ν•„μš”
* 500:
* description: μ„œλ²„ 였λ₯˜
*/
router.get('/downloads', authenticateJwt, PromptDownloadController.getDownloadedPrompts);

export default router;
export default router;
7 changes: 1 addition & 6 deletions src/purchases/routes/purchase.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,6 @@ router.post('/requests', authenticateJwt, PurchaseRequestController.requestPurch
* purchased_at:
* type: string
* format: date-time
* pg:
* type: string
* description: 결제 제곡자 (DB Enum)
* enum: [TOSSPAYMENTS, KAKAOPAY, TOSSPAY, NAVERPAY, SAMSUNGPAY, APPLEPAY, LPAY, PAYCO, SSG, PINPAY]
* nullable: true
* statusCode:
* type: integer
*/
Expand Down Expand Up @@ -152,7 +147,7 @@ router.get('/', authenticateJwt, PurchaseHistoryController.list);
* type: object
* properties:
* message: { type: string }
* status: { type: string, enum: [Succeed, Failed, Pending] }
* status: { type: string, enum: [Succeed, Failed, Pending, Refunded] }
* purchase_id: { type: integer }
* statusCode: { type: integer }
*/
Expand Down
2 changes: 2 additions & 0 deletions src/settlements/dtos/admin-seller.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export interface IndividualSellerDetail {
name: string;
email: string;
registration_type: 'INDIVIDUAL';
status: 'APPROVED' | 'PENDING' | 'REJECTED';
settlement_account: SettlementAccountSummary;
created_at: Date;
updated_at: Date;
Expand All @@ -97,6 +98,7 @@ export interface BusinessSellerDetail {
name: string;
email: string;
registration_type: 'BUSINESS';
status: 'APPROVED' | 'PENDING' | 'REJECTED';
business_number: string | null;
representative_name: string | null;
company_name: string | null;
Expand Down
2 changes: 2 additions & 0 deletions src/settlements/routes/admin-seller.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ router.get('/business', authenticateJwt, isAdmin, getBusinessSellerList);
* name: { type: string, example: 홍길동 }
* email: { type: string, example: gildong@example.com }
* registration_type: { type: string, example: INDIVIDUAL }
* status: { type: string, enum: [APPROVED, PENDING, REJECTED], example: APPROVED }
* settlement_account:
* type: object
* properties:
Expand Down Expand Up @@ -517,6 +518,7 @@ router.get(
* name: { type: string, example: 홍길동 }
* email: { type: string, example: gildong@example.com }
* registration_type: { type: string, example: BUSINESS }
* status: { type: string, enum: [APPROVED, PENDING, REJECTED], example: APPROVED }
* business_number: { type: string, nullable: true, example: "123-45-67890" }
* representative_name: { type: string, nullable: true, example: 홍길동 }
* company_name: { type: string, nullable: true, example: ν™κΈΈλ™μ»΄νΌλ‹ˆ }
Expand Down
7 changes: 3 additions & 4 deletions src/settlements/routes/settlement.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,12 +504,11 @@ router.post("/register/business", authenticateJwt, registerBusiness);
* get:
* summary: μ •μ‚° μ˜ˆμ • κΈˆμ•‘ 쑰회 (λŒ€μ‹œλ³΄λ“œ)
* description: |
* λ‘œκ·ΈμΈν•œ 판맀자의 μ •μ‚° μ˜ˆμ • κΈˆμ•‘(Settlement.status='Pending'인 ν–‰μ˜ amount 합계)을 μ‘°νšŒν•©λ‹ˆλ‹€.
* λ‘œκ·ΈμΈν•œ 판맀자의 μ •μ‚° μ˜ˆμ • κΈˆμ•‘(`Settlement.status='Pending'`인 ν–‰μ˜ amount 합계)을 μ‘°νšŒν•©λ‹ˆλ‹€.
* 정산관리 ν™”λ©΄ 상단 λŒ€μ‹œλ³΄λ“œμ— λ…ΈμΆœ.
*
* ⚠️ ν˜„μž¬ μ½”λ“œμ—λŠ” Settlement.statusλ₯Ό 'Succeed'둜 μ—…λ°μ΄νŠΈν•˜λŠ” μ •μ‚° μ™„λ£Œ 처리 흐름이 μ—†μ–΄μ„œ,
* λͺ¨λ“  λ―Έμ •μ‚° 거래의 λˆ„κ³„κ°€ λ°˜ν™˜λ©λ‹ˆλ‹€. 별도 μ΄μŠˆμ—μ„œ Payple μ •μ‚°λ‚΄μ—­ μ‘°νšŒμ™€ μ—°λ™ν•œ 동기화 흐름이 κ΅¬ν˜„λœ λ’€μ—λŠ”
* μ‹€μ œ μ •μ‚° μ˜ˆμ •λΆ„λ§Œ λ°˜ν™˜λ©λ‹ˆλ‹€.
* μ •μ‚° μ™„λ£Œ μ²˜λ¦¬λŠ” 맀일 KST 08:00에 λ™μž‘ν•˜λŠ” `settlement-sync` cron이 Payple μ •μ‚°λ‚΄μ—­ 쑰회 κ²°κ³Όλ₯Ό
* λ°”νƒ•μœΌλ‘œ `Pending β†’ Succeed`둜 μ „μ΄ν•©λ‹ˆλ‹€ (#482). ν™˜λΆˆλœ κ±°λž˜λŠ” `Refunded` μƒνƒœκ°€ λ˜μ–΄ λ³Έ ν•©κ³„μ—μ„œ μ œμ™Έλ©λ‹ˆλ‹€ (#485).
* tags: [Settlement]
* security:
* - jwt: []
Expand Down
2 changes: 2 additions & 0 deletions src/settlements/services/admin-seller.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ export const getIndividualSellerDetail = async (
name: account.user.name,
email: account.user.email,
registration_type: 'INDIVIDUAL',
status: account.status,
settlement_account: {
bank_code: account.bank_code,
account_number: account.account_number,
Expand Down Expand Up @@ -281,6 +282,7 @@ export const getBusinessSellerDetail = async (
name: account.user.name,
email: account.user.email,
registration_type: 'BUSINESS',
status: account.status,
business_number: account.business_number,
representative_name: account.representative_name,
company_name: account.company_name,
Expand Down
Loading
Loading