diff --git a/frontend/src/components/VerificationBoardPage.tsx b/frontend/src/components/VerificationBoardPage.tsx index 8baefc2..4ccb5d1 100644 --- a/frontend/src/components/VerificationBoardPage.tsx +++ b/frontend/src/components/VerificationBoardPage.tsx @@ -10,13 +10,12 @@ 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 { prepareAttachmentsForUpload } from '../utils/verificationAttachmentFiles'; import '../styles/verificationBoard.css'; interface Props { @@ -148,18 +147,14 @@ const VerificationBoardPage: React.FC = ({ 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); + const prepared = await prepareAttachmentsForUpload(pendingImages); await uploadVerificationIssueImages(issueId, prepared); } setAttachmentsRefreshKey(k => k + 1); @@ -173,6 +168,10 @@ const VerificationBoardPage: React.FC = ({ } }, [reload, stageFilter]); + const handleAttachmentsChanged = useCallback(() => { + setAttachmentsRefreshKey(k => k + 1); + }, []); + return ( <> = ({ attachmentsRefreshKey={attachmentsRefreshKey} saving={detailSaving} onSave={handleDetailSave} + onAttachmentsChanged={handleAttachmentsChanged} /> )} /> diff --git a/frontend/src/components/verificationBoard/VerificationIssueAttachmentsSection.tsx b/frontend/src/components/verificationBoard/VerificationIssueAttachmentsSection.tsx index dd60f47..b494314 100644 --- a/frontend/src/components/verificationBoard/VerificationIssueAttachmentsSection.tsx +++ b/frontend/src/components/verificationBoard/VerificationIssueAttachmentsSection.tsx @@ -53,14 +53,13 @@ interface Props { /** 등록 draft — 저장 전 로컬 파일 */ draftFiles?: File[]; onDraftFilesChange?: (files: File[]) => void; - /** 상세 화면 — 저장 시 삭제할 서버 이미지 ID */ - pendingDeleteIds?: number[]; - onPendingDeleteIdsChange?: (ids: number[]) => void; - /** true: 추가·삭제를 저장 버튼까지 지연 (상세 편집) */ + /** true: 새 첨부 업로드만 저장 시 반영 (삭제는 즉시 API) */ deferUpload?: boolean; disabled?: boolean; - /** 모달 등 좁은 영역 */ + /** compact: 모달 등 좁은 영역 */ compact?: boolean; + /** 첨부 추가·삭제 후 (목록 갱신 등) */ + onAttachmentsChanged?: () => void; } interface CtxMenuState { @@ -72,11 +71,10 @@ const VerificationIssueAttachmentsSection: React.FC = ({ issueId = null, draftFiles = [], onDraftFilesChange, - pendingDeleteIds = [], - onPendingDeleteIdsChange, deferUpload = false, disabled = false, compact = false, + onAttachmentsChanged, }) => { const isDraft = issueId == null; const saveOnSubmit = deferUpload && issueId != null; @@ -169,6 +167,7 @@ const VerificationIssueAttachmentsSection: React.FC = ({ const prepared = await prepareAttachmentsForUpload(picked); await uploadVerificationIssueImages(issueId, prepared); await reload(); + onAttachmentsChanged?.(); } catch (e) { console.warn('[VerificationBoard] attachment upload', e); window.alert(uploadErrorMessage(e)); @@ -203,20 +202,16 @@ const VerificationIssueAttachmentsSection: React.FC = ({ const handleDeleteServer = async (imageId: number) => { if (issueId == null) return; if (!window.confirm('이 첨부 파일을 삭제할까요?')) return; - if (saveOnSubmit) { - onPendingDeleteIdsChange?.( - pendingDeleteIds.includes(imageId) - ? pendingDeleteIds - : [...pendingDeleteIds, imageId], - ); - return; - } + setUploading(true); try { await deleteVerificationIssueImage(issueId, imageId); await reload(); + onAttachmentsChanged?.(); } catch (e) { - console.warn('[VerificationBoard] image delete', e); - window.alert('이미지 삭제에 실패했습니다.'); + console.warn('[VerificationBoard] attachment delete', e); + window.alert('첨부 파일 삭제에 실패했습니다.'); + } finally { + setUploading(false); } }; @@ -275,8 +270,8 @@ const VerificationIssueAttachmentsSection: React.FC = ({ }; const visibleServerImages = useMemo( - () => images.filter(img => img.id != null && !pendingDeleteIds.includes(img.id)), - [images, pendingDeleteIds], + () => images.filter(img => img.id != null), + [images], ); const totalCount = isDraft || saveOnSubmit diff --git a/frontend/src/components/verificationBoard/VerificationIssueDetailPanel.tsx b/frontend/src/components/verificationBoard/VerificationIssueDetailPanel.tsx index 784f81f..f676a90 100644 --- a/frontend/src/components/verificationBoard/VerificationIssueDetailPanel.tsx +++ b/frontend/src/components/verificationBoard/VerificationIssueDetailPanel.tsx @@ -20,8 +20,8 @@ interface Props { onSave?: ( dto: VerificationIssueDto, pendingImages: File[], - pendingDeleteIds: number[], ) => void | Promise; + onAttachmentsChanged?: () => void; } const VerificationIssueDetailPanel: React.FC = ({ @@ -29,13 +29,13 @@ const VerificationIssueDetailPanel: React.FC = ({ attachmentsRefreshKey = 0, saving = false, onSave, + onAttachmentsChanged, }) => { 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 ?? ''); @@ -43,7 +43,6 @@ const VerificationIssueDetailPanel: React.FC = ({ setReproductionPath(src.reproductionPath ?? ''); setStage(src.stage ?? 'REGISTERED'); setPendingImages([]); - setPendingDeleteIds([]); }, []); useEffect(() => { @@ -60,9 +59,8 @@ const VerificationIssueDetailPanel: React.FC = ({ || pathTrimmed !== issuePath || stage !== issue.stage || pendingImages.length > 0 - || pendingDeleteIds.length > 0 ); - }, [issue, title, content, reproductionPath, stage, pendingImages, pendingDeleteIds]); + }, [issue, title, content, reproductionPath, stage, pendingImages]); const handleCancel = () => { if (issue) resetFromIssue(issue); @@ -85,7 +83,6 @@ const VerificationIssueDetailPanel: React.FC = ({ stage, }, pendingImages, - pendingDeleteIds, ); }; @@ -163,9 +160,8 @@ const VerificationIssueDetailPanel: React.FC = ({ deferUpload draftFiles={pendingImages} onDraftFilesChange={setPendingImages} - pendingDeleteIds={pendingDeleteIds} - onPendingDeleteIdsChange={setPendingDeleteIds} disabled={saving} + onAttachmentsChanged={onAttachmentsChanged} /> )} diff --git a/frontend/src/styles/verificationBoard.css b/frontend/src/styles/verificationBoard.css index 1a14d05..e45a452 100644 --- a/frontend/src/styles/verificationBoard.css +++ b/frontend/src/styles/verificationBoard.css @@ -910,8 +910,13 @@ font-size: 12px; line-height: 1; cursor: pointer; - opacity: 0; - transition: opacity 0.15s; + opacity: 0.82; + transition: opacity 0.15s, background 0.15s; +} + +.vbd-attach-remove:hover:not(:disabled) { + opacity: 1; + background: rgba(198, 40, 40, 0.88); } .vbd-attach-item:hover .vbd-attach-remove {