feat: mock payment flow + flash token page (test mode)
- Mock purchase: order create → mock-pay → FlashToken issued instantly (no real billing) - Flash page (/flash/:token): esp-web-tools integration, token state display, consume on complete - Orders route: create/mock-pay/me/refund with full audit logging - Flash route: GET validate, GET manifest (esp-web-tools compatible), POST consume - MinIO file proxy (/api/files/*): browser CORS solved, firmware served through backend - Schema: chipFamily on Project, flashOffset on ProjectFile - ProjectNew: chipFamily selector + firmware flash offset option - MyOrders: real order list with flash token status and buttons - Dockerfile: prisma db push (no migration files needed for dev) - TOSS_PAYMENT_GUIDE.md: step-by-step guide for real payment after business registration Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,5 +15,5 @@ COPY src ./src
|
||||
|
||||
EXPOSE 3201
|
||||
|
||||
# 마이그레이션 후 서버 시작
|
||||
CMD ["sh", "-c", "npx prisma migrate deploy && node src/index.js"]
|
||||
# prisma db push: 마이그레이션 파일 없이 스키마를 DB에 직접 동기화 (개발용)
|
||||
CMD ["sh", "-c", "npx prisma db push --accept-data-loss && node src/index.js"]
|
||||
|
||||
@@ -60,6 +60,7 @@ model Project {
|
||||
title String
|
||||
description String @db.Text
|
||||
difficultyLevel Int @default(3)
|
||||
chipFamily String @default("ESP32-S3")
|
||||
requiredParts Json?
|
||||
status ProjectStatus @default(draft)
|
||||
adminNote String?
|
||||
@@ -81,6 +82,7 @@ model ProjectFile {
|
||||
fileSize Int
|
||||
mimeType String
|
||||
originalName String?
|
||||
flashOffset String @default("0x0")
|
||||
displayOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
@@ -14,27 +14,15 @@ async function ensureBucket() {
|
||||
const exists = await client.bucketExists(BUCKET);
|
||||
if (!exists) {
|
||||
await client.makeBucket(BUCKET);
|
||||
// 공개 읽기 정책 (이미지·영상 직접 접근용)
|
||||
const policy = JSON.stringify({
|
||||
Version: '2012-10-17',
|
||||
Statement: [{
|
||||
Effect: 'Allow',
|
||||
Principal: { AWS: ['*'] },
|
||||
Action: ['s3:GetObject'],
|
||||
Resource: [`arn:aws:s3:::${BUCKET}/*`],
|
||||
}],
|
||||
});
|
||||
await client.setBucketPolicy(BUCKET, policy);
|
||||
console.log(`MinIO bucket "${BUCKET}" created`);
|
||||
}
|
||||
}
|
||||
|
||||
// 파일 업로드 후 공개 URL 반환
|
||||
// 파일 URL — 플랫폼 백엔드 프록시를 통해 서빙 (CORS 문제 없음)
|
||||
// objectName: "images/uuid.webp" 형식
|
||||
function publicUrl(objectName) {
|
||||
const proto = process.env.MINIO_USE_SSL === 'true' ? 'https' : 'http';
|
||||
const host = process.env.MINIO_ENDPOINT || 'localhost';
|
||||
const port = process.env.MINIO_PORT || '9000';
|
||||
return `${proto}://${host}:${port}/${BUCKET}/${objectName}`;
|
||||
const base = (process.env.BASE_URL || 'http://localhost:3200').replace(/\/$/, '');
|
||||
return `${base}/api/files/${objectName}`;
|
||||
}
|
||||
|
||||
module.exports = { client, BUCKET, ensureBucket, publicUrl };
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
const path = require('path');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const { ensureBucket } = require('./config/minio');
|
||||
const { ensureBucket, client, BUCKET } = require('./config/minio');
|
||||
const prisma = require('./config/db');
|
||||
|
||||
const app = express();
|
||||
@@ -10,7 +11,7 @@ const PORT = process.env.PORT || 3201;
|
||||
|
||||
// ── 기본 미들웨어 ────────────────────────────────────────────────────────────
|
||||
app.set('trust proxy', 1);
|
||||
app.use(helmet());
|
||||
app.use(helmet({ crossOriginResourcePolicy: false }));
|
||||
app.use(cors({
|
||||
origin: process.env.ALLOWED_ORIGIN || '*',
|
||||
credentials: true,
|
||||
@@ -20,8 +21,8 @@ app.use(express.urlencoded({ extended: true, limit: '2mb' }));
|
||||
|
||||
// ── Rate Limiting ────────────────────────────────────────────────────────────
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15분
|
||||
max: 200,
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 300,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
message: { error: '요청이 너무 많습니다. 잠시 후 다시 시도해주세요.' },
|
||||
@@ -33,13 +34,45 @@ const authLimiter = rateLimit({
|
||||
});
|
||||
|
||||
app.use('/api/', limiter);
|
||||
app.use('/api/auth/login', authLimiter);
|
||||
app.use('/api/auth/login', authLimiter);
|
||||
app.use('/api/auth/register', authLimiter);
|
||||
|
||||
// ── MinIO 파일 프록시 ─────────────────────────────────────────────────────────
|
||||
// /api/files/images/xxx.webp → MinIO BUCKET/images/xxx.webp 스트리밍
|
||||
// esp-web-tools가 bin 파일을 직접 fetch할 때 CORS 헤더 포함
|
||||
const MIME = {
|
||||
'.webp': 'image/webp', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
||||
'.png': 'image/png', '.gif': 'image/gif', '.svg': 'image/svg+xml',
|
||||
'.bin': 'application/octet-stream', '.stl': 'model/stl',
|
||||
'.pdf': 'application/pdf', '.mp4': 'video/mp4', '.webm': 'video/webm',
|
||||
};
|
||||
|
||||
app.get('/api/files/*', async (req, res) => {
|
||||
try {
|
||||
const objectName = req.params[0];
|
||||
if (!objectName) return res.status(400).end();
|
||||
|
||||
const ext = path.extname(objectName).toLowerCase();
|
||||
const contentType = MIME[ext] || 'application/octet-stream';
|
||||
|
||||
res.setHeader('Content-Type', contentType);
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Cache-Control', 'public, max-age=86400');
|
||||
|
||||
const stream = await client.getObject(BUCKET, objectName);
|
||||
stream.pipe(res);
|
||||
stream.on('error', () => res.status(404).end());
|
||||
} catch {
|
||||
res.status(404).end();
|
||||
}
|
||||
});
|
||||
|
||||
// ── 라우트 ───────────────────────────────────────────────────────────────────
|
||||
app.use('/api/auth', require('./routes/auth'));
|
||||
app.use('/api/projects', require('./routes/projects'));
|
||||
app.use('/api/products', require('./routes/products'));
|
||||
app.use('/api/orders', require('./routes/orders'));
|
||||
app.use('/api/flash', require('./routes/flash'));
|
||||
app.use('/api/admin', require('./routes/admin'));
|
||||
|
||||
// 헬스 체크
|
||||
|
||||
142
platform/backend/src/routes/flash.js
Normal file
142
platform/backend/src/routes/flash.js
Normal file
@@ -0,0 +1,142 @@
|
||||
const router = require('express').Router();
|
||||
const prisma = require('../config/db');
|
||||
const { writeAuditLog } = require('../middleware/audit');
|
||||
|
||||
// FlashToken 포함 조회 헬퍼
|
||||
async function findToken(token) {
|
||||
return prisma.flashToken.findUnique({
|
||||
where: { token },
|
||||
include: {
|
||||
order: {
|
||||
include: {
|
||||
product: {
|
||||
include: {
|
||||
project: {
|
||||
select: {
|
||||
id: true, title: true, chipFamily: true,
|
||||
files: {
|
||||
where: { fileType: 'firmware' },
|
||||
orderBy: { displayOrder: 'asc' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// GET /api/flash/:token — 토큰 유효성 확인 (Flash 페이지, webflash 둘 다 사용)
|
||||
router.get('/:token', async (req, res) => {
|
||||
try {
|
||||
const ft = await findToken(req.params.token);
|
||||
if (!ft) return res.status(404).json({ error: '유효하지 않은 토큰입니다' });
|
||||
|
||||
const expired = new Date() > new Date(ft.expiresAt);
|
||||
const project = ft.order.product.project;
|
||||
|
||||
res.json({
|
||||
valid: !expired && !ft.isUsed,
|
||||
isUsed: ft.isUsed,
|
||||
expired,
|
||||
expiresAt: ft.expiresAt,
|
||||
productName: project.title,
|
||||
chipFamily: project.chipFamily,
|
||||
hasFirmware: project.files.length > 0,
|
||||
usedAt: ft.usedAt,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: '서버 오류' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/flash/:token/manifest — esp-web-tools 매니페스트 반환
|
||||
// 브라우저에서 직접 fetch하므로 CORS 허용
|
||||
router.get('/:token/manifest', async (req, res) => {
|
||||
try {
|
||||
const ft = await findToken(req.params.token);
|
||||
if (!ft) return res.status(404).json({ error: '유효하지 않은 토큰입니다' });
|
||||
|
||||
if (ft.isUsed) {
|
||||
return res.status(410).json({ error: '이미 플래시에 사용된 토큰입니다' });
|
||||
}
|
||||
if (new Date() > new Date(ft.expiresAt)) {
|
||||
return res.status(410).json({ error: '만료된 토큰입니다' });
|
||||
}
|
||||
|
||||
const project = ft.order.product.project;
|
||||
if (!project.files.length) {
|
||||
return res.status(404).json({ error: '펌웨어 파일이 없습니다. 판매자에게 문의하세요.' });
|
||||
}
|
||||
|
||||
const manifest = {
|
||||
name: project.title,
|
||||
new_install_improv_wait_ms: 0,
|
||||
builds: [{
|
||||
chipFamily: project.chipFamily,
|
||||
parts: project.files.map(f => ({
|
||||
path: f.url,
|
||||
offset: parseInt(f.flashOffset || '0x0', 16),
|
||||
})),
|
||||
}],
|
||||
};
|
||||
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.json(manifest);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: '서버 오류' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/flash/:token/consume — 플래시 완료 기록
|
||||
// esp-web-tools 또는 플래시 페이지에서 호출
|
||||
router.post('/:token/consume', async (req, res) => {
|
||||
try {
|
||||
const ft = await findToken(req.params.token);
|
||||
if (!ft) return res.status(404).json({ error: '유효하지 않은 토큰입니다' });
|
||||
if (ft.isUsed) return res.status(410).json({ error: '이미 사용된 토큰입니다' });
|
||||
if (new Date() > new Date(ft.expiresAt)) return res.status(410).json({ error: '만료된 토큰입니다' });
|
||||
|
||||
const { mac = 'unknown', chipFamily, success = true, errorMessage } = req.body;
|
||||
const project = ft.order.product.project;
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.flashToken.update({
|
||||
where: { id: ft.id },
|
||||
data: { isUsed: true, usedAt: new Date(), macAddress: mac, chipFamily: chipFamily || project.chipFamily },
|
||||
}),
|
||||
prisma.flashLog.create({
|
||||
data: {
|
||||
flashTokenId: ft.id,
|
||||
macAddress: mac,
|
||||
chipFamily: chipFamily || project.chipFamily,
|
||||
firmwareName: project.title,
|
||||
firmwareId: project.id,
|
||||
success: Boolean(success),
|
||||
errorMessage: errorMessage || null,
|
||||
clientIp: req.ip || 'unknown',
|
||||
userAgent: req.headers['user-agent'] || null,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
await writeAuditLog({
|
||||
userId: ft.order.buyerId,
|
||||
action: success ? 'FLASH_SUCCESS' : 'FLASH_FAIL',
|
||||
targetType: 'FlashToken', targetId: ft.id,
|
||||
req, metadata: { mac, chipFamily, firmwareId: project.id }, responseStatus: 200,
|
||||
});
|
||||
|
||||
res.json({ success: true, message: success ? '플래시 완료 기록됨' : '실패 기록됨' });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: '서버 오류' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
195
platform/backend/src/routes/orders.js
Normal file
195
platform/backend/src/routes/orders.js
Normal file
@@ -0,0 +1,195 @@
|
||||
const router = require('express').Router();
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const prisma = require('../config/db');
|
||||
const { requireAuth, requireRole } = require('../middleware/auth');
|
||||
const { writeAuditLog } = require('../middleware/audit');
|
||||
|
||||
// POST /api/orders — 주문 생성 (결제 전)
|
||||
router.post('/', requireAuth, async (req, res) => {
|
||||
const { productId } = req.body;
|
||||
if (!productId) return res.status(400).json({ error: 'productId가 필요합니다' });
|
||||
|
||||
try {
|
||||
const product = await prisma.product.findUnique({
|
||||
where: { id: productId },
|
||||
include: { project: { select: { commissionRate: true, userId: true } } },
|
||||
});
|
||||
if (!product) return res.status(404).json({ error: '상품을 찾을 수 없습니다' });
|
||||
if (!product.isOnSale) return res.status(400).json({ error: '판매 중지된 상품입니다' });
|
||||
if (product.project.userId === req.user.id) return res.status(400).json({ error: '자신의 상품은 구매할 수 없습니다' });
|
||||
|
||||
// 이미 유효한 주문이 있는지 확인 (중복 구매 방지)
|
||||
const existing = await prisma.order.findFirst({
|
||||
where: { buyerId: req.user.id, productId, status: { in: ['pending', 'paid'] } },
|
||||
include: { flashToken: true },
|
||||
});
|
||||
if (existing?.status === 'paid') {
|
||||
return res.status(409).json({
|
||||
error: '이미 구매한 상품입니다',
|
||||
flashToken: existing.flashToken?.token,
|
||||
orderId: existing.id,
|
||||
});
|
||||
}
|
||||
if (existing?.status === 'pending') {
|
||||
return res.json({ orderId: existing.id, amount: existing.amount, status: 'pending' });
|
||||
}
|
||||
|
||||
const commissionAmount = Math.round(product.price * product.project.commissionRate);
|
||||
const sellerAmount = product.price - commissionAmount;
|
||||
|
||||
const order = await prisma.order.create({
|
||||
data: {
|
||||
buyerId: req.user.id,
|
||||
productId,
|
||||
amount: product.price,
|
||||
commissionAmount,
|
||||
sellerAmount,
|
||||
paymentGateway: 'toss',
|
||||
tossOrderId: `mock_${uuidv4()}`,
|
||||
buyerInfo: { email: req.user.email, nickname: req.user.nickname },
|
||||
deviceInfo: { ip: req.ip, userAgent: req.headers['user-agent'] },
|
||||
},
|
||||
});
|
||||
|
||||
await writeAuditLog({ userId: req.user.id, action: 'ORDER_CREATE', targetType: 'Order', targetId: order.id, req, responseStatus: 201 });
|
||||
res.status(201).json({ orderId: order.id, amount: order.amount, status: 'pending' });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: '서버 오류' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/orders/:id/mock-pay — 모의 결제 (사업자 등록 전 테스트용)
|
||||
// 실제 결제 없이 즉시 paid 처리하고 FlashToken 발급
|
||||
router.post('/:id/mock-pay', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const order = await prisma.order.findUnique({
|
||||
where: { id: req.params.id },
|
||||
include: { flashToken: true },
|
||||
});
|
||||
if (!order) return res.status(404).json({ error: '주문을 찾을 수 없습니다' });
|
||||
if (order.buyerId !== req.user.id) return res.status(403).json({ error: '권한이 없습니다' });
|
||||
if (order.status === 'paid') {
|
||||
return res.json({ flashToken: order.flashToken?.token, orderId: order.id, alreadyPaid: true });
|
||||
}
|
||||
if (order.status !== 'pending') {
|
||||
return res.status(400).json({ error: `${order.status} 상태의 주문은 결제할 수 없습니다` });
|
||||
}
|
||||
|
||||
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30일
|
||||
|
||||
const [updatedOrder, flashToken] = await prisma.$transaction([
|
||||
prisma.order.update({
|
||||
where: { id: order.id },
|
||||
data: { status: 'paid', paidAt: new Date(), paymentKey: `mock_${uuidv4()}` },
|
||||
}),
|
||||
prisma.flashToken.create({
|
||||
data: { orderId: order.id, expiresAt },
|
||||
}),
|
||||
prisma.product.update({
|
||||
where: { id: order.productId },
|
||||
data: { totalSales: { increment: 1 } },
|
||||
}),
|
||||
]);
|
||||
|
||||
await writeAuditLog({
|
||||
userId: req.user.id, action: 'MOCK_PAYMENT',
|
||||
targetType: 'Order', targetId: order.id,
|
||||
req, metadata: { amount: order.amount, mode: 'mock' }, responseStatus: 200,
|
||||
});
|
||||
|
||||
res.json({ flashToken: flashToken.token, orderId: order.id, expiresAt: flashToken.expiresAt });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: '서버 오류' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/orders/me — 내 주문 목록
|
||||
router.get('/me', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const orders = await prisma.order.findMany({
|
||||
where: { buyerId: req.user.id },
|
||||
orderBy: { orderedAt: 'desc' },
|
||||
include: {
|
||||
product: {
|
||||
select: {
|
||||
id: true, price: true,
|
||||
project: { select: { title: true, chipFamily: true, files: { where: { fileType: 'image' }, take: 1 } } },
|
||||
},
|
||||
},
|
||||
flashToken: { select: { token: true, isUsed: true, expiresAt: true } },
|
||||
review: { select: { id: true } },
|
||||
},
|
||||
});
|
||||
res.json(orders);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: '서버 오류' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/orders/:id
|
||||
router.get('/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const order = await prisma.order.findUnique({
|
||||
where: { id: req.params.id },
|
||||
include: {
|
||||
product: { include: { project: true } },
|
||||
flashToken: true,
|
||||
},
|
||||
});
|
||||
if (!order) return res.status(404).json({ error: '주문을 찾을 수 없습니다' });
|
||||
if (order.buyerId !== req.user.id && req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: '권한이 없습니다' });
|
||||
}
|
||||
res.json(order);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: '서버 오류' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/orders/:id/refund — 환불 (모의 / 플래시 미완료 시만)
|
||||
router.post('/:id/refund', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const order = await prisma.order.findUnique({
|
||||
where: { id: req.params.id },
|
||||
include: { flashToken: true },
|
||||
});
|
||||
if (!order) return res.status(404).json({ error: '주문을 찾을 수 없습니다' });
|
||||
if (order.buyerId !== req.user.id && req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: '권한이 없습니다' });
|
||||
}
|
||||
if (order.status !== 'paid') return res.status(400).json({ error: '결제 완료 상태가 아닙니다' });
|
||||
if (order.flashToken?.isUsed && req.user.role !== 'admin') {
|
||||
return res.status(400).json({ error: '플래시가 완료된 주문은 환불할 수 없습니다' });
|
||||
}
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.order.update({
|
||||
where: { id: order.id },
|
||||
data: { status: 'refunded', refundedAt: new Date(), refundReason: req.body.reason || '사용자 요청' },
|
||||
}),
|
||||
// FlashToken 무효화
|
||||
...(order.flashToken ? [prisma.flashToken.update({
|
||||
where: { id: order.flashToken.id },
|
||||
data: { expiresAt: new Date() },
|
||||
})] : []),
|
||||
prisma.product.update({
|
||||
where: { id: order.productId },
|
||||
data: { totalSales: { decrement: 1 } },
|
||||
}),
|
||||
]);
|
||||
|
||||
await writeAuditLog({
|
||||
userId: req.user.id, action: 'REFUND',
|
||||
targetType: 'Order', targetId: order.id,
|
||||
req, responseStatus: 200,
|
||||
});
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: '서버 오류' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -108,7 +108,7 @@ router.post('/', requireAuth, [
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(400).json({ error: errors.array()[0].msg });
|
||||
|
||||
const { title, description, difficultyLevel, requiredParts } = req.body;
|
||||
const { title, description, difficultyLevel, chipFamily, requiredParts } = req.body;
|
||||
try {
|
||||
const project = await prisma.project.create({
|
||||
data: {
|
||||
@@ -116,6 +116,7 @@ router.post('/', requireAuth, [
|
||||
title,
|
||||
description,
|
||||
difficultyLevel: difficultyLevel ? parseInt(difficultyLevel) : 3,
|
||||
chipFamily: chipFamily || 'ESP32-S3',
|
||||
requiredParts: requiredParts || [],
|
||||
},
|
||||
});
|
||||
@@ -138,13 +139,14 @@ router.put('/:id', requireAuth, async (req, res) => {
|
||||
return res.status(400).json({ error: '승인 대기/완료 상태에서는 수정할 수 없습니다' });
|
||||
}
|
||||
|
||||
const { title, description, difficultyLevel, requiredParts } = req.body;
|
||||
const { title, description, difficultyLevel, chipFamily, requiredParts } = req.body;
|
||||
const updated = await prisma.project.update({
|
||||
where: { id: req.params.id },
|
||||
data: {
|
||||
...(title && { title }),
|
||||
...(description && { description }),
|
||||
...(difficultyLevel && { difficultyLevel: parseInt(difficultyLevel) }),
|
||||
...(chipFamily && { chipFamily }),
|
||||
...(requiredParts !== undefined && { requiredParts }),
|
||||
},
|
||||
});
|
||||
@@ -205,7 +207,8 @@ router.post('/:id/files', requireAuth, upload.array('files', 10), async (req, re
|
||||
if (project.userId !== req.user.id) return res.status(403).json({ error: '권한이 없습니다' });
|
||||
if (!req.files?.length) return res.status(400).json({ error: '파일이 없습니다' });
|
||||
|
||||
const fileType = req.body.fileType || 'image'; // image|video|stl|wiring|firmware
|
||||
const fileType = req.body.fileType || 'image'; // image|video|stl|wiring|firmware
|
||||
const flashOffset = req.body.flashOffset || '0x0';
|
||||
const results = [];
|
||||
|
||||
for (const file of req.files) {
|
||||
@@ -225,6 +228,7 @@ router.post('/:id/files', requireAuth, upload.array('files', 10), async (req, re
|
||||
fileSize: stored.fileSize,
|
||||
mimeType: stored.mimeType,
|
||||
originalName: file.originalname,
|
||||
flashOffset: fileType === 'firmware' ? flashOffset : '0x0',
|
||||
displayOrder: results.length,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user