검증게시판 첨부이미지 삭제 기능
This commit is contained in:
@@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user