- Flash.jsx: replace esp-web-tools with direct esptool-js integration → reads MAC address + chip type before flash via Web Serial API → step-by-step UI (connect → board info → download → flash → done) → retry button on failure with remaining-attempt counter → firmware update token request after successful flash - Schema: FlashToken (maxAttempts/attemptCount/isLocked/isUpdateToken), FlashLog (startedAt/completedAt/durationMs/chipId/flashSize), FlashAnomaly model (RATE_LIMIT_IP/HIGH_VOLUME_IP/MAC_REUSE/TOKEN_LOCK/SUSPICIOUS_DURATION), ProjectFile.firmwareVersion - flash.js: new POST /start (board info + IP log + anomaly detection), updated POST /consume (timing, lock on exhaustion), GET returns firmwareFiles - orders.js: POST /request-reflash (free firmware update token for paid orders), updated to flashTokens[] relation - admin.js: GET /flash/metrics, GET/PUT /flash/anomalies, POST /flash/tokens/:id/unlock - Admin/FlashMetrics.jsx: dashboard with today stats, recent logs table, top-IP chart, anomaly management with resolve button Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
247 lines
6.7 KiB
Plaintext
247 lines
6.7 KiB
Plaintext
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)
|
|
chipFamily String @default("ESP32-S3")
|
|
requiredParts Json?
|
|
status ProjectStatus @default(draft)
|
|
adminNote String?
|
|
commissionRate Float @default(0.1)
|
|
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
|
|
url String
|
|
thumbnailUrl String?
|
|
fileSize Int
|
|
mimeType String
|
|
originalName String?
|
|
flashOffset String @default("0x0")
|
|
firmwareVersion String? // e.g. "1.0.0" — firmware 파일에만 사용
|
|
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
|
|
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?
|
|
tossOrderId String? @unique
|
|
status OrderStatus @default(pending)
|
|
buyerInfo Json
|
|
deviceInfo Json?
|
|
orderedAt DateTime @default(now())
|
|
paidAt DateTime?
|
|
refundedAt DateTime?
|
|
refundReason String?
|
|
|
|
flashTokens FlashToken[] // 원본 + 업데이트 토큰 복수 허용
|
|
review Review?
|
|
}
|
|
|
|
model FlashToken {
|
|
id String @id @default(uuid())
|
|
token String @unique @default(uuid())
|
|
orderId String
|
|
order Order @relation(fields: [orderId], references: [id])
|
|
isUsed Boolean @default(false) // 성공적으로 플래시된 경우 true
|
|
usedAt DateTime?
|
|
macAddress String?
|
|
chipFamily String?
|
|
expiresAt DateTime
|
|
createdAt DateTime @default(now())
|
|
maxAttempts Int @default(3) // 최대 시도 횟수
|
|
attemptCount Int @default(0) // 현재 시도 횟수 (실패 포함)
|
|
lastAttemptAt DateTime?
|
|
isLocked Boolean @default(false) // maxAttempts 초과 시 잠금
|
|
lockedReason String?
|
|
isUpdateToken Boolean @default(false) // 펌웨어 업데이트용 재발급 토큰
|
|
|
|
flashLogs FlashLog[]
|
|
|
|
@@index([orderId])
|
|
}
|
|
|
|
model FlashLog {
|
|
id String @id @default(uuid())
|
|
flashTokenId String
|
|
flashToken FlashToken @relation(fields: [flashTokenId], references: [id])
|
|
attemptNumber Int @default(1)
|
|
macAddress String? // 플래시 전 보드에서 읽은 MAC 주소
|
|
chipFamily String? // 칩 종류 (esptool-js 에서 읽음)
|
|
chipId String? // 칩 ID
|
|
flashSize String? // 플래시 메모리 크기
|
|
firmwareName String
|
|
firmwareId String
|
|
success Boolean
|
|
errorMessage String?
|
|
clientIp String
|
|
userAgent String?
|
|
startedAt DateTime? // 플래시 시작 시각
|
|
completedAt DateTime? // 플래시 완료/실패 시각
|
|
durationMs Int? // 소요 시간 ms
|
|
flashedAt DateTime @default(now())
|
|
|
|
@@index([flashTokenId])
|
|
@@index([clientIp])
|
|
@@index([macAddress])
|
|
@@index([flashedAt])
|
|
}
|
|
|
|
// 이상 감지 이벤트
|
|
model FlashAnomaly {
|
|
id String @id @default(uuid())
|
|
type String // RATE_LIMIT_IP | HIGH_VOLUME_IP | MAC_REUSE | TOKEN_LOCK | SUSPICIOUS_DURATION
|
|
severity String @default("medium") // low | medium | high
|
|
description String
|
|
clientIp String?
|
|
macAddress String?
|
|
flashTokenId String?
|
|
flashLogId String?
|
|
metadata Json?
|
|
resolved Boolean @default(false)
|
|
resolvedAt DateTime?
|
|
resolvedBy String?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([type])
|
|
@@index([clientIp])
|
|
@@index([resolved])
|
|
@@index([createdAt])
|
|
}
|
|
|
|
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
|
|
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
|
|
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
|
|
targetType String?
|
|
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])
|
|
}
|