Files
webflash/platform/frontend/src/pages/Dashboard/ProjectEdit.jsx
root bdef4b7ae0 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>
2026-05-20 06:05:46 +09:00

176 lines
6.7 KiB
JavaScript

import { useEffect, useState, useRef } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import api from '../../api/client';
const STATUS_LABEL = {
draft: '임시저장', pending: '검토 대기', approved: '승인됨',
rejected: '반려됨', suspended: '정지됨',
};
export default function ProjectEdit() {
const { id } = useParams();
const navigate = useNavigate();
const [project, setProject] = useState(null);
const [form, setForm] = useState({});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const [msg, setMsg] = useState('');
const [files, setFiles] = useState([]);
const [fileType, setFileType] = useState('image');
const fileRef = useRef();
useEffect(() => {
api.get(`/projects/${id}`)
.then(r => {
setProject(r.data);
setForm({ title: r.data.title, description: r.data.description, difficultyLevel: r.data.difficultyLevel });
})
.catch(() => navigate('/dashboard'))
.finally(() => setLoading(false));
}, [id]);
async function handleSave(e) {
e.preventDefault();
setSaving(true);
try {
await api.put(`/projects/${id}`, form);
setMsg('저장되었습니다');
} catch (err) {
setError(err.response?.data?.error || '저장 실패');
} finally {
setSaving(false);
}
}
async function handleUpload() {
if (!files.length) return;
setSaving(true);
try {
const fd = new FormData();
fd.append('fileType', fileType);
files.forEach(f => fd.append('files', f));
const { data } = await api.post(`/projects/${id}/files`, fd, {
headers: { 'Content-Type': 'multipart/form-data' },
});
setProject(p => ({ ...p, files: [...(p.files || []), ...data] }));
setFiles([]);
setMsg('파일이 업로드되었습니다');
} catch (err) {
setError(err.response?.data?.error || '업로드 실패');
} finally {
setSaving(false);
}
}
async function handleDeleteFile(fileId) {
if (!confirm('파일을 삭제하시겠습니까?')) return;
await api.delete(`/projects/${id}/files/${fileId}`);
setProject(p => ({ ...p, files: p.files.filter(f => f.id !== fileId) }));
}
async function handleSubmit() {
setSaving(true);
try {
await api.post(`/projects/${id}/submit`);
navigate('/dashboard');
} catch (err) {
setError(err.response?.data?.error || '제출 실패');
} finally {
setSaving(false);
}
}
if (loading) return <div className="spinner" />;
const canEdit = ['draft', 'rejected'].includes(project?.status);
return (
<div className="container page" style={{ maxWidth: 700 }}>
<div className="flex items-center justify-between" style={{ marginBottom: 24 }}>
<h2>프로젝트 관리</h2>
<span className={`badge badge-${project.status}`}>{STATUS_LABEL[project.status]}</span>
</div>
{project.adminNote && (
<div className="alert alert-warn">
<strong>관리자 메모:</strong> {project.adminNote}
</div>
)}
{error && <div className="alert alert-error">{error}</div>}
{msg && <div className="alert alert-success">{msg}</div>}
{/* 기본 정보 수정 */}
<form className="card" onSubmit={handleSave} style={{ marginBottom: 20 }}>
<h3 style={{ marginBottom: 16 }}>기본 정보</h3>
<div className="form-group">
<label>제목</label>
<input required disabled={!canEdit} value={form.title || ''}
onChange={e => setForm(f => ({ ...f, title: e.target.value }))} />
</div>
<div className="form-group">
<label>설명</label>
<textarea disabled={!canEdit} value={form.description || ''}
onChange={e => setForm(f => ({ ...f, description: e.target.value }))} rows={6} />
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button className="btn btn-primary" disabled={!canEdit || saving}>
{saving ? '저장 중...' : '저장'}
</button>
{['draft', 'rejected'].includes(project.status) && (
<button type="button" className="btn btn-success" onClick={handleSubmit} disabled={saving}>
검토 요청
</button>
)}
</div>
</form>
{/* 파일 관리 */}
<div className="card">
<h3 style={{ marginBottom: 16 }}>파일 관리</h3>
{project.files?.length > 0 && (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(100px, 1fr))', gap: 8, marginBottom: 20 }}>
{project.files.map(f => (
<div key={f.id} style={{ position: 'relative', borderRadius: 4, overflow: 'hidden', background: 'var(--bg3)' }}>
{f.fileType === 'image'
? <img src={f.thumbnailUrl || f.url} alt="" style={{ width: '100%', height: 80, objectFit: 'cover' }} />
: <div style={{ height: 80, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 24 }}>
{f.fileType === 'stl' ? '🖨️' : f.fileType === 'firmware' ? '💾' : '📄'}
</div>
}
<button onClick={() => handleDeleteFile(f.id)}
style={{ position: 'absolute', top: 2, right: 2, background: 'var(--danger)', border: 'none',
borderRadius: 4, color: '#fff', cursor: 'pointer', fontSize: 11, padding: '2px 5px' }}>
</button>
</div>
))}
</div>
)}
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
<select value={fileType} onChange={e => setFileType(e.target.value)} style={{ width: 160 }}>
<option value="image">이미지</option>
<option value="video">영상</option>
<option value="wiring">배선도</option>
<option value="stl">STL</option>
<option value="firmware">펌웨어</option>
</select>
<button className="btn btn-outline" onClick={() => fileRef.current.click()}>파일 선택</button>
<input ref={fileRef} type="file" multiple hidden
onChange={e => setFiles(Array.from(e.target.files))} />
{files.length > 0 && (
<button className="btn btn-primary" onClick={handleUpload} disabled={saving}>
{saving ? '업로드 중...' : `${files.length}개 업로드`}
</button>
)}
</div>
{files.length > 0 && (
<div className="text-muted" style={{ fontSize: 12 }}>
{files.map(f => f.name).join(', ')}
</div>
)}
</div>
</div>
);
}