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>
This commit is contained in:
819
PLATFORM_DESIGN.md
Normal file
819
PLATFORM_DESIGN.md
Normal file
@@ -0,0 +1,819 @@
|
||||
# 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 내부 통신용
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*이 문서를 기반으로 구현을 시작합니다. 단계별 진행 중 변경 사항이 생기면 이 문서를 업데이트합니다.*
|
||||
Reference in New Issue
Block a user