[st frontend] as 수정에 폼은 등록폼을 같이 쓰도록 코딩함

This commit is contained in:
2026-05-29 14:31:34 +09:00
parent a6f05b3850
commit 5d31c70e16
2 changed files with 167 additions and 59 deletions
@@ -1,17 +1,16 @@
"use client";
import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { Button, Input } from "@common/components";
import { CnfmRegistPage } from "./cnfm-regist-page";
import { InstlSncnApi } from "@/entities/instl-sncn";
import "./cnfm-page.css";
function CnfmModifyPage({ id }: { id: string }) {
const router = useRouter();
const query = useQuery({
queryKey: ["instl-sncn-modify", id],
// share cache with view page
queryKey: ["instl-sncn-detail", id],
queryFn: () => InstlSncnApi.getById(id),
});
@@ -25,53 +24,11 @@ function CnfmModifyPage({ id }: { id: string }) {
const data = query.data;
if (!data) return <div className="mt-6 text-[1.4rem]">수정 대상을 찾을 수 없습니다.</div>;
return (
<div className="mt-6 space-y-4">
<div className="cnfm-card p-4">
<h2 className="cnfm-section-title">설치인가 수정</h2>
<p className="mt-2 text-[1.3rem] text-gray-600">
상세 조회 API 데이터를 기반으로 수정 화면을 구성했습니다.
</p>
</div>
<div className="cnfm-card p-4">
<h3 className="text-[1.5rem] font-semibold">기본 정보</h3>
<table className="cnfm-form-table mt-3">
<tbody>
<tr>
<th>문서번호</th>
<td>{data.instlSncnDocNo}</td>
<th>진행상태</th>
<td>{data.prgrsSttsCdNm || data.prgrsSttsComCd || "-"}</td>
</tr>
<tr>
<th>사업명</th>
<td>
<Input defaultValue={data.bizNm || ""} className="!text-[1.3rem]" />
</td>
<th>사업내용</th>
<td>
<Input defaultValue={data.bizCn || ""} className="!text-[1.3rem]" />
</td>
</tr>
<tr>
<th>설계자명</th>
<td>
<Input defaultValue={data.dsgnrNm || ""} className="!text-[1.3rem]" />
</td>
<th>설계자 연락처</th>
<td>
<Input defaultValue={data.dsgnrTelno || ""} className="!text-[1.3rem]" />
</td>
</tr>
</tbody>
</table>
</div>
<div className="cnfm-action-row">
<Button color="white" onClick={() => router.push(`/instl-sncn/${id}`)}>상세로</Button>
<Button type="button" onClick={() => window.alert("수정 API 연동은 추후 확장 가능합니다.")}>저장</Button>
</div>
<div className="mt-6">
<CnfmRegistPage initialData={data} />
</div>
);
}
@@ -1,4 +1,5 @@
"use client";
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useEffect, useMemo, useRef, useState } from "react";
import { useRouter } from "next/navigation";
@@ -76,7 +77,7 @@ import { NumCell, ReadCell, PrivacyConsent, EtcAttachments } from '@/widgets/cnf
/* ------------------------------------------------------------------ */
/* 메인 컴포넌트 */
/* ------------------------------------------------------------------ */
function CnfmRegistPage() {
function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
const router = useRouter();
const userInfo = useUserStore((state) => state.userInfo);
const alert = useAlertStore((state) => state.setMessage);
@@ -112,6 +113,12 @@ function CnfmRegistPage() {
const [dsgnrTelno, setDsgnrTelno] = useState("");
const [dsgnrEmlAddr, setDsgnrEmlAddr] = useState("");
/* ── 신청자 정보 (수정 모드 매핑용) ── */
const [aplcntNm, setAplcntNm] = useState("");
const [aplcntMblTelno, setAplcntMblTelno] = useState("");
const [aplcntOgdpNm, setAplcntOgdpNm] = useState("");
const [aplcntEmlAddr, setAplcntEmlAddr] = useState("");
/* ── 개인정보 동의 ── */
const [privacyAgree, setPrivacyAgree] = useState<"Y" | "N">("N");
@@ -222,6 +229,122 @@ function CnfmRegistPage() {
const [etcUploading, setEtcUploading] = useState(false);
const etcFileGroupIdRef = useRef("");
// 초기 데이터가 주어지면 폼 상태로 매핑 (수정 모드에서 사용)
useEffect(() => {
if (!initialData) return;
try {
const det = (initialData.detDto ?? initialData) as any;
const bizSclList = initialData.bizSclList ?? [];
const bizCstList = initialData.bizCstList ?? [];
const cklstResultList = initialData.cklstResultList ?? [];
const esntlFileList = initialData.esntlFileList ?? [];
const etcFileList = initialData.etcFileList ?? [];
// 기본 정보 및 설계자 정보
setBzNm(det.bizNm ?? "");
setBzBgngYr(det.bizBgngYr ?? "");
setBzEndYr(det.bizEndYr ?? "");
setBzCn(det.bizCn ?? "");
setInstlCnfmApr(det.envoSeComCd ?? det.envoSeCd ?? "CNFM");
// 설계자
setDsgnrNm(det.dsgnrNm ?? "");
setDsgnrPsitn(det.dsgnrOgdpNm ?? "");
setDsgnrTelno(det.dsgnrTelno ?? "");
setDsgnrEmlAddr(det.dsgnrEmlAddr ?? "");
// 개인정보 동의
if (det.privacyAgree) setPrivacyAgree(det.privacyAgree === "Y" ? "Y" : "N");
// 신청자
setAplcntNm(det.aplcntNm ?? "");
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 ?? "",
})),
);
}
// 필수첨부파일: requiredFiles는 requiredAttachmentDefs 로 초기화되므로 atflCd/atflId만 덮어쓴다
if (Array.isArray(esntlFileList)) {
setRequiredFiles((prev) => {
const next = { ...prev };
for (const f of esntlFileList) {
if (!f || typeof f !== "object") continue;
const key = f.esntlAtchFileId ?? f.esntlAtchFileId;
if (!key) continue;
const existing = next[key] ?? {
key,
esntlAtchFileNm: f.esntlAtchFileNm ?? "",
esntlAtchYn: f.esntlAtchYn ?? "N",
esntlAtchFileRmrk: "",
atflCd: "",
atflId: "",
fileName: "",
uploading: false,
};
next[key] = {
...existing,
atflCd: f.atflCd ?? existing.atflCd,
atflId: f.atflId ?? existing.atflId,
fileName: f.orgnlFileNm ?? existing.fileName,
uploading: false,
};
}
return next;
});
}
// 사업규모 매핑
if (Array.isArray(bizSclList)) {
const map: Record<string, BzScaleRow> = {};
for (const row of bizSclList) {
if (!row.cnstpSeCd) continue;
map[row.cnstpSeCd] = {
bscPl: String(row.bscPl ?? row.bscPlanQty ?? ""),
budgetReflct: String(row.budgetReflct ?? row.bgtRfltQty ?? ""),
thtiDsgn: String(row.thtiDsgn ?? row.tstmDesignQty ?? ""),
};
}
setBzScaleMap(map);
}
// 총사업비 매핑
if (Array.isArray(bizCstList)) {
const map: Record<string, BzAmtRow> = {};
for (const row of bizCstList) {
if (!row.acntgSeCd) continue;
map[row.acntgSeCd] = {
bscPl: String(row.bscPlanAmt ?? row.bscPl ?? ""),
budgetReflct: String(row.bgtRfltAmt ?? row.budgetReflct ?? ""),
thtiDsgn: String(row.tstmDesignAmt ?? row.thtiDsgn ?? ""),
};
}
setAmtMap(map);
}
} catch (err) {
console.error("initialData 매핑 실패", err);
}
}, [initialData]);
/** 기타첨부파일 삭제 핸들러: 백엔드에 DELETE 요청을 보내 소프트삭제 처리 후 상태 갱신 */
const handleDeleteEtcFile = async (fileId: string, atflId: string) => {
try {
@@ -425,6 +548,34 @@ function CnfmRegistPage() {
});
}, [cklstRows]);
// 수정 모드에서 서버의 체크리스트 결과값을 폼 상태로 반영
useEffect(() => {
if (!initialData || cklstRows.length === 0) return;
const det = (initialData.detDto ?? initialData) as any;
if (!Array.isArray(det.cklstResultList)) return;
const lookup = new Map<string, string>();
for (const r of det.cklstResultList) {
// 서버의 결과에 cklstSn, cklstArtclSn, cklstRvwSn 조합이 있다면 key 생성
const key = `${r.cklstSn}_${r.cklstArtclSn ?? ""}_${r.cklstRvwSn}`;
if (r.rvwRsltAnmlyYn === "Y" || r.rvwRsltAnmlyYn === "N") lookup.set(key, r.rvwRsltAnmlyYn);
}
setCheckResults((prev) => {
const next = { ...prev };
let changed = false;
for (const row of cklstRows) {
const k = cklstKey(row);
const v = lookup.get(k);
if (v && next[k] !== v) {
next[k] = v as CheckResultVal;
changed = true;
}
}
return changed ? next : prev;
});
}, [initialData, cklstRows]);
const cklstStats = useMemo(() => {
const total = cklstRows.length;
const completed = cklstRows.reduce((acc, row) => {
@@ -663,10 +814,10 @@ function CnfmRegistPage() {
bizBgngYr: bzBgngYr,
bizEndYr: bzEndYr,
bizCn: bzCn,
aplcntNm: "",
aplcntMblTelno: "",
aplcntOgdpNm: "",
aplcntEmlAddr: "",
aplcntNm: aplcntNm || userInfo?.userName || "",
aplcntMblTelno: aplcntMblTelno || (userInfo?.mphn ?? userInfo?.telno ?? ""),
aplcntOgdpNm: aplcntOgdpNm || (userInfo?.instSeNm ?? ""),
aplcntEmlAddr: aplcntEmlAddr || (userInfo?.email ?? ""),
dsgnrNm,
dsgnrTelno,
dsgnrOgdpNm: dsgnrPsitn,
@@ -852,22 +1003,22 @@ function CnfmRegistPage() {
<th rowSpan={2} style={{ textAlign: "center" }} className="text-center align-middle w-[6.5rem]">신청자</th>
<th style={{ textAlign: "center" }}>이름</th>
<td>
<span className="inline-block min-w-[16rem] whitespace-nowrap text-[1.3rem] text-gray-500">{userInfo?.userName ?? "\u00A0"}</span>
<span className="inline-block min-w-[16rem] whitespace-nowrap text-[1.3rem] text-gray-500">{aplcntNm || userInfo?.userName || "\u00A0"}</span>
</td>
<th style={{ textAlign: "center" }}>전화번호</th>
<td>
<span className="inline-block min-w-[16rem] whitespace-nowrap text-[1.3rem] text-gray-500">{userInfo?.mphn ?? userInfo?.telno ?? "\u00A0"}</span>
<span className="inline-block min-w-[16rem] whitespace-nowrap text-[1.3rem] text-gray-500">{aplcntMblTelno || userInfo?.mphn || userInfo?.telno || "\u00A0"}</span>
</td>
</tr>
<tr>
<th style={{ textAlign: "center" }}>부서</th>
<td>
<span className="text-[1.3rem] text-gray-500">{userInfo?.instSeNm ?? "\u00A0"}</span>
<span className="text-[1.3rem] text-gray-500">{aplcntOgdpNm || userInfo?.instSeNm || "\u00A0"}</span>
</td>
<th style={{ textAlign: "center" }}>이메일</th>
<td>
<span className="text-[1.3rem] text-gray-500">{userInfo?.email ?? "\u00A0"}</span>
<span className="text-[1.3rem] text-gray-500">{aplcntEmlAddr || userInfo?.email || "\u00A0"}</span>
</td>
</tr>