/* ============================================================ Home (project grid) + Project create/edit modal ============================================================ */ const { useState: useSH, useMemo: useMH } = React; const ALL_MONTHS = Array.from({ length: 12 }, (_, i) => ({ month: i + 1, label: (i + 1) + '월' })); /* ---------------- Project card ---------------- */ function ProjectCard({ p, isStaff, onOpen, onEdit, onStatus, onDelete }) { return (
onOpen(p)}>
{p.name}
{p.months.map((m) => m.month).join(' · ')}월
{isStaff && ( e.stopPropagation()}>}> {p.status !== 'not_started' && } {p.status !== 'active' && } {p.status !== 'done' && }
)}
{p.description &&
{p.description}
}
{p.currentSteps && p.currentSteps.length ? 현재: {p.currentSteps.map((s) => s.label).join(', ')} : 단계 미지정} 열기
); } /* ---------------- Home ---------------- */ function Home({ user, navigate, toast }) { const isStaff = user.role === 'ADMIN' || user.role === 'CNX'; const { loading, error, data, reload, setData } = useAsync(() => window.api.getProjects(), [user.id]); const [form, setForm] = useSH(null); // {mode, project} const [confirm, setConfirm] = useSH(null); const stats = useMH(() => { const list = data || []; const by = (s) => list.filter((p) => p.status === s).length; return { total: list.length, notStarted: by('not_started'), active: by('active'), done: by('done') }; }, [data]); const openProject = (p) => { if (!p.months.length) return; navigate({ name: 'project', projectId: p.id, tab: 'ops', month: p.months[0].month }); }; const doStatus = async (p, status) => { await window.api.setProjectStatus(p.id, status); toast(`'${p.name}' → ${STATUS_LABEL[status]}`); reload(); }; const doDelete = async () => { const p = confirm; setConfirm(null); await window.api.deleteProject(p.id); toast(`'${p.name}' 삭제됨`); reload(); }; const onSaved = (saved, mode) => { setForm(null); toast(mode === 'create' ? '새 프로젝트가 생성되었습니다' : '프로젝트가 수정되었습니다'); reload(); }; return (
프로젝트
{isStaff ? '운영 사이클을 등록하고 월·테마·에피소드를 관리합니다.' : '진행 중인 운영 사이클을 확인하고 에피소드를 선택·제출합니다.'}
{isStaff && } onClick={() => setForm({ mode: 'create' })}>새 프로젝트}
{/* stat strip */} {!loading && !error && (
{[ { v: stats.total, l: '전체 프로젝트', c: 'var(--ink)', bar: 'var(--ink-3)' }, { v: stats.notStarted, l: '시작 전', c: 'var(--slate)', bar: 'var(--slate)' }, { v: stats.active, l: '진행중', c: 'var(--pub-green-700)', bar: 'var(--pub-green)' }, { v: stats.done, l: '완료', c: 'var(--cnx-blue-700)', bar: 'var(--cnx-blue)' }, ].map((s, i) => (
{s.v}
{s.l}
))}
)} {loading &&
{[0, 1, 2].map((i) => )}
} {error && } {!loading && !error && data.length === 0 && ( } title="프로젝트가 없습니다" desc={isStaff ? '첫 운영 사이클을 만들어 시작하세요.' : '현재 진행 중인 프로젝트가 없습니다. 운영팀(CNX)에 문의하세요.'} action={isStaff ? } onClick={() => setForm({ mode: 'create' })}>새 프로젝트 : null} /> )} {!loading && !error && data.length > 0 && (
{data.map((p) => ( setForm({ mode: 'edit', project: p })} onStatus={doStatus} onDelete={(p) => setConfirm(p)} /> ))} {isStaff && ( )}
)} {form && setForm(null)} onSaved={onSaved} />} {confirm && ( setConfirm(null)} footer={<> setConfirm(null)}>취소} onClick={doDelete}>삭제}>

이 프로젝트와 연결된 테마·에피소드·선택·제출 데이터가 모두 삭제됩니다. 이 작업은 되돌릴 수 없습니다.

)}
); } /* ---------------- Project create / edit ---------------- */ function ProjectForm({ mode, project, onClose, onSaved }) { const [name, setName] = useSH(project?.name || ''); const [status, setStatus] = useSH(project?.status || 'not_started'); const [months, setMonths] = useSH(() => new Set((project?.months || []).map((m) => m.month))); const [desc, setDesc] = useSH(project?.description || ''); const [busy, setBusy] = useSH(false); const [err, setErr] = useSH(''); const toggleMonth = (m) => setMonths((prev) => { const next = new Set(prev); if (next.has(m)) next.delete(m); else next.add(m); return next; }); const save = async () => { if (!name.trim()) return setErr('프로젝트 이름을 입력하세요.'); const monthArr = [...months].sort((a, b) => a - b); if (!monthArr.length) return setErr('포함할 월을 1개 이상 선택하세요.'); setBusy(true); setErr(''); const body = { name: name.trim(), status, description: desc.trim() || undefined, months: monthArr.map((m) => ({ month: m, label: m + '월' })), }; try { const saved = mode === 'create' ? await window.api.createProject(body) : await window.api.updateProject(project.id, body); if (mode === 'edit' && status !== project.status) await window.api.setProjectStatus(project.id, status); onSaved(saved, mode); } catch (e) { setErr(e.message); setBusy(false); } }; return ( 취소 : }>{mode === 'create' ? '생성' : '저장'}}>
setName(e.target.value)} placeholder="예: LG AI 365 — 8·9월 캠페인" />