검증게시판 첨부이미지 삭제 기능

This commit is contained in:
Macbook
2026-06-13 01:48:06 +09:00
parent 864d85484d
commit 3a4cc1ff3e
4 changed files with 32 additions and 36 deletions
@@ -10,13 +10,12 @@ import type { Theme } from '../types';
import type { VerificationIssueStage } from '../utils/verificationBoardStages'; import type { VerificationIssueStage } from '../utils/verificationBoardStages';
import { import {
deleteVerificationIssue, deleteVerificationIssue,
deleteVerificationIssueImage,
loadVerificationIssues, loadVerificationIssues,
saveVerificationIssue, saveVerificationIssue,
uploadVerificationIssueImages, uploadVerificationIssueImages,
type VerificationIssueDto, type VerificationIssueDto,
} from '../utils/verificationBoardApi'; } from '../utils/verificationBoardApi';
import { prepareImagesForUpload } from '../utils/clipboardImageFiles'; import { prepareAttachmentsForUpload } from '../utils/verificationAttachmentFiles';
import '../styles/verificationBoard.css'; import '../styles/verificationBoard.css';
interface Props { interface Props {
@@ -148,18 +147,14 @@ const VerificationBoardPage: React.FC<Props> = ({
const handleDetailSave = useCallback(async ( const handleDetailSave = useCallback(async (
dto: VerificationIssueDto, dto: VerificationIssueDto,
pendingImages: File[], pendingImages: File[],
pendingDeleteIds: number[],
) => { ) => {
if (dto.id == null) return; if (dto.id == null) return;
setDetailSaving(true); setDetailSaving(true);
try { try {
const saved = await saveVerificationIssue(dto); const saved = await saveVerificationIssue(dto);
const issueId = saved.id ?? dto.id; const issueId = saved.id ?? dto.id;
for (const imageId of pendingDeleteIds) {
await deleteVerificationIssueImage(issueId, imageId);
}
if (pendingImages.length > 0) { if (pendingImages.length > 0) {
const prepared = await prepareImagesForUpload(pendingImages); const prepared = await prepareAttachmentsForUpload(pendingImages);
await uploadVerificationIssueImages(issueId, prepared); await uploadVerificationIssueImages(issueId, prepared);
} }
setAttachmentsRefreshKey(k => k + 1); setAttachmentsRefreshKey(k => k + 1);
@@ -173,6 +168,10 @@ const VerificationBoardPage: React.FC<Props> = ({
} }
}, [reload, stageFilter]); }, [reload, stageFilter]);
const handleAttachmentsChanged = useCallback(() => {
setAttachmentsRefreshKey(k => k + 1);
}, []);
return ( return (
<> <>
<BuilderPageShell <BuilderPageShell
@@ -232,6 +231,7 @@ const VerificationBoardPage: React.FC<Props> = ({
attachmentsRefreshKey={attachmentsRefreshKey} attachmentsRefreshKey={attachmentsRefreshKey}
saving={detailSaving} saving={detailSaving}
onSave={handleDetailSave} onSave={handleDetailSave}
onAttachmentsChanged={handleAttachmentsChanged}
/> />
)} )}
/> />
@@ -53,14 +53,13 @@ interface Props {
/** 등록 draft — 저장 전 로컬 파일 */ /** 등록 draft — 저장 전 로컬 파일 */
draftFiles?: File[]; draftFiles?: File[];
onDraftFilesChange?: (files: File[]) => void; onDraftFilesChange?: (files: File[]) => void;
/** 상세 화면 — 저장 시 삭제할 서버 이미지 ID */ /** true: 새 첨부 업로드만 저장 시 반영 (삭제는 즉시 API) */
pendingDeleteIds?: number[];
onPendingDeleteIdsChange?: (ids: number[]) => void;
/** true: 추가·삭제를 저장 버튼까지 지연 (상세 편집) */
deferUpload?: boolean; deferUpload?: boolean;
disabled?: boolean; disabled?: boolean;
/** 모달 등 좁은 영역 */ /** compact: 모달 등 좁은 영역 */
compact?: boolean; compact?: boolean;
/** 첨부 추가·삭제 후 (목록 갱신 등) */
onAttachmentsChanged?: () => void;
} }
interface CtxMenuState { interface CtxMenuState {
@@ -72,11 +71,10 @@ const VerificationIssueAttachmentsSection: React.FC<Props> = ({
issueId = null, issueId = null,
draftFiles = [], draftFiles = [],
onDraftFilesChange, onDraftFilesChange,
pendingDeleteIds = [],
onPendingDeleteIdsChange,
deferUpload = false, deferUpload = false,
disabled = false, disabled = false,
compact = false, compact = false,
onAttachmentsChanged,
}) => { }) => {
const isDraft = issueId == null; const isDraft = issueId == null;
const saveOnSubmit = deferUpload && issueId != null; const saveOnSubmit = deferUpload && issueId != null;
@@ -169,6 +167,7 @@ const VerificationIssueAttachmentsSection: React.FC<Props> = ({
const prepared = await prepareAttachmentsForUpload(picked); const prepared = await prepareAttachmentsForUpload(picked);
await uploadVerificationIssueImages(issueId, prepared); await uploadVerificationIssueImages(issueId, prepared);
await reload(); await reload();
onAttachmentsChanged?.();
} catch (e) { } catch (e) {
console.warn('[VerificationBoard] attachment upload', e); console.warn('[VerificationBoard] attachment upload', e);
window.alert(uploadErrorMessage(e)); window.alert(uploadErrorMessage(e));
@@ -203,20 +202,16 @@ const VerificationIssueAttachmentsSection: React.FC<Props> = ({
const handleDeleteServer = async (imageId: number) => { const handleDeleteServer = async (imageId: number) => {
if (issueId == null) return; if (issueId == null) return;
if (!window.confirm('이 첨부 파일을 삭제할까요?')) return; if (!window.confirm('이 첨부 파일을 삭제할까요?')) return;
if (saveOnSubmit) { setUploading(true);
onPendingDeleteIdsChange?.(
pendingDeleteIds.includes(imageId)
? pendingDeleteIds
: [...pendingDeleteIds, imageId],
);
return;
}
try { try {
await deleteVerificationIssueImage(issueId, imageId); await deleteVerificationIssueImage(issueId, imageId);
await reload(); await reload();
onAttachmentsChanged?.();
} catch (e) { } catch (e) {
console.warn('[VerificationBoard] image delete', e); console.warn('[VerificationBoard] attachment delete', e);
window.alert('이미지 삭제에 실패했습니다.'); window.alert('첨부 파일 삭제에 실패했습니다.');
} finally {
setUploading(false);
} }
}; };
@@ -275,8 +270,8 @@ const VerificationIssueAttachmentsSection: React.FC<Props> = ({
}; };
const visibleServerImages = useMemo( const visibleServerImages = useMemo(
() => images.filter(img => img.id != null && !pendingDeleteIds.includes(img.id)), () => images.filter(img => img.id != null),
[images, pendingDeleteIds], [images],
); );
const totalCount = isDraft || saveOnSubmit const totalCount = isDraft || saveOnSubmit
@@ -20,8 +20,8 @@ interface Props {
onSave?: ( onSave?: (
dto: VerificationIssueDto, dto: VerificationIssueDto,
pendingImages: File[], pendingImages: File[],
pendingDeleteIds: number[],
) => void | Promise<void>; ) => void | Promise<void>;
onAttachmentsChanged?: () => void;
} }
const VerificationIssueDetailPanel: React.FC<Props> = ({ const VerificationIssueDetailPanel: React.FC<Props> = ({
@@ -29,13 +29,13 @@ const VerificationIssueDetailPanel: React.FC<Props> = ({
attachmentsRefreshKey = 0, attachmentsRefreshKey = 0,
saving = false, saving = false,
onSave, onSave,
onAttachmentsChanged,
}) => { }) => {
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const [content, setContent] = useState(''); const [content, setContent] = useState('');
const [reproductionPath, setReproductionPath] = useState(''); const [reproductionPath, setReproductionPath] = useState('');
const [stage, setStage] = useState<VerificationIssueStage>('REGISTERED'); const [stage, setStage] = useState<VerificationIssueStage>('REGISTERED');
const [pendingImages, setPendingImages] = useState<File[]>([]); const [pendingImages, setPendingImages] = useState<File[]>([]);
const [pendingDeleteIds, setPendingDeleteIds] = useState<number[]>([]);
const resetFromIssue = useCallback((src: VerificationIssueDto) => { const resetFromIssue = useCallback((src: VerificationIssueDto) => {
setTitle(src.title ?? ''); setTitle(src.title ?? '');
@@ -43,7 +43,6 @@ const VerificationIssueDetailPanel: React.FC<Props> = ({
setReproductionPath(src.reproductionPath ?? ''); setReproductionPath(src.reproductionPath ?? '');
setStage(src.stage ?? 'REGISTERED'); setStage(src.stage ?? 'REGISTERED');
setPendingImages([]); setPendingImages([]);
setPendingDeleteIds([]);
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -60,9 +59,8 @@ const VerificationIssueDetailPanel: React.FC<Props> = ({
|| pathTrimmed !== issuePath || pathTrimmed !== issuePath
|| stage !== issue.stage || stage !== issue.stage
|| pendingImages.length > 0 || pendingImages.length > 0
|| pendingDeleteIds.length > 0
); );
}, [issue, title, content, reproductionPath, stage, pendingImages, pendingDeleteIds]); }, [issue, title, content, reproductionPath, stage, pendingImages]);
const handleCancel = () => { const handleCancel = () => {
if (issue) resetFromIssue(issue); if (issue) resetFromIssue(issue);
@@ -85,7 +83,6 @@ const VerificationIssueDetailPanel: React.FC<Props> = ({
stage, stage,
}, },
pendingImages, pendingImages,
pendingDeleteIds,
); );
}; };
@@ -163,9 +160,8 @@ const VerificationIssueDetailPanel: React.FC<Props> = ({
deferUpload deferUpload
draftFiles={pendingImages} draftFiles={pendingImages}
onDraftFilesChange={setPendingImages} onDraftFilesChange={setPendingImages}
pendingDeleteIds={pendingDeleteIds}
onPendingDeleteIdsChange={setPendingDeleteIds}
disabled={saving} disabled={saving}
onAttachmentsChanged={onAttachmentsChanged}
/> />
)} )}
+7 -2
View File
@@ -910,8 +910,13 @@
font-size: 12px; font-size: 12px;
line-height: 1; line-height: 1;
cursor: pointer; cursor: pointer;
opacity: 0; opacity: 0.82;
transition: opacity 0.15s; 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 { .vbd-attach-item:hover .vbd-attach-remove {