# 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 내부 통신용
```

---

*이 문서를 기반으로 구현을 시작합니다. 단계별 진행 중 변경 사항이 생기면 이 문서를 업데이트합니다.*
