|
|
|
@@ -4,7 +4,10 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
|
|
|
|
import { useRouter } from "next/navigation";
|
|
|
|
|
|
|
|
|
|
import { useQuery } from "@tanstack/react-query";
|
|
|
|
|
import { type HttpResponse,Upload } from "tus-js-client";
|
|
|
|
|
|
|
|
|
|
import { AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/config";
|
|
|
|
|
import { getCookie, setCookie } from "@/shared/lib";
|
|
|
|
|
import type { AcntgSeMngTreeDto } from "@/entities/acntg";
|
|
|
|
|
import { acntgService } from "@/entities/acntg";
|
|
|
|
|
import type { CklstTreeNode } from "@/entities/cklst";
|
|
|
|
@@ -38,6 +41,34 @@ type BzAmtRow = {
|
|
|
|
|
thtiDsgn: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type RequiredAttachKey =
|
|
|
|
|
| "applyForm"
|
|
|
|
|
| "planGoalYear"
|
|
|
|
|
| "adequacyReview"
|
|
|
|
|
| "supervisionCost"
|
|
|
|
|
| "costBreakdown"
|
|
|
|
|
| "designDrawing"
|
|
|
|
|
| "etc";
|
|
|
|
|
|
|
|
|
|
type RequiredAttachItem = {
|
|
|
|
|
key: RequiredAttachKey;
|
|
|
|
|
label: string;
|
|
|
|
|
atflCd: string;
|
|
|
|
|
atflId: string;
|
|
|
|
|
fileName: string;
|
|
|
|
|
uploading: boolean;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const REQUIRED_ATTACHMENTS: Array<{ key: RequiredAttachKey; label: string }> = [
|
|
|
|
|
{ key: "applyForm", label: "인가신청서" },
|
|
|
|
|
{ key: "planGoalYear", label: "계획수립 기준 및 목표연도 계획" },
|
|
|
|
|
{ key: "adequacyReview", label: "하수도 시설계획 적정성 검토 결과" },
|
|
|
|
|
{ key: "supervisionCost", label: "관로설계시설물 감리비용 산출 계획서" },
|
|
|
|
|
{ key: "costBreakdown", label: "사업비 내역서" },
|
|
|
|
|
{ key: "designDrawing", label: "설계도면" },
|
|
|
|
|
{ key: "etc", label: "기타 첨부자료" },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const emptyScale = (): BzScaleRow => ({ bscPl: "", budgetReflct: "", thtiDsgn: "" });
|
|
|
|
|
const emptyAmt = (): BzAmtRow => ({ bscPl: "", budgetReflct: "", thtiDsgn: "" });
|
|
|
|
|
|
|
|
|
@@ -106,6 +137,98 @@ function CnfmRegistPage() {
|
|
|
|
|
const [etcFiles, setEtcFiles] = useState<EtcFile[]>([]);
|
|
|
|
|
const [etcNoticeOpen, setEtcNoticeOpen] = useState(true);
|
|
|
|
|
|
|
|
|
|
/* ── 필수 첨부파일 ── */
|
|
|
|
|
const [requiredFiles, setRequiredFiles] = useState<Record<RequiredAttachKey, RequiredAttachItem>>(() =>
|
|
|
|
|
REQUIRED_ATTACHMENTS.reduce(
|
|
|
|
|
(acc, item) => {
|
|
|
|
|
acc[item.key] = {
|
|
|
|
|
key: item.key,
|
|
|
|
|
label: item.label,
|
|
|
|
|
atflCd: "",
|
|
|
|
|
atflId: "",
|
|
|
|
|
fileName: "",
|
|
|
|
|
uploading: false,
|
|
|
|
|
};
|
|
|
|
|
return acc;
|
|
|
|
|
},
|
|
|
|
|
{} as Record<RequiredAttachKey, RequiredAttachItem>,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
const requiredFileInputRefs = useRef<Record<RequiredAttachKey, HTMLInputElement | null>>({
|
|
|
|
|
applyForm: null,
|
|
|
|
|
planGoalYear: null,
|
|
|
|
|
adequacyReview: null,
|
|
|
|
|
supervisionCost: null,
|
|
|
|
|
costBreakdown: null,
|
|
|
|
|
designDrawing: null,
|
|
|
|
|
etc: null,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const uploadRequiredFile = (file: File, key: RequiredAttachKey) => {
|
|
|
|
|
setRequiredFiles((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[key]: { ...prev[key], uploading: true },
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
setCookie("atfl-cd-key", "");
|
|
|
|
|
|
|
|
|
|
const upload = new Upload(file, {
|
|
|
|
|
endpoint: ENDPOINT.ATTACH_SERVICE.TUS_UPLOAD,
|
|
|
|
|
chunkSize: 10 * 1024 * 1024,
|
|
|
|
|
headers: {
|
|
|
|
|
[CLAIM_NAME]: getCookie(CLAIM_NAME) ?? "",
|
|
|
|
|
[AUTH_USER_ID]: getCookie(AUTH_USER_ID) ?? "",
|
|
|
|
|
},
|
|
|
|
|
metadata: {
|
|
|
|
|
filename: file.name,
|
|
|
|
|
filetype: file.type,
|
|
|
|
|
},
|
|
|
|
|
onError(error: Error) {
|
|
|
|
|
console.error("필수 첨부파일 업로드 실패", error);
|
|
|
|
|
setRequiredFiles((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[key]: { ...prev[key], uploading: false },
|
|
|
|
|
}));
|
|
|
|
|
alert("필수 첨부파일 업로드 중 오류가 발생했습니다.");
|
|
|
|
|
},
|
|
|
|
|
onAfterResponse(_req: unknown, res: HttpResponse) {
|
|
|
|
|
const fileInfo = res.getHeader("X-File-Info");
|
|
|
|
|
if (typeof fileInfo !== "string") return;
|
|
|
|
|
|
|
|
|
|
const parsed = JSON.parse(fileInfo) as { atflCd?: string; atflId?: string };
|
|
|
|
|
const atflCd = parsed.atflCd ?? "";
|
|
|
|
|
const atflId = parsed.atflId ?? "";
|
|
|
|
|
if (atflCd) setCookie("atfl-cd-key", atflCd);
|
|
|
|
|
|
|
|
|
|
setRequiredFiles((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[key]: {
|
|
|
|
|
...prev[key],
|
|
|
|
|
atflCd,
|
|
|
|
|
atflId,
|
|
|
|
|
fileName: file.name,
|
|
|
|
|
uploading: false,
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
},
|
|
|
|
|
onSuccess() {
|
|
|
|
|
setRequiredFiles((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[key]: { ...prev[key], uploading: false },
|
|
|
|
|
}));
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
upload.start();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleRequiredFileChange = (key: RequiredAttachKey, e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
const file = e.target.files?.[0];
|
|
|
|
|
if (!file) return;
|
|
|
|
|
uploadRequiredFile(file, key);
|
|
|
|
|
e.target.value = "";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* ── 체크리스트 ── */
|
|
|
|
|
type CheckResultVal = "Y" | "N" | null;
|
|
|
|
|
const [checkResults, setCheckResults] = useState<Record<string, CheckResultVal>>({});
|
|
|
|
@@ -212,6 +335,14 @@ function CnfmRegistPage() {
|
|
|
|
|
selected: checkResults[cklstKey(row)] ?? null,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const toEsntlAtchFileSaveDtoList = () =>
|
|
|
|
|
Object.values(requiredFiles)
|
|
|
|
|
.filter((item) => Boolean(item.atflCd) && Boolean(item.atflId))
|
|
|
|
|
.map((item) => ({
|
|
|
|
|
esntlAtchFileId: item.key,
|
|
|
|
|
atchFileGroupId: item.atflCd,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
/* ── 사업규모 (공종구분 트리 연동) ── */
|
|
|
|
|
// cnstpSeCd → { bscPl, budgetReflct, thtiDsgn } 단일 map
|
|
|
|
|
const [bzScaleMap, setBzScaleMap] = useState<Record<string, BzScaleRow>>({});
|
|
|
|
@@ -436,6 +567,7 @@ function CnfmRegistPage() {
|
|
|
|
|
bzScaleList: toBzScaleSaveDtoList(),
|
|
|
|
|
bizCstList: toBizCstSaveDtoList(),
|
|
|
|
|
cklstResultList: toCklstSaveDtoList(),
|
|
|
|
|
esntlAtchFileList: toEsntlAtchFileSaveDtoList(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
@@ -966,7 +1098,7 @@ function CnfmRegistPage() {
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* ── 첨부파일 ── */}
|
|
|
|
|
{/* ── 필수 첨부파일 ── */}
|
|
|
|
|
<div className="cnfm-card p-4">
|
|
|
|
|
<h3 className="mb-3 border-b pb-2 text-[1.5rem] font-bold text-gray-700">첨부파일</h3>
|
|
|
|
|
<div className="cnfm-table-wrap">
|
|
|
|
@@ -979,17 +1111,9 @@ function CnfmRegistPage() {
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{[
|
|
|
|
|
"인가신청서",
|
|
|
|
|
"계획수립 기준 및 목표연도 계획",
|
|
|
|
|
"하수도 시설계획 적정성 검토 결과",
|
|
|
|
|
"관로설계시설물 감리비용 산출 계획서",
|
|
|
|
|
"사업비 내역서",
|
|
|
|
|
"설계도면",
|
|
|
|
|
"기타 첨부자료",
|
|
|
|
|
].map((name) => (
|
|
|
|
|
<tr key={name}>
|
|
|
|
|
<td className="pl-2 text-left">{name}</td>
|
|
|
|
|
{REQUIRED_ATTACHMENTS.map(({ key, label }) => (
|
|
|
|
|
<tr key={key}>
|
|
|
|
|
<td className="pl-2 text-left">{label}</td>
|
|
|
|
|
<td>hwp/pdf</td>
|
|
|
|
|
<td>
|
|
|
|
|
<div className="flex items-center justify-center gap-2">
|
|
|
|
@@ -997,9 +1121,25 @@ function CnfmRegistPage() {
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="첨부문서를 올려주세요"
|
|
|
|
|
className="cnfm-input flex-1"
|
|
|
|
|
value={requiredFiles[key]?.uploading ? "업로드 중..." : requiredFiles[key]?.fileName || ""}
|
|
|
|
|
readOnly
|
|
|
|
|
/>
|
|
|
|
|
<button className="cnfm-btn-search text-[1.2rem]">첨부하기</button>
|
|
|
|
|
<input
|
|
|
|
|
ref={(el) => {
|
|
|
|
|
requiredFileInputRefs.current[key] = el;
|
|
|
|
|
}}
|
|
|
|
|
type="file"
|
|
|
|
|
className="hidden"
|
|
|
|
|
onChange={(e) => handleRequiredFileChange(key, e)}
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="cnfm-btn-search text-[1.2rem]"
|
|
|
|
|
onClick={() => requiredFileInputRefs.current[key]?.click()}
|
|
|
|
|
disabled={requiredFiles[key]?.uploading}
|
|
|
|
|
>
|
|
|
|
|
{requiredFiles[key]?.uploading ? "업로드중" : "첨부하기"}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|