[st frontend] 첨부 까지 해서 저장함
This commit is contained in:
@@ -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: "",
|
||||
|
||||
@@ -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)}>
|
||||
|
||||
Reference in New Issue
Block a user