From d3ca402e4a3b0a3495d3ef32b944a244b091f6fa Mon Sep 17 00:00:00 2001 From: jangwoong Date: Fri, 29 May 2026 15:57:30 +0900 Subject: [PATCH] =?UTF-8?q?[st=20frontend]=20=EC=B2=A8=EB=B6=80=20?= =?UTF-8?q?=EA=B9=8C=EC=A7=80=20=ED=95=B4=EC=84=9C=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/instl-sncn/ui/cnfm-regist-page.tsx | 98 ++++++++++++++----- apps/as/src/widgets/cnfm/ui/attachments.tsx | 8 +- 2 files changed, 83 insertions(+), 23 deletions(-) diff --git a/apps/as/src/pages/instl-sncn/ui/cnfm-regist-page.tsx b/apps/as/src/pages/instl-sncn/ui/cnfm-regist-page.tsx index 21eed8efc..c607911d0 100644 --- a/apps/as/src/pages/instl-sncn/ui/cnfm-regist-page.tsx +++ b/apps/as/src/pages/instl-sncn/ui/cnfm-regist-page.tsx @@ -4,14 +4,14 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useRouter } from "next/navigation"; -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import axios from "axios"; import { type HttpResponse, Upload } from "tus-js-client"; import { AUTH_USER_ID, CLAIM_NAME, ENDPOINT, PROXY_HOST } from "@/shared/config"; import { deleteAttachment } from "@/shared/lib/attach"; import { getCookie, setCookie, useAlertStore, useUserStore } from "@/shared/lib"; - +import { formatFileSize } from "@/shared/lib/file-utils"; import type { AcntgSeMngTreeDto } from "@/entities/acntg"; import { acntgService } from "@/entities/acntg"; import type { CklstTreeNode } from "@/entities/cklst"; @@ -77,10 +77,42 @@ import { NumCell, ReadCell, PrivacyConsent, EtcAttachments } from '@/widgets/cnf /* ------------------------------------------------------------------ */ /* 메인 컴포넌트 */ /* ------------------------------------------------------------------ */ +/** + * initialData 매핑 안내 + * - initialData 데이터는 cnfm-modify-page에서 서버로부터 받아온 상세 데이터를 그대로 전달받거나, cnfm-regist-page에서 수정 모드로 진입할 때 전달받을 수 있습니다. + * - 이 컴포넌트는 수정 모드로 호출될 때 `initialData`를 받습니다. + * - 서버 응답은 두 가지 형태 중 하나일 수 있습니다: + * 1) 상세 객체 그대로: { ...detFields } + * 2) 래퍼 형태: { detDto: { ...detFields }, bizSclList: [...], bizCstList: [...], cklstResultList: [...], esntlFileList: [...], etcFileList: [...] } + * + * - 주요 detFields (det 또는 detDto 내부): + * - bizNm, bizBgngYr, bizEndYr, bizCn + * - envoSeComCd or envoSeCd (설치구분) + * - dsgnrNm, dsgnrOgdpNm (소속), dsgnrTelno, dsgnrEmlAddr + * - privacyAgree ("Y" | "N") + * - aplcntNm, aplcntMblTelno, aplcntOgdpNm, aplcntEmlAddr + * + * - 주요 배열 (위치: 최상위 내부) + * - bizSclList: [{ cnstpSeCd, bscPl|bscPlanQty, budgetReflct|bgtRfltQty, thtiDsgn|tstmDesignQty }, ...] + * - bizCstList: [{ acntgSeCd, bscPlanAmt|bscPl, bgtRfltAmt|budgetReflct, tstmDesignAmt|thtiDsgn }, ...] + * - cklstResultList: [{ cklstSn, cklstArtclSn, cklstRvwSn, rvwRsltAnmlyYn: 'Y'|'N'|null, ... }, ...] + * - esntlFileList: [{ esntlAtchFileId, esntlAtchFileNm, esntlAtchYn, atflCd, atflId, orgnlFileNm, ... }, ...] + * - etcFileList: [{ atflId, orgnlFileNm, atflFlieSz, upldUserNm, atflCd, ... }, ...] + * + * - 주의사항: + * - 필드명이 서버의 버전이나 API 경로에 따라 달라질 수 있어(예: bscPl vs bscPlanQty) 코드에서 양쪽을 모두 확인합니다. + * - 일부 배열/속성은 최상위에 있거나 detDto 내부에 있을 수 있으므로 둘 다 우선순위로 체크합니다. + */ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { const router = useRouter(); const userInfo = useUserStore((state) => state.userInfo); const alert = useAlertStore((state) => state.setMessage); + const qc = useQueryClient(); + + const docId = useMemo(() => { + const det = initialData?.detDto ?? initialData; + return det?.instlSncnDocNo ?? det?.instlSncnDocNo ?? ""; + }, [initialData]); const [docSeComCd] = useState("INSTL"); const [taskSeComCdNm, setTaskSeComCdNm] = useState("재이용수"); @@ -228,17 +260,19 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { const [etcFileGroupId, setEtcFileGroupId] = useState(""); const [etcUploading, setEtcUploading] = useState(false); const etcFileGroupIdRef = useRef(""); + const initialMappedRef = useRef(false); // 초기 데이터가 주어지면 폼 상태로 매핑 (수정 모드에서 사용) useEffect(() => { if (!initialData) return; + if (initialMappedRef.current) return; try { const det = (initialData.detDto ?? initialData) as any; const bizSclList = initialData.bizSclList ?? []; const bizCstList = initialData.bizCstList ?? []; - const cklstResultList = initialData.cklstResultList ?? []; + //const cklstResultList = initialData.cklstResultList ?? []; const esntlFileList = initialData.esntlFileList ?? []; const etcFileList = initialData.etcFileList ?? []; @@ -264,25 +298,41 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { setAplcntMblTelno(det.aplcntMblTelno ?? ""); setAplcntOgdpNm(det.aplcntOgdpNm ?? ""); setAplcntEmlAddr(det.aplcntEmlAddr ?? ""); - - // 기타첨부파일 + // 수정모드 기타첨부파일 if (Array.isArray(etcFileList)) { setEtcFiles( etcFileList .filter((f: any) => f && typeof f === "object") - .map((f: any) => ({ - id: f.atflId ?? String(Math.random()), - name: f.orgnlFileNm ?? f.physFileNm ?? "", - ext: "", - size: f.atflFlieSz ?? 0, - uploader: f.upldUserNm ?? "", - atflCd: f.atflCd ?? "", - atflId: f.atflId ?? "", - })), + .map((f: any) => { + const fileName = f.orgnlFileNm ?? f.physFileNm ?? ""; + const ext = fileName && typeof fileName === "string" && fileName.includes(".") ? fileName.split(".").pop() ?? "" : ""; + return { + id: f.atflId , + name: fileName, + ext, + size: f.atflFlieSz ?? 0, + uploader: det.rgtrNm ?? "", + atflCd: f.atflCd ?? "", + atflId: f.atflId ?? "", + }; + }), ); } - // 필수첨부파일: requiredFiles는 requiredAttachmentDefs 로 초기화되므로 atflCd/atflId만 덮어쓴다 + // 수정모드시 기타첨부 그룹아이디 적용 (중복 매핑 방지) + // - det.atchFileGroupId 또는 det.atflCd 우선, 없으면 etcFileList 첫 항목의 atflCd 사용 + const existingGroup = (det.atchFileGroupId ) || (Array.isArray(etcFileList) && etcFileList[0]?.atflCd) || ""; + //console.log("etcFileGroupIdRef.current 초기화1:", existingGroup); + if (existingGroup && etcFileGroupIdRef.current !== existingGroup) { + etcFileGroupIdRef.current = existingGroup; + setEtcFileGroupId((prev) => prev || existingGroup); + } + // console.log("etcFileGroupIdRef.current 초기화:", etcFileGroupIdRef.current); + + // 매핑 완료 플래그 설정 — 이후 동일 컴포넌트 인스턴스에서 재매핑 방지 + initialMappedRef.current = true; + + //수정모드 필수첨부파일: requiredFiles는 requiredAttachmentDefs 로 초기화되므로 atflCd/atflId만 덮어쓴다 if (Array.isArray(esntlFileList)) { setRequiredFiles((prev) => { const next = { ...prev }; @@ -373,15 +423,12 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { uploadRequiredFile(file, key); e.target.value = ""; }; - - const toFileSizeText = (size: number): string => - size > 1024 * 1024 ? `${(size / 1024 / 1024).toFixed(1)}MB` : `${(size / 1024).toFixed(0)}KB`; - + const uploadEtcFile = (file: File): Promise => new Promise((resolve, reject) => { let uploadedAtflCd = ""; let uploadedAtflId = ""; - + console.log("기타첨부파일 업로드 시작:", file.name ,etcFileGroupIdRef.current); const upload = new Upload(file, { endpoint: `${PROXY_HOST}${ENDPOINT.ATTACH_SERVICE.TUS_UPLOAD}`, chunkSize: 10 * 1024 * 1024, @@ -416,7 +463,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { id: uploadedAtflId || `${Date.now()}_${Math.random()}`, name: file.name, ext, - size: toFileSizeText(file.size), + size: formatFileSize(file.size), uploader: userInfo?.userName ?? "알 수 없음", atflCd: uploadedAtflCd, atflId: uploadedAtflId, @@ -451,6 +498,13 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { setEtcFileGroupId(uploadedRows[0].atflCd); etcFileGroupIdRef.current = uploadedRows[0].atflCd; } + // 업로드 후 상세 페이지가 최신화되도록 해당 상세 쿼리 무효화 + try { + if (docId) qc.invalidateQueries({ queryKey: ["instl-sncn-detail", docId] }); + else qc.invalidateQueries({ queryKey: ["instl-sncn-detail"] }); + } catch (_e) { + // noop: 무효화 실패는 치명적이지 않음 + } } if (failedNames.length > 0) { @@ -797,7 +851,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { } const toInstlSncnMngPayload = (prgrsSttsComCd: "SV" | "ST"): InstlSncnMng => ({ - instlSncnDocNo: "", + instlSncnDocNo: docId ?? "", // 수정 시 기존 문서번호 유지, 신규 저장 시 빈값 admdstCd: "", sidoNm: "", sggNm: "", diff --git a/apps/as/src/widgets/cnfm/ui/attachments.tsx b/apps/as/src/widgets/cnfm/ui/attachments.tsx index b481e04c9..dcd68209e 100644 --- a/apps/as/src/widgets/cnfm/ui/attachments.tsx +++ b/apps/as/src/widgets/cnfm/ui/attachments.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { formatFileSize } from "@/shared/lib/file-utils"; type EtcFile = { id: string; @@ -52,7 +53,12 @@ export function EtcAttachments({ etcFiles, etcUploading, onFilesChange, onDelete {f.name} {f.ext} - {f.size} + {(() => { + const raw = f.size; + if (typeof raw === "number") return formatFileSize(raw); + const s = String(raw ?? "").trim(); + return /^[0-9]+$/.test(s) ? formatFileSize(Number(s)) : s; + })()} {f.uploader}