- 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>
820 lines
29 KiB
Markdown
820 lines
29 KiB
Markdown
# 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 내부 통신용
|
||
```
|
||
|
||
---
|
||
|
||
*이 문서를 기반으로 구현을 시작합니다. 단계별 진행 중 변경 사항이 생기면 이 문서를 업데이트합니다.*
|