diff --git a/apps/as/src/entities/attach/api/attach-service.ts b/apps/as/src/entities/attach/api/attach-service.ts index ceba5de3f..2cb1fc35d 100644 --- a/apps/as/src/entities/attach/api/attach-service.ts +++ b/apps/as/src/entities/attach/api/attach-service.ts @@ -3,7 +3,13 @@ import { AxiosResponse } from "axios"; import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; -import type { AttachService } from "../model/attach-interface"; +import type { Attach, AttachService } from "../model/attach-interface"; + +async function findByAtflCd(atflCd: string): Promise { + if (!atflCd) return []; + const response = await client.get(`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflCd}`); + return response.data; +} async function deleteAttachmentByAtflId(atflId: string): Promise { if (!atflId) return; @@ -23,6 +29,7 @@ async function downloadAttachmentByAtflId(atflId: string): Promise Promise; deleteAttachmentByAtflId: (atflId: string) => Promise; downloadAttachmentByAtflCd: (atflCd: string) => Promise; downloadAttachmentByAtflId: (atflId: string) => Promise; diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/ai-pending-utils.ts b/apps/as/src/pages/fnrs-cnsltn/lib/ai-pending-utils.ts index c310dbf16..1ac079216 100644 --- a/apps/as/src/pages/fnrs-cnsltn/lib/ai-pending-utils.ts +++ b/apps/as/src/pages/fnrs-cnsltn/lib/ai-pending-utils.ts @@ -3,9 +3,9 @@ const AI_PENDING_STATUS_CODES = ["FV", "AC", "AV"] as const; type AiPendingStatusCode = (typeof AI_PENDING_STATUS_CODES)[number]; const AI_PENDING_MESSAGES: Record = { - FV: "파일 검증이 진행 중입니다", - AC: "실무협의서 초안 생성이 진행 중입니다", - AV: "금액 검증이 진행 중입니다", + FV: "AI 파일 검증이 진행 중입니다", + AC: "AI 실무협의서 초안 생성이 진행 중입니다", + AV: "AI 금액 검증이 진행 중입니다", }; function isAiPendingStatus(prgrsSttsComCd: string): boolean { diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx index 63e6526c9..adfc0ffd7 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx @@ -322,7 +322,7 @@ function FloatingActionButtons({ dmndTtl: data.detDto?.bizNm ?? "", dmndCn: data.detDto?.bizCn ?? "", elaprCycl: data.detDto?.prgrsSttsComCd === "RK" ? 2 : 1, - docPrgrsSttsComCd: data.detDto?.prgrsSttsComCd, + docPrgrsSttsComCd: "AC", elaprPrgrsSttsList: buildElaprPrgrsSttsListFromAplnRows(aplnRows), }; await executeAction({ diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx index 6271183f9..c73a1aafc 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx @@ -88,7 +88,7 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps) function buildFormPayload( data: FnrsCnsltnRegistFormData, - prgrsSttsComCd: "SV" | "ST" | "TE", + prgrsSttsComCd: string, ): FnrsCnsltnApplySaveRequestDto | undefined { const withTotals = recalculateFnrsSharTotals(data.fnrsSharList, sharItems); const validationMessage = validateBizCstSharBalance( @@ -123,7 +123,7 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps) // eslint-disable-next-line @typescript-eslint/no-unused-vars ({ splmntAtchFileNm, splmntAtchFileAtflId, splmntDmndAtchFileNm, ...item }) => ({ ...item, - ...(prgrsSttsComCd === "TE" ? { cmptnYn: "Y" } : null), + ...(prgrsSttsComCd !== "SV" && prgrsSttsComCd !== "ST" && { cmptnYn: "Y" }), }), ), }; @@ -139,12 +139,13 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps) } async function submitSupplementForm(data: FnrsCnsltnRegistFormData): Promise { + const beforePrgrsSttsComCd = initialData?.detDto?.beforePrgrsSttsComCd; + if (!beforePrgrsSttsComCd) return undefined; + const docNo = initialData?.detDto?.fnrsCnsltnDocNo; - if (!docNo) return undefined; - - const payload = buildFormPayload(data, "TE"); - if (!payload) return undefined; - + const payload = buildFormPayload(data, beforePrgrsSttsComCd); + if (!docNo || !payload) return undefined; + debugger; return fnrsCnsltnMngService.submitSupplement(docNo, payload); } diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-view-page.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-view-page.tsx index 8d64073d4..2e85018b0 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-view-page.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-view-page.tsx @@ -102,9 +102,15 @@ function FnrsViewPage({ id }: FnrsViewPageProps) {
{isAiPending && ( - - {getAiPendingMessage(detDto.prgrsSttsComCd || "")} - +
+ + + + + + {getAiPendingMessage(detDto.prgrsSttsComCd || "")} + +
)}
diff --git a/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-dialog.tsx b/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-dialog.tsx index 24dbace6e..95aaaede2 100644 --- a/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-dialog.tsx +++ b/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-dialog.tsx @@ -2,6 +2,8 @@ import { useEffect, useId, useState } from "react"; +import { Paperclip, X } from "lucide-react"; + import { getDefaultFileKey, useTus } from "@/shared/lib"; import { TextShimmer } from "@/shared/ui"; import { attachService } from "@/entities/attach"; @@ -36,7 +38,7 @@ interface SplmntDmndRow { splmntArtclComCd: string; splmntDmndCn: string; splmntDmndAtchFileGroupId: string; - uploadedFile: UploadedSplmntFile | null; + uploadedFiles: UploadedSplmntFile[]; } interface SplmntDmndDialogConfirmItem { @@ -67,7 +69,7 @@ function createEmptyRow(): SplmntDmndRow { splmntArtclComCd: "", splmntDmndCn: "", splmntDmndAtchFileGroupId: "", - uploadedFile: null, + uploadedFiles: [], }; } @@ -105,11 +107,13 @@ function SplmntDmndDialog({ setRows((prev) => prev.map((row) => { - if (row.rowKey !== rowKey || row.uploadedFile?.fileKey !== fileKey) return row; + if (row.rowKey !== rowKey) return row; return { ...row, splmntDmndAtchFileGroupId: atflCd, - uploadedFile: { ...row.uploadedFile, atflCd, atflId }, + uploadedFiles: row.uploadedFiles.map((file) => + file.fileKey === fileKey ? { ...file, atflCd, atflId } : file, + ), }; }), ); @@ -120,8 +124,11 @@ function SplmntDmndDialog({ setRows((prev) => prev.map((row) => { - if (row.rowKey !== rowKey || row.uploadedFile?.fileKey !== fileKey) return row; - return { ...row, uploadedFile: null }; + if (row.rowKey !== rowKey) return row; + return { + ...row, + uploadedFiles: row.uploadedFiles.filter((file) => file.fileKey !== fileKey), + }; }), ); }, @@ -132,8 +139,8 @@ function SplmntDmndDialog({ setRows([createEmptyRow()]); }, [open]); - const isAnyUploading = rows.some( - (row) => row.uploadedFile != null && (isUploading[row.uploadedFile.fileKey] ?? false), + const isAnyUploading = rows.some((row) => + row.uploadedFiles.some((file) => isUploading[file.fileKey] ?? false), ); const canSubmit = isRowsValid(rows) && !isAnyUploading && !isSubmitting; @@ -152,30 +159,42 @@ function SplmntDmndDialog({ async function handleFileUpload(rowKey: string, files: FileList | null) { if (!files || files.length === 0) return; - const file = files[0]; - const fileKey = createUploadKey(rowKey, file); const targetRow = rows.find((row) => row.rowKey === rowKey); - const existingAtflCd = targetRow?.splmntDmndAtchFileGroupId; + let existingAtflCd = targetRow?.splmntDmndAtchFileGroupId ?? ""; - updateRow(rowKey, { - uploadedFile: { fileName: file.name, fileKey }, - }); + for (const file of Array.from(files)) { + const fileKey = createUploadKey(rowKey, file); - try { - await uploadFileAsync(file, { - fileKey, - metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined, - }); - } catch { - // onError callback handles cleanup + setRows((prev) => + prev.map((row) => + row.rowKey === rowKey + ? { + ...row, + uploadedFiles: [...row.uploadedFiles, { fileName: file.name, fileKey }], + } + : row, + ), + ); + + try { + const atflCd = await uploadFileAsync(file, { + fileKey, + metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined, + }); + + if (!existingAtflCd) existingAtflCd = atflCd; + } catch { + // onError callback handles cleanup + } } } - async function handleFileRemove(rowKey: string) { + async function handleFileRemove(rowKey: string, fileIndex: number) { const targetRow = rows.find((row) => row.rowKey === rowKey); - if (!targetRow?.uploadedFile) return; + const targetFile = targetRow?.uploadedFiles[fileIndex]; + if (!targetRow || !targetFile) return; - const { fileKey, atflId } = targetRow.uploadedFile; + const { fileKey, atflId } = targetFile; uploads[fileKey]?.abort(); if (atflId) { @@ -186,9 +205,10 @@ function SplmntDmndDialog({ } } + const nextFiles = targetRow.uploadedFiles.filter((_, index) => index !== fileIndex); updateRow(rowKey, { - uploadedFile: null, - splmntDmndAtchFileGroupId: "", + uploadedFiles: nextFiles, + splmntDmndAtchFileGroupId: nextFiles.length > 0 ? targetRow.splmntDmndAtchFileGroupId : "", }); } @@ -237,9 +257,9 @@ function SplmntDmndDialog({ {rows.map((row) => { - const uploading = row.uploadedFile - ? (isUploading[row.uploadedFile.fileKey] ?? false) - : false; + const isRowUploading = row.uploadedFiles.some( + (file) => isUploading[file.fileKey] ?? false, + ); const fileInputId = `${formId}-${row.rowKey}-file`; return ( @@ -266,40 +286,58 @@ function SplmntDmndDialog({ className="min-h-[8rem] !text-xl" /> - -
- {row.uploadedFile ? ( -
- - {row.uploadedFile.fileName} - - -
- ) : ( - - )} -
+ + {row.uploadedFiles.length > 0 ? ( +
+ {row.uploadedFiles.map((file, fileIndex) => { + const uploading = isUploading[file.fileKey] ?? false; + + return ( +
+ + + {file.fileName} + + +
+ ); + })} +
+ ) : null} +
{rows.length > 1 ? ( diff --git a/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-form-section.tsx b/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-form-section.tsx index a8efff36d..a8f38ecb4 100644 --- a/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-form-section.tsx +++ b/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-form-section.tsx @@ -3,12 +3,12 @@ import { useEffect, useId, useRef, useState } from "react"; import { Controller, useFormContext, useWatch } from "react-hook-form"; -import { Paperclip } from "lucide-react"; +import { Paperclip, X } from "lucide-react"; import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib"; import { TextShimmer } from "@/shared/ui"; import { SectionCollapsible } from "@/shared/ui"; -import { attachService, useDownloadAttachmentByAtflCd } from "@/entities/attach"; +import { attachService } from "@/entities/attach"; import { Badge, Button, @@ -42,8 +42,6 @@ interface SplmntDmndFormItem { splmntPicNm?: string; splmntCmptnDt?: string; splmntAtchFileGroupId?: string; - splmntAtchFileNm?: string; - splmntAtchFileAtflId?: string; cmptnYn?: string; } @@ -75,9 +73,10 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps) const formId = useId(); const { control, setValue, getValues } = useFormContext(); const splmntDmndList = useWatch({ control, name: "splmntDmndList" }) ?? []; - const [uploadedFiles, setUploadedFiles] = useState>({}); + const [uploadedFiles, setUploadedFiles] = useState>({}); const initialFilesMappedRef = useRef(false); - const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd(); + const isMountedRef = useRef(true); + const atflCdByRowRef = useRef>({}); const { uploadFileAsync, isUploading, uploads } = useTus({ onSuccess: (atflCd, atflId, _file, fileKey) => { @@ -86,55 +85,93 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps) if (!rowKey) return; setUploadedFiles((prev) => { - const current = prev[rowKey]; - if (!current || current.fileKey !== fileKey) return prev; + const current = prev[rowKey] ?? []; return { ...prev, - [rowKey]: { ...current, atflCd, atflId }, + [rowKey]: current.map((item) => + item.fileKey === fileKey ? { ...item, atflCd, atflId } : item, + ), }; }); + atflCdByRowRef.current[rowKey] = atflCd; const list = getValues("splmntDmndList") ?? []; const index = list.findIndex((item, idx) => getRowKey(item, idx) === rowKey); if (index < 0) return; setValue(`splmntDmndList.${index}.splmntAtchFileGroupId`, atflCd, { shouldDirty: true }); - setValue(`splmntDmndList.${index}.splmntAtchFileAtflId`, atflId, { shouldDirty: true }); }, onError: (_error, _file, fileKey) => { const rowKey = getRowKeyFromFileKey(fileKey); if (!rowKey) return; setUploadedFiles((prev) => { - const current = prev[rowKey]; - if (!current || current.fileKey !== fileKey) return prev; - return { ...prev, [rowKey]: null }; + const nextFiles = (prev[rowKey] ?? []).filter((item) => item.fileKey !== fileKey); + return { ...prev, [rowKey]: nextFiles }; }); }, }); + useEffect( + () => () => { + isMountedRef.current = false; + }, + [], + ); + useEffect(() => { if (initialFilesMappedRef.current) return; if (splmntDmndList.length === 0) return; + initialFilesMappedRef.current = true; + + const rowsWithFiles = splmntDmndList.flatMap((item, index) => { + if (!item.splmntAtchFileGroupId) return []; - const initial: Record = {}; - for (let index = 0; index < splmntDmndList.length; index += 1) { - const item = splmntDmndList[index]; const rowKey = getRowKey(item, index); - if (item.splmntAtchFileGroupId && item.splmntAtchFileNm) { - initial[rowKey] = { - fileName: item.splmntAtchFileNm, - fileKey: `${SPLMNT_CMPTN_FILE_KEY_PREFIX}${rowKey}::initial`, - atflCd: item.splmntAtchFileGroupId, - atflId: item.splmntAtchFileAtflId, - }; - } else { - initial[rowKey] = null; - } + atflCdByRowRef.current[rowKey] = item.splmntAtchFileGroupId; + return [{ rowKey, atflCd: item.splmntAtchFileGroupId }]; + }); + + async function mapInitialFiles() { + const results = await Promise.all( + rowsWithFiles.map(async ({ rowKey, atflCd }) => { + try { + const files = await attachService.findByAtflCd(atflCd); + return { + rowKey, + files: files.map((file) => ({ + fileName: file.orgnlFileNm, + fileKey: `${SPLMNT_CMPTN_FILE_KEY_PREFIX}${rowKey}::server::${file.atflId}`, + atflCd: file.atflCd, + atflId: file.atflId, + })), + }; + } catch (error) { + console.error("보완완료 첨부파일 조회 실패:", error); + return { rowKey, files: [] }; + } + }), + ); + + if (!isMountedRef.current) return; + + setUploadedFiles((prev) => { + const next = { ...prev }; + + for (const { rowKey, files } of results) { + const localFiles = prev[rowKey] ?? []; + const serverAtflIds = new Set(files.map((file) => file.atflId)); + next[rowKey] = [ + ...files, + ...localFiles.filter((file) => !file.atflId || !serverAtflIds.has(file.atflId)), + ]; + } + + return next; + }); } - setUploadedFiles(initial); - initialFilesMappedRef.current = true; + void mapInitialFiles(); }, [splmntDmndList]); if (splmntDmndList.length === 0) return null; @@ -144,30 +181,37 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps) const item = splmntDmndList[index]; const rowKey = getRowKey(item, index); - const file = files[0]; - const fileKey = createUploadKey(rowKey, file); - const existingAtflCd = item.splmntAtchFileGroupId; + let existingAtflCd = atflCdByRowRef.current[rowKey] || item.splmntAtchFileGroupId || ""; - setUploadedFiles((prev) => ({ - ...prev, - [rowKey]: { fileName: file.name, fileKey }, - })); - setValue(`splmntDmndList.${index}.splmntAtchFileNm`, file.name, { shouldDirty: true }); + for (const file of Array.from(files)) { + const fileKey = createUploadKey(rowKey, file); - try { - await uploadFileAsync(file, { - fileKey, - metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined, - }); - } catch { - // onError callback handles cleanup + setUploadedFiles((prev) => ({ + ...prev, + [rowKey]: [...(prev[rowKey] ?? []), { fileName: file.name, fileKey }], + })); + + try { + const atflCd = await uploadFileAsync(file, { + fileKey, + metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined, + }); + + if (!existingAtflCd) { + existingAtflCd = atflCd; + atflCdByRowRef.current[rowKey] = atflCd; + } + } catch { + // onError callback handles cleanup + } } } - async function handleFileRemove(index: number) { + async function handleFileRemove(index: number, fileIndex: number) { const item = splmntDmndList[index]; const rowKey = getRowKey(item, index); - const uploaded = uploadedFiles[rowKey]; + const currentFiles = uploadedFiles[rowKey] ?? []; + const uploaded = currentFiles[fileIndex]; if (!uploaded) return; const { fileKey, atflId } = uploaded; @@ -181,10 +225,13 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps) } } - setUploadedFiles((prev) => ({ ...prev, [rowKey]: null })); + const nextFiles = currentFiles.filter((_, currentIndex) => currentIndex !== fileIndex); + setUploadedFiles((prev) => ({ ...prev, [rowKey]: nextFiles })); + + if (nextFiles.length > 0) return; + + delete atflCdByRowRef.current[rowKey]; setValue(`splmntDmndList.${index}.splmntAtchFileGroupId`, "", { shouldDirty: true }); - setValue(`splmntDmndList.${index}.splmntAtchFileNm`, "", { shouldDirty: true }); - setValue(`splmntDmndList.${index}.splmntAtchFileAtflId`, "", { shouldDirty: true }); } return ( @@ -212,8 +259,8 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps) {splmntDmndList.map((item, index) => { const rowKey = getRowKey(item, index); - const uploaded = uploadedFiles[rowKey] ?? null; - const uploading = uploaded ? (isUploading[uploaded.fileKey] ?? false) : false; + const rowFiles = uploadedFiles[rowKey] ?? []; + const isRowUploading = rowFiles.some((file) => isUploading[file.fileKey] ?? false); const fileInputId = `${formId}-${rowKey}-file`; const isRowReadOnly = readOnly || item.cmptnYn === "Y"; @@ -229,13 +276,10 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps)
{item.splmntDmndCn || "-"}
{item.splmntDmndAtchFileGroupId ? ( - downloadAttachment(item.splmntDmndAtchFileGroupId!)} - > + - {item.splmntDmndAtchFileNm || "다운로드"} - + {item.splmntDmndAtchFileNm || "첨부파일"} + ) : null}
@@ -258,54 +302,64 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps) /> )}
- - {uploaded ? ( -
- {uploaded.atflCd || item.splmntAtchFileGroupId ? ( - - downloadAttachment((uploaded.atflCd || item.splmntAtchFileGroupId)!) - } - > - - {uploaded.fileName} - - ) : ( - - {uploaded.fileName} - - )} - {!isRowReadOnly ? ( - - ) : null} + + {rowFiles.length > 0 ? ( +
+ {rowFiles.map((file, fileIndex) => { + const uploading = isUploading[file.fileKey] ?? false; + + return ( +
+ + + {file.fileName} + + {!isRowReadOnly ? ( + + ) : null} +
+ ); + })}
- ) : isRowReadOnly ? ( + ) : null} + {!isRowReadOnly ? ( + + ) : rowFiles.length === 0 ? ( - - ) : ( - - )} + ) : null}