/** * 검증게시판 — 우측 이슈 상세 (인라인 편집 + 저장) */ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import type { VerificationIssueDto } from '../../utils/verificationBoardApi'; import { VERIFICATION_ISSUE_STAGES, stageColor, type VerificationIssueStage, } from '../../utils/verificationBoardStages'; import ReproductionPathInput from './ReproductionPathInput'; import VerificationIssueCommentsSection from './VerificationIssueCommentsSection'; import VerificationIssueAttachmentsSection from './VerificationIssueAttachmentsSection'; interface Props { issue: VerificationIssueDto | null; attachmentsRefreshKey?: number; saving?: boolean; onSave?: ( dto: VerificationIssueDto, pendingImages: File[], pendingDeleteIds: number[], ) => void | Promise; } function formatDateTime(iso?: string): string { if (!iso) return '—'; try { return new Date(iso).toLocaleString('ko-KR'); } catch { return iso; } } const VerificationIssueDetailPanel: React.FC = ({ issue, attachmentsRefreshKey = 0, saving = false, onSave, }) => { const [title, setTitle] = useState(''); const [content, setContent] = useState(''); const [reproductionPath, setReproductionPath] = useState(''); const [stage, setStage] = useState('REGISTERED'); const [pendingImages, setPendingImages] = useState([]); const [pendingDeleteIds, setPendingDeleteIds] = useState([]); const resetFromIssue = useCallback((src: VerificationIssueDto) => { setTitle(src.title ?? ''); setContent(src.content ?? ''); setReproductionPath(src.reproductionPath ?? ''); setStage(src.stage ?? 'REGISTERED'); setPendingImages([]); setPendingDeleteIds([]); }, []); useEffect(() => { if (issue) resetFromIssue(issue); }, [issue?.id, attachmentsRefreshKey, resetFromIssue]); // eslint-disable-line react-hooks/exhaustive-deps const dirty = useMemo(() => { if (!issue) return false; const pathTrimmed = reproductionPath.trim(); const issuePath = (issue.reproductionPath ?? '').trim(); return ( title.trim() !== (issue.title ?? '').trim() || content.trim() !== (issue.content ?? '').trim() || pathTrimmed !== issuePath || stage !== issue.stage || pendingImages.length > 0 || pendingDeleteIds.length > 0 ); }, [issue, title, content, reproductionPath, stage, pendingImages, pendingDeleteIds]); const handleCancel = () => { if (issue) resetFromIssue(issue); }; const handleSave = () => { if (!issue?.id || !onSave) return; const trimmed = title.trim(); if (!trimmed) { window.alert('제목을 입력하세요.'); return; } const pathTrimmed = reproductionPath.trim(); void onSave( { id: issue.id, title: trimmed, content: content.trim(), reproductionPath: pathTrimmed || undefined, stage, }, pendingImages, pendingDeleteIds, ); }; if (!issue) { return (

좌측 목록에서 검증 이슈를 선택하세요.

); } return (
setTitle(e.target.value)} maxLength={300} disabled={saving} placeholder="검증 이슈 제목" aria-label="제목" />
등록 {formatDateTime(issue.createdAt)} 수정 {formatDateTime(issue.updatedAt)}