[st frontend] 첨부 까지 해서 저장함

This commit is contained in:
2026-05-29 15:57:30 +09:00
parent b5eea1a476
commit d3ca402e4a
2 changed files with 83 additions and 23 deletions
@@ -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<EtcFile> =>
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: "",
+7 -1
View File
@@ -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
<tr key={f.id}>
<td className="pr-2">{f.name}</td>
<td className="text-center">{f.ext}</td>
<td className="text-right">{f.size}</td>
<td className="text-right">{(() => {
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;
})()}</td>
<td className="text-center">{f.uploader}</td>
<td className="text-center">
<button className="cnfm-btn-delete" onClick={() => onDelete(f.id, f.atflId)}>