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

29 KiB
Raw Permalink Blame History

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

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