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