- 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>
29 KiB
29 KiB
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
# 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)
// 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 라이브러리)
// 업로드 즉시 처리
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)
// 업로드 후 비동기 처리 (백그라운드 작업)
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에 토큰 검증 엔드포인트 추가:
// 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) 전략
자동 로깅 미들웨어
// 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)
# 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 내부 통신용
이 문서를 기반으로 구현을 시작합니다. 단계별 진행 중 변경 사항이 생기면 이 문서를 업데이트합니다.