Files
webflash/PLATFORM_DESIGN.md
root bdef4b7ae0 feat: add ESP32 DIY platform Phase 1 (marketplace scaffold)
- Docker Compose with Postgres, Redis, MinIO, backend, frontend (port 3200/3201)
- Prisma schema: User, Project, ProjectFile, Product, Order, FlashToken, Review, AuditLog
- Backend: JWT auth, project CRUD + file upload (MinIO + sharp WebP), admin approval flow
- Frontend: React + Vite SPA with auth, project/shop browse, seller dashboard, admin panel
- Admin: pending approval queue, user management, audit log viewer, stats dashboard
- Audit logging middleware for legal compliance
- Admin init script: createAdmin.js
- Full design document in PLATFORM_DESIGN.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 06:05:46 +09:00

820 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ESP32 DIY 플랫폼 — 전체 설계 문서
> webflash(포트 3100)와 연동되는 ESP32 프로젝트 마켓플레이스
> 포트: **3200** (프론트엔드 Nginx) / **3201** (백엔드 API)
> 작성일: 2026-05-20
---
## 1. 프로젝트 개요
| 구분 | 내용 |
|------|------|
| 목적 | ESP32 DIY 프로젝트 공유·판매 플랫폼 |
| 사용자 역할 | `admin` / `seller` (프로젝트 등록자) / `buyer` (구매자) |
| 수익 구조 | 판매가의 일정 % 수수료 (프로젝트별 admin 설정) |
| 결제 | 토스페이먼츠 (1차), Stripe (2차) |
| 플래시 연동 | 구매 후 1회용 토큰 → webflash에서 토큰 검증 후 플래시 |
---
## 2. 시스템 아키텍처
```
외부 접근 (HTTPS — 사용자 직접 설정)
┌─────────────────────────────────────────────────────┐
│ Docker Network │
│ │
│ :3200 platform-frontend (Nginx + React SPA) │
│ │ │
│ ▼ /api/* /auth/* │
│ :3201 platform-backend (Node.js + Express) │
│ │ │
│ ┌──────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ postgres redis minio (:9000) │
│ (DB) (세션·캐시) (파일 스토리지) │
│ │
│ :3100 webflash (기존, 별도 네트워크) │
│ platform-backend → webflash API (토큰 검증용) │
└─────────────────────────────────────────────────────┘
```
---
## 3. 디렉터리 구조
```
webflash/ ← 기존 webflash
platform/
├── backend/
│ ├── Dockerfile
│ ├── package.json
│ ├── prisma/
│ │ └── schema.prisma ← DB 스키마
│ └── src/
│ ├── index.js ← 진입점
│ ├── config/
│ │ ├── db.js ← Prisma 클라이언트
│ │ ├── redis.js
│ │ └── minio.js ← S3 클라이언트
│ ├── middleware/
│ │ ├── auth.js ← JWT 검증
│ │ ├── roles.js ← admin/seller/buyer 가드
│ │ ├── audit.js ← 모든 요청 자동 로깅
│ │ └── rateLimit.js ← Redis 기반
│ ├── routes/
│ │ ├── auth.js
│ │ ├── users.js
│ │ ├── projects.js
│ │ ├── products.js
│ │ ├── orders.js
│ │ ├── payments.js ← 토스페이먼츠
│ │ ├── reviews.js
│ │ ├── flash.js ← 토큰 발급·검증
│ │ └── admin.js
│ └── services/
│ ├── media.js ← sharp + ffmpeg 압축
│ ├── toss.js ← 토스 API 래퍼
│ └── storage.js ← MinIO 업·다운로드
├── frontend/
│ ├── Dockerfile
│ ├── nginx.conf
│ └── src/
│ ├── main.jsx
│ ├── App.jsx ← React Router 루트
│ ├── pages/
│ │ ├── Home.jsx
│ │ ├── Projects.jsx ← 탐색
│ │ ├── ProjectDetail.jsx
│ │ ├── Shop.jsx ← 상품 목록
│ │ ├── ProductDetail.jsx ← 구매·리뷰
│ │ ├── Auth/
│ │ │ ├── Login.jsx
│ │ │ └── Register.jsx
│ │ ├── Dashboard/ ← 판매자
│ │ │ ├── Index.jsx
│ │ │ ├── ProjectNew.jsx
│ │ │ ├── ProjectEdit.jsx
│ │ │ ├── Orders.jsx
│ │ │ └── Sales.jsx
│ │ ├── Flash.jsx ← 구매 후 플래시 페이지
│ │ └── Admin/
│ │ ├── Index.jsx
│ │ ├── PendingProjects.jsx
│ │ ├── Users.jsx
│ │ ├── Orders.jsx
│ │ └── Logs.jsx
│ ├── components/ ← 공통 컴포넌트
│ └── hooks/ ← useAuth, useCart 등
└── docker-compose.yml ← platform 전용
```
---
## 4. Docker Compose
```yaml
# platform/docker-compose.yml
services:
platform-db:
image: postgres:16-alpine
restart: unless-stopped
volumes:
- platform-db-data:/var/lib/postgresql/data
environment:
POSTGRES_DB: platform
POSTGRES_USER: platform
POSTGRES_PASSWORD: ${DB_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U platform"]
interval: 10s
platform-redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- platform-redis-data:/data
platform-minio:
image: minio/minio:latest
restart: unless-stopped
volumes:
- platform-storage:/data
environment:
MINIO_ROOT_USER: ${MINIO_USER}
MINIO_ROOT_PASSWORD: ${MINIO_PASSWORD}
command: server /data --console-address ":9001"
# 9000: S3 API (backend 내부용), 9001: 관리 콘솔
platform-backend:
build: ./backend
restart: unless-stopped
ports:
- "3201:3201"
depends_on:
platform-db:
condition: service_healthy
platform-redis:
condition: service_started
platform-minio:
condition: service_started
environment:
PORT: 3201
DATABASE_URL: postgresql://platform:${DB_PASSWORD}@platform-db:5432/platform
REDIS_URL: redis://platform-redis:6379
MINIO_ENDPOINT: platform-minio
MINIO_PORT: 9000
MINIO_ACCESS_KEY: ${MINIO_USER}
MINIO_SECRET_KEY: ${MINIO_PASSWORD}
MINIO_BUCKET: platform
JWT_SECRET: ${JWT_SECRET}
JWT_EXPIRES_IN: 7d
TOSS_CLIENT_KEY: ${TOSS_CLIENT_KEY}
TOSS_SECRET_KEY: ${TOSS_SECRET_KEY}
TOSS_SUCCESS_URL: ${BASE_URL}/payment/success
TOSS_FAIL_URL: ${BASE_URL}/payment/fail
WEBFLASH_API_URL: http://host.docker.internal:3000 # webflash backend
ALLOWED_ORIGIN: ${BASE_URL}
platform-frontend:
build: ./frontend
restart: unless-stopped
ports:
- "3200:80"
depends_on:
- platform-backend
volumes:
platform-db-data:
platform-redis-data:
platform-storage:
```
---
## 5. 데이터베이스 스키마 (Prisma)
```prisma
// platform/backend/prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum UserRole {
admin
seller
buyer
}
enum ProjectStatus {
draft
pending // 관리자 검토 대기
approved // 승인됨 (판매 가능)
rejected
suspended // 관리자 강제 비활성화
}
enum OrderStatus {
pending // 결제 전
paid // 결제 완료
cancelled
refunded
}
enum PaymentGateway {
toss
stripe
}
model User {
id String @id @default(uuid())
email String @unique
passwordHash String
nickname String @unique
role UserRole @default(buyer)
profileImageUrl String?
isEmailVerified Boolean @default(false)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lastLoginAt DateTime?
lastLoginIp String?
projects Project[]
orders Order[]
reviews Review[]
auditLogs AuditLog[]
}
model Project {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id])
title String
description String @db.Text
difficultyLevel Int @default(3) // 1~5
requiredParts Json? // [{name, link, quantity}]
status ProjectStatus @default(draft)
adminNote String? // 반려 사유 등
commissionRate Float @default(0.1) // 10%
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
files ProjectFile[]
product Product?
}
model ProjectFile {
id String @id @default(uuid())
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
fileType String // image | video | stl | wiring | firmware
url String // MinIO presigned or public URL
thumbnailUrl String? // 영상/이미지 썸네일
fileSize Int
mimeType String
displayOrder Int @default(0)
createdAt DateTime @default(now())
}
model Product {
id String @id @default(uuid())
projectId String @unique
project Project @relation(fields: [projectId], references: [id])
price Int // 원 단위 (KRW)
isOnSale Boolean @default(true)
totalSales Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
orders Order[]
reviews Review[]
}
model Order {
id String @id @default(uuid())
buyerId String
buyer User @relation(fields: [buyerId], references: [id])
productId String
product Product @relation(fields: [productId], references: [id])
amount Int // 실 결제금액 (원)
commissionAmount Int // 플랫폼 수수료
sellerAmount Int // 판매자 정산금액
paymentGateway PaymentGateway @default(toss)
paymentKey String? // 토스 paymentKey / Stripe PaymentIntent ID
orderId String? @unique // 토스 orderId (자체 생성)
status OrderStatus @default(pending)
buyerInfo Json // {name, phone, email} — 법적 보존
deviceInfo Json? // {userAgent, ip}
orderedAt DateTime @default(now())
paidAt DateTime?
refundedAt DateTime?
refundReason String?
flashToken FlashToken?
review Review?
}
model FlashToken {
id String @id @default(uuid())
token String @unique @default(uuid())
orderId String @unique
order Order @relation(fields: [orderId], references: [id])
isUsed Boolean @default(false)
usedAt DateTime?
macAddress String?
chipFamily String?
expiresAt DateTime // 발급 후 30일
createdAt DateTime @default(now())
flashLog FlashLog?
}
model FlashLog {
id String @id @default(uuid())
flashTokenId String @unique
flashToken FlashToken @relation(fields: [flashTokenId], references: [id])
macAddress String
chipFamily String
firmwareName String
firmwareId String
success Boolean
errorMessage String?
clientIp String
userAgent String?
flashedAt DateTime @default(now())
}
model Review {
id String @id @default(uuid())
orderId String @unique
order Order @relation(fields: [orderId], references: [id])
userId String
user User @relation(fields: [userId], references: [id])
productId String
product Product @relation(fields: [productId], references: [id])
rating Int // 1~5
title String
content String @db.Text
isVisible Boolean @default(true) // 관리자 숨김 처리 가능
createdAt DateTime @default(now())
media ReviewMedia[]
}
model ReviewMedia {
id String @id @default(uuid())
reviewId String
review Review @relation(fields: [reviewId], references: [id], onDelete: Cascade)
mediaType String // image | video
url String
thumbnailUrl String?
createdAt DateTime @default(now())
}
// 모든 주요 행동 로그 (법적 대응용)
model AuditLog {
id String @id @default(uuid())
userId String?
user User? @relation(fields: [userId], references: [id])
action String // LOGIN | REGISTER | PROJECT_SUBMIT | ORDER_CREATE | PAYMENT_CONFIRM | FLASH | REVIEW_POST | ADMIN_APPROVE | ...
targetType String? // Project | Order | User | Product | Review
targetId String?
ipAddress String
userAgent String?
requestMethod String?
requestPath String?
requestBody Json? // 민감 필드 마스킹 후 저장
responseStatus Int?
metadata Json? // 추가 컨텍스트
createdAt DateTime @default(now())
@@index([userId])
@@index([action])
@@index([createdAt])
}
```
---
## 6. REST API 목록
### Auth
| 메서드 | 경로 | 설명 | 권한 |
|--------|------|------|------|
| POST | `/api/auth/register` | 회원가입 | Public |
| POST | `/api/auth/login` | 로그인 → JWT 반환 | Public |
| POST | `/api/auth/logout` | 로그아웃 (Redis 블랙리스트) | User |
| GET | `/api/auth/me` | 내 프로필 | User |
| POST | `/api/auth/refresh` | 액세스 토큰 갱신 | User |
### Projects
| 메서드 | 경로 | 설명 | 권한 |
|--------|------|------|------|
| GET | `/api/projects` | 승인된 프로젝트 목록 (페이지네이션) | Public |
| GET | `/api/projects/:id` | 프로젝트 상세 | Public |
| POST | `/api/projects` | 새 프로젝트 생성 | User |
| PUT | `/api/projects/:id` | 수정 (draft 상태만) | Owner |
| DELETE | `/api/projects/:id` | 삭제 | Owner/Admin |
| POST | `/api/projects/:id/submit` | 관리자 검토 요청 | Owner |
| POST | `/api/projects/:id/files` | 파일 업로드 (multipart) | Owner |
| DELETE | `/api/projects/:id/files/:fileId` | 파일 삭제 | Owner |
### Products & Shop
| 메서드 | 경로 | 설명 | 권한 |
|--------|------|------|------|
| GET | `/api/products` | 판매 중인 상품 목록 | Public |
| GET | `/api/products/:id` | 상품 상세 + 리뷰 | Public |
| PUT | `/api/products/:id/toggle` | 판매 일시중지/재개 | Owner/Admin |
### Orders & Payment (토스페이먼츠)
| 메서드 | 경로 | 설명 | 권한 |
|--------|------|------|------|
| POST | `/api/orders` | 주문 생성 (결제 전) | User |
| GET | `/api/orders/:id` | 주문 상세 | Owner/Admin |
| GET | `/api/orders/me` | 내 주문 목록 | User |
| POST | `/api/payments/toss/confirm` | 토스 결제 승인 (서버→토스) | User |
| POST | `/api/payments/toss/webhook` | 토스 웹훅 수신 | Internal |
| POST | `/api/orders/:id/refund` | 환불 요청 | User/Admin |
### Flash Token
| 메서드 | 경로 | 설명 | 권한 |
|--------|------|------|------|
| GET | `/api/flash/:token` | 토큰 정보 조회 (webflash가 호출) | Token |
| POST | `/api/flash/:token/consume` | 플래시 완료 기록 (webflash가 호출) | Token |
### Reviews
| 메서드 | 경로 | 설명 | 권한 |
|--------|------|------|------|
| GET | `/api/reviews?productId=` | 상품 리뷰 목록 | Public |
| POST | `/api/reviews` | 리뷰 작성 (구매자만) | Buyer |
| PUT | `/api/reviews/:id` | 수정 | Owner |
| DELETE | `/api/reviews/:id` | 삭제 | Owner/Admin |
### Admin
| 메서드 | 경로 | 설명 | 권한 |
|--------|------|------|------|
| GET | `/api/admin/projects/pending` | 승인 대기 목록 | Admin |
| POST | `/api/admin/projects/:id/approve` | 승인 (commissionRate 설정) | Admin |
| POST | `/api/admin/projects/:id/reject` | 반려 (사유 포함) | Admin |
| GET | `/api/admin/users` | 사용자 목록 | Admin |
| PUT | `/api/admin/users/:id/toggle` | 계정 활성/비활성 | Admin |
| GET | `/api/admin/logs` | 감사 로그 조회 (필터·페이지네이션) | Admin |
| GET | `/api/admin/flash-logs` | 플래시 로그 | Admin |
| GET | `/api/admin/stats` | 매출·가입·플래시 통계 | Admin |
---
## 7. 주요 사용자 흐름
### 7-1. 프로젝트 등록 → 판매 승인 흐름
```
판매자 서버 관리자
│ │ │
│── POST /projects ────────────>│ draft 상태 생성 │
│── POST /projects/:id/files ──>│ MinIO 업로드 + 압축 │
│── POST /projects/:id/submit ─>│ status = pending │
│ │── 관리자에게 알림 ─────────>│
│ │ │── GET /admin/projects/pending
│ │ │── POST /admin/projects/:id/approve
│ │<─── commissionRate 설정 ───│
│ │ status = approved │
│ │ Product 자동 생성 │
│<── 이메일 알림 ───────────────│ │
```
### 7-2. 구매 → 플래시 흐름
```
구매자 platform-backend webflash-backend
│ │ │
│── POST /orders ──────────────>│ Order(pending) 생성 │
│ (토스 결제창 진행) │ │
│── POST /payments/toss/confirm>│ 토스 API 승인 요청 │
│ │── 토스 서버 확인 │
│ │ Order status = paid │
│ │ FlashToken 생성 (UUID, 30일)│
│<── { flashToken, orderId } ───│ │
│ │ │
│── GET /flash/[token] ────────────────────────────────────>│
│ │ │── token 검증 API 호출
│ │<── validate ──────────────│
│ │── firmware manifest 반환 ─>│
│ │ │── manifest 서빙
│ (브라우저 → USB → ESP32 플래시)│ │
│── POST /flash/[token]/consume ─────────────────────────────>│
│ │<── {mac, chipFamily} ──────│
│ │ FlashToken.isUsed = true │
│ │ FlashLog 저장 │
```
### 7-3. 리뷰 작성 흐름
```
구매자 (플래시 완료 후)
│── POST /reviews
│ body: { orderId, rating, title, content }
│ files: images[] + videos[]
서버:
├── 주문 소유자 확인
├── FlashToken.isUsed = true 확인 (구매+플래시 완료 검증)
├── 이미지: sharp → WebP 변환, 1920px 리사이즈, 80% 품질
├── 영상: ffmpeg → H.264/720p, 2Mbps, 최대 3분
└── ReviewMedia 저장
```
---
## 8. 파일 처리 (미디어 압축)
### 이미지 (sharp 라이브러리)
```javascript
// 업로드 즉시 처리
const processed = await sharp(buffer)
.resize(1920, 1080, { fit: 'inside', withoutEnlargement: true })
.webp({ quality: 82 })
.toBuffer();
// 썸네일
const thumb = await sharp(buffer)
.resize(400, 300, { fit: 'cover' })
.webp({ quality: 70 })
.toBuffer();
```
| 원본 | 처리 후 | 제한 |
|------|---------|------|
| JPEG/PNG/WEBP | WebP 1920px이하 82% | 업로드 최대 20MB |
| 썸네일 | WebP 400×300 | - |
### 영상 (fluent-ffmpeg)
```javascript
// 업로드 후 비동기 처리 (백그라운드 작업)
ffmpeg(inputPath)
.videoCodec('libx264')
.audioCodec('aac')
.videoBitrate('2000k')
.audioBitrate('128k')
.size('?x720') // 720p (세로 기준)
.duration(180) // 최대 3분
.outputOptions(['-preset fast', '-crf 23'])
.output(outputPath)
.run();
```
| 원본 | 처리 후 | 제한 |
|------|---------|------|
| MP4/MOV/AVI | H.264/720p/2Mbps | 업로드 최대 500MB, 처리 후 ~100MB 이하 |
| 썸네일 | 첫 프레임 WebP | - |
### 프로젝트 파일 허용 타입
| 필드 | 허용 확장자 | 최대 크기 |
|------|-----------|---------|
| 이미지 | jpg, jpeg, png, webp | 20 MB |
| 영상 | mp4, mov, avi, webm | 500 MB |
| 배선도 | jpg, png, pdf, svg | 20 MB |
| STL | stl | 50 MB |
| 펌웨어 | bin | 64 MB |
---
## 9. 결제 흐름 — 토스페이먼츠
```
프론트엔드 백엔드 토스페이먼츠
│ │ │
│── POST /api/orders ──────────>│ Order 생성 (pending) │
│<── { orderId, amount } ───────│ │
│ │ │
│ (TossPayments.js SDK 결제창) │ │
│──────────────────────────────────────────────────────────>│
│<── successUrl?paymentKey=XXX&orderId=YYY&amount=ZZZ ──────│
│ │ │
│── POST /api/payments/toss/confirm │
│ { paymentKey, orderId, amount } ──────────────────────>│
│ │── POST /v1/payments/confirm>│
│ │<── 결제 정보 응답 ─────────│
│ │ Order.status = paid │
│ │ FlashToken 생성 │
│<── { flashToken } ────────────│ │
│ │ │
│ (환불 시) │ │
│── POST /api/orders/:id/refund>│── POST /v1/payments/{key}/cancel
│ │<── 환불 완료 │
│ │ Order.status = refunded │
```
### 환불 정책 (서버 로직)
- 플래시 미완료: 전액 환불 가능
- 플래시 완료(FlashToken.isUsed=true): 환불 불가 (정책 명시)
- 관리자: 특수 케이스 강제 환불 가능
---
## 10. webflash 연동 상세
webflash `backend/server.js`에 토큰 검증 엔드포인트 추가:
```javascript
// webflash backend에 추가할 코드
app.get('/api/firmware/token/:token/manifest', async (req, res) => {
// platform backend에 토큰 검증 요청
const resp = await fetch(
`${PLATFORM_API}/api/flash/${req.params.token}`
);
if (!resp.ok) return res.status(403).json({ error: '유효하지 않은 토큰' });
const { firmwareId } = await resp.json();
const firmware = loadMeta().find(f => f.id === firmwareId);
if (!firmware) return res.status(404).json({ error: '펌웨어 없음' });
// 기존 manifest 생성 로직 재사용
res.json(buildManifest(firmware));
});
// 플래시 완료 콜백
app.post('/api/firmware/token/:token/complete', async (req, res) => {
const { mac, chipFamily } = req.body;
await fetch(`${PLATFORM_API}/api/flash/${req.params.token}/consume`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mac, chipFamily }),
});
res.json({ success: true });
});
```
플랫폼 플래시 페이지 (`/flash/:token`):
```
1. 토큰 유효성 표시 (만료일, 사용 여부)
2. webflash를 iframe 또는 새 탭으로 열기
URL: http://localhost:3100?token=xxx (또는 외부 도메인)
3. 플래시 완료 후 리뷰 작성 유도
```
---
## 11. 감사 로그 (AuditLog) 전략
### 자동 로깅 미들웨어
```javascript
// middleware/audit.js
// 모든 POST/PUT/DELETE에 자동 적용
// 민감 필드 마스킹: password, passwordHash, cardNumber
const SENSITIVE = ['password', 'passwordHash', 'token', 'paymentKey'];
function maskBody(body) {
const masked = { ...body };
SENSITIVE.forEach(k => { if (masked[k]) masked[k] = '***'; });
return masked;
}
```
### 법적 대응에 필요한 보존 데이터
| 이벤트 | 저장 데이터 |
|--------|-----------|
| 회원가입 | email, ip, userAgent, timestamp |
| 로그인 | userId, ip, userAgent, success/fail, timestamp |
| 구매 | orderId, buyerId, productId, amount, ip, buyerInfo JSON |
| 결제 확인 | paymentKey, orderId, amount, 토스 응답 전체 |
| 환불 | orderId, reason, adminId(강제환불 시), timestamp |
| 플래시 | token, mac, chipFamily, ip, firmwareId, success |
| 리뷰 | reviewId, orderId, rating, ip |
| 관리자 행동 | adminId, action, targetId, timestamp |
### 보존 정책
- AuditLog: **영구 보존** (삭제 API 없음)
- FlashLog: **영구 보존**
- Order + 결제 정보: **영구 보존**
- 미디어 파일: 계정 삭제 후 **30일 유예** 후 삭제
---
## 12. 프론트엔드 라우트 (React Router v6)
```
/ 홈 (추천 프로젝트, 인기 상품)
/projects 프로젝트 탐색 (필터: 칩/난이도/카테고리)
/projects/:id 프로젝트 상세 (파일 목록, 부품, 구매 버튼)
/shop 상품 목록 (가격/평점 정렬)
/shop/:id 상품 상세 + 리뷰
/auth/login
/auth/register
/dashboard 판매자 대시보드
/dashboard/projects 내 프로젝트 목록 + 상태
/dashboard/projects/new 새 프로젝트 작성 (멀티스텝)
/dashboard/projects/:id/edit
/dashboard/sales 판매·정산 내역
/dashboard/orders 구매 내역
/payment/success 토스 결제 성공 리다이렉트
/payment/fail 토스 결제 실패
/flash/:token 플래시 실행 페이지
/admin 관리자 대시보드 (매출 통계)
/admin/projects 승인 대기 목록
/admin/users 사용자 관리
/admin/orders 주문 관리
/admin/logs 감사 로그 (필터·CSV 내보내기)
```
---
## 13. 구현 단계 계획
### 1단계 — 핵심 (약 2주)
- [ ] Docker Compose + Postgres + Redis + MinIO 세팅
- [ ] Prisma 스키마 마이그레이션
- [ ] JWT 인증 (register/login/me)
- [ ] 프로젝트 CRUD + 파일 업로드 (MinIO)
- [ ] 관리자 승인/반려
- [ ] 상품 목록·상세 페이지 (React)
- [ ] 관리자 패널 기본 (승인 대기 목록)
### 2단계 — 결제·플래시 (약 1.5주)
- [ ] 토스페이먼츠 연동 (sandbox)
- [ ] 주문 생성·결제 확인·환불
- [ ] FlashToken 발급
- [ ] webflash 토큰 검증 API
- [ ] 플래시 페이지 (/flash/:token)
### 3단계 — 미디어·리뷰 (약 1주)
- [ ] sharp 이미지 압축
- [ ] ffmpeg 영상 압축 (백그라운드)
- [ ] 리뷰 작성·목록
- [ ] 별점 집계 (Product 테이블 업데이트)
### 4단계 — 운영 도구 (약 1주)
- [ ] 감사 로그 뷰어 (관리자)
- [ ] 매출 통계 대시보드
- [ ] 이메일 알림 (승인/결제/환불)
- [ ] Rate Limiting 강화
### 5단계 — Stripe (별도)
- [ ] Stripe Payment Intent 연동
- [ ] 환율 처리 (USD 기준)
---
## 14. 보안 체크리스트
- [ ] JWT 시크릿 환경변수 (rotate 가능)
- [ ] 비밀번호 bcrypt (rounds: 12)
- [ ] 파일 업로드: MIME 타입 + 매직 바이트 검사
- [ ] SQL Injection: Prisma ORM (파라미터 바인딩)
- [ ] XSS: React 기본 이스케이프 + DOMPurify (리뷰 내용)
- [ ] CORS: ALLOWED_ORIGIN 환경변수
- [ ] Rate Limit: Redis, IP당 100req/15min
- [ ] 토스 웹훅: HMAC 서명 검증
- [ ] 민감 정보 로그 마스킹
- [ ] MinIO presigned URL (직접 노출 방지)
---
## 15. 환경변수 목록 (.env)
```env
# DB
DB_PASSWORD=strongpassword
# Redis (기본값 사용 가능)
# MinIO
MINIO_USER=admin
MINIO_PASSWORD=strongpassword
# App
JWT_SECRET=very_long_random_secret
BASE_URL=https://your-domain.com
# 토스페이먼츠
TOSS_CLIENT_KEY=test_ck_...
TOSS_SECRET_KEY=test_sk_...
# webflash 연동
WEBFLASH_API_URL=http://localhost:3000
WEBFLASH_INTERNAL_TOKEN=shared_secret # platform↔webflash 내부 통신용
```
---
*이 문서를 기반으로 구현을 시작합니다. 단계별 진행 중 변경 사항이 생기면 이 문서를 업데이트합니다.*