/* ============================================================
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 && (
)}
{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) => (
))}
)}
{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월 캠페인" />
{ALL_MONTHS.map((m) => {
const on = months.has(m.month);
return (
toggleMonth(m.month)}>
{on && }
{m.label}
);
})}
이 운영 사이클에 포함할 월을 선택합니다. 에피소드 제출은 월별 수량 제한 없이 자유롭게 진행됩니다.
LGE에게는 '진행 중' 프로젝트만 노출됩니다.
{err && {err}
}
);
}
Object.assign(window, { Home, ProjectForm });