/** * 검증게시판 — 좌 목록 + 우 상세 */ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import BuilderPageShell from './layout/BuilderPageShell'; import VerificationIssueListPanel from './verificationBoard/VerificationIssueListPanel'; import VerificationIssueDetailPanel from './verificationBoard/VerificationIssueDetailPanel'; import VerificationIssueFormModal from './verificationBoard/VerificationIssueFormModal'; import type { Theme } from '../types'; import type { VerificationIssueStage } from '../utils/verificationBoardStages'; import { deleteVerificationIssue, deleteVerificationIssueImage, loadVerificationIssues, saveVerificationIssue, uploadVerificationIssueImages, type VerificationIssueDto, } from '../utils/verificationBoardApi'; import { prepareImagesForUpload } from '../utils/clipboardImageFiles'; import '../styles/verificationBoard.css'; interface Props { theme?: Theme; focusIssueId?: number | null; onFocusIssueConsumed?: () => void; } const IconAdd = () => ( ); const IconEdit = () => ( ); const IconDelete = () => ( ); const VerificationBoardPage: React.FC = ({ theme = 'dark', focusIssueId = null, onFocusIssueConsumed, }) => { const [issues, setIssues] = useState([]); const [selectedId, setSelectedId] = useState(null); const [stageFilter, setStageFilter] = useState(''); const [searchQuery, setSearchQuery] = useState(''); const [loading, setLoading] = useState(true); const [modalOpen, setModalOpen] = useState(false); const [editTarget, setEditTarget] = useState(null); const [saving, setSaving] = useState(false); const [detailSaving, setDetailSaving] = useState(false); const [attachmentsRefreshKey, setAttachmentsRefreshKey] = useState(0); const reload = useCallback(async (stage?: VerificationIssueStage | '') => { setLoading(true); try { const list = await loadVerificationIssues(stage ?? stageFilter); setIssues(list); setSelectedId(prev => { if (prev != null && list.some(i => i.id === prev)) return prev; return list[0]?.id ?? null; }); } catch (e) { console.warn('[VerificationBoard] load', e); window.alert('검증 이슈 목록을 불러오지 못했습니다.'); } finally { setLoading(false); } }, [stageFilter]); useEffect(() => { void reload(stageFilter); }, [stageFilter]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (focusIssueId == null) return; setSelectedId(focusIssueId); setStageFilter(''); onFocusIssueConsumed?.(); }, [focusIssueId, onFocusIssueConsumed]); const selected = useMemo( () => issues.find(i => i.id === selectedId) ?? null, [issues, selectedId], ); const handleSelect = useCallback((issue: VerificationIssueDto) => { if (issue.id != null) setSelectedId(issue.id); }, []); const handleAdd = useCallback(() => { setEditTarget(null); setModalOpen(true); }, []); const handleEdit = useCallback(() => { if (!selected) { window.alert('수정할 이슈를 선택하세요.'); return; } setEditTarget(selected); setModalOpen(true); }, [selected]); const handleDelete = useCallback(async () => { if (!selected?.id) { window.alert('삭제할 이슈를 선택하세요.'); return; } if (!window.confirm(`「${selected.title}」 이슈를 삭제할까요?`)) return; try { await deleteVerificationIssue(selected.id); await reload(stageFilter); } catch (e) { console.warn('[VerificationBoard] delete', e); window.alert('삭제에 실패했습니다.'); } }, [selected, reload, stageFilter]); const handleSave = useCallback(async (dto: VerificationIssueDto, pendingImages: File[]) => { setSaving(true); try { const saved = await saveVerificationIssue(dto); if (pendingImages.length > 0 && saved.id != null) { await uploadVerificationIssueImages(saved.id, pendingImages); } setModalOpen(false); setEditTarget(null); setAttachmentsRefreshKey(k => k + 1); await reload(stageFilter); if (saved.id != null) setSelectedId(saved.id); } catch (e) { console.warn('[VerificationBoard] save', e); window.alert('저장에 실패했습니다.'); } finally { setSaving(false); } }, [reload, stageFilter]); const handleDetailSave = useCallback(async ( dto: VerificationIssueDto, pendingImages: File[], pendingDeleteIds: number[], ) => { if (dto.id == null) return; setDetailSaving(true); try { const saved = await saveVerificationIssue(dto); const issueId = saved.id ?? dto.id; for (const imageId of pendingDeleteIds) { await deleteVerificationIssueImage(issueId, imageId); } if (pendingImages.length > 0) { const prepared = await prepareImagesForUpload(pendingImages); await uploadVerificationIssueImages(issueId, prepared); } setAttachmentsRefreshKey(k => k + 1); await reload(stageFilter); if (issueId != null) setSelectedId(issueId); } catch (e) { console.warn('[VerificationBoard] detail save', e); window.alert('저장에 실패했습니다.'); } finally { setDetailSaving(false); } }, [reload, stageFilter]); return ( <> )} leftStorageKey="vbd-left-width" leftDefaultWidth={320} leftCollapsedStorageKey="vbd-left-open" collapsiblePanels leftTitle="검증 이슈" left={( )} center={( )} /> void handleSave(dto, pendingImages)} onCancel={() => { setModalOpen(false); setEditTarget(null); }} /> ); }; export default VerificationBoardPage;