[as instl] 설치인가 필수 첨부 파일 수정

This commit is contained in:
2026-05-22 14:11:13 +09:00
parent da8f66fca1
commit 0e8e646547
5 changed files with 161 additions and 81 deletions
@@ -0,0 +1,22 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type {
EsntlAtchFileMngSearchParams,
EsntlAtchFileMngService,
EsntlAtchFileMngTreeResponseDto,
} from "../model/esntl-interface";
async function findTree(
params: EsntlAtchFileMngSearchParams,
): Promise<EsntlAtchFileMngTreeResponseDto[]> {
const response = await client.get<EsntlAtchFileMngTreeResponseDto[]>(
`${ENDPOINT.AS_COMMON_SERVICE.ESNTL_ATCH_FILE}/list/tree`,
{ params },
);
return response.data;
}
export const esntlAtchFileMngService = {
findTree,
} satisfies EsntlAtchFileMngService;
+6
View File
@@ -0,0 +1,6 @@
export type {
EsntlAtchFileMngSearchParams,
EsntlAtchFileMngService,
EsntlAtchFileMngTreeResponseDto,
} from "./model/esntl-interface";
export { esntlAtchFileMngService } from "./api/esntl-service";
@@ -0,0 +1,25 @@
/** 필수첨부파일관리 트리 조회 응답 DTO */
export interface EsntlAtchFileMngTreeResponseDto {
/** 필수첨부파일아이디 */
esntlAtchFileId: string;
/** 문서구분공통코드 */
docSeComCd: string;
/** 필수첨부파일명 */
esntlAtchFileNm: string;
/** 필수첨부여부 ("Y" | "N") */
esntlAtchYn: "Y" | "N";
/** 필수첨부파일비고 (파일형식 등) */
esntlAtchFileRmrk: string;
}
/** 필수첨부파일관리 조회 파라미터 */
export interface EsntlAtchFileMngSearchParams {
/** 문서구분공통코드 (예: "INSTL") */
docSeComCd: string;
}
/** 필수첨부파일관리 서비스 인터페이스 */
export interface EsntlAtchFileMngService {
/** 필수첨부파일 트리 조회 */
findTree(params: EsntlAtchFileMngSearchParams): Promise<EsntlAtchFileMngTreeResponseDto[]>;
}
@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import { type HttpResponse,Upload } from "tus-js-client";
import { type HttpResponse, Upload } from "tus-js-client";
import { AUTH_USER_ID, CLAIM_NAME, ENDPOINT, PROXY_HOST } from "@/shared/config";
import { getCookie, setCookie } from "@/shared/lib";
@@ -15,6 +15,10 @@ import type { CklstTreeNode } from "@/entities/cklst";
import { cklstMngService } from "@/entities/cklst";
import type { CnstpSeMngTreeDto } from "@/entities/cnstp";
import { cnstpService } from "@/entities/cnstp";
import {
esntlAtchFileMngService,
type EsntlAtchFileMngTreeResponseDto,
} from "@/entities/esntl";
import {
INSTL_SNCN_REGISTER_SELECTION_KEY,
InstlSncnApi,
@@ -42,34 +46,19 @@ type BzAmtRow = {
thtiDsgn: string;
};
type RequiredAttachKey =
| "applyForm"
| "planGoalYear"
| "adequacyReview"
| "supervisionCost"
| "costBreakdown"
| "designDrawing"
| "etc";
type RequiredAttachKey = string;
type RequiredAttachItem = {
key: RequiredAttachKey;
label: string;
esntlAtchFileNm: string;
esntlAtchYn: "Y" | "N";
esntlAtchFileRmrk: 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: "" });
@@ -99,6 +88,7 @@ const ReadCell = ({ value }: { value: string | number }) => (
/* ------------------------------------------------------------------ */
function CnfmRegistPage() {
const router = useRouter();
const [docSeComCd] = useState("INSTL");
const [taskSeComCdNm, setTaskSeComCdNm] = useState("재이용수");
const [taskSeComCd, setTaskSeComCd] = useState("RE");
@@ -139,32 +129,39 @@ function CnfmRegistPage() {
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 { data: requiredAttachmentDefs = [] } = useQuery<EsntlAtchFileMngTreeResponseDto[]>({
queryKey: ["esntlAtchFileTree", docSeComCd],
queryFn: () => esntlAtchFileMngService.findTree({ docSeComCd }),
enabled: Boolean(docSeComCd),
staleTime: Infinity,
});
const [requiredFiles, setRequiredFiles] = useState<Record<RequiredAttachKey, RequiredAttachItem>>({});
const requiredFileInputRefs = useRef<Record<RequiredAttachKey, HTMLInputElement | null>>({});
useEffect(() => {
if (requiredAttachmentDefs.length === 0) return;
setRequiredFiles((prev) => {
const next: Record<RequiredAttachKey, RequiredAttachItem> = {};
for (const item of requiredAttachmentDefs) {
const key = item.esntlAtchFileId;
const prevItem = prev[key];
next[key] = {
key,
esntlAtchFileNm: item.esntlAtchFileNm,
esntlAtchYn: item.esntlAtchYn,
esntlAtchFileRmrk: item.esntlAtchFileRmrk,
atflCd: prevItem?.atflCd ?? "",
atflId: prevItem?.atflId ?? "",
fileName: prevItem?.fileName ?? "",
uploading: prevItem?.uploading ?? false,
};
}
return next;
});
}, [requiredAttachmentDefs]);
const uploadRequiredFile = (file: File, key: RequiredAttachKey) => {
setRequiredFiles((prev) => ({
...prev,
@@ -236,9 +233,9 @@ function CnfmRegistPage() {
const cklstRowRefs = useRef<Record<string, HTMLTableRowElement | null>>({});
const { data: cklstTree = [] } = useQuery<CklstTreeNode[]>({
queryKey: ["cklstTree", "INSTL", taskSeComCd],
queryFn: () => cklstMngService.findTree({ docSeComCd: "INSTL", taskSeComCd }),
enabled: Boolean(taskSeComCd),
queryKey: ["cklstTree", docSeComCd, taskSeComCd],
queryFn: () => cklstMngService.findTree({ docSeComCd, taskSeComCd }),
enabled: Boolean(docSeComCd) && Boolean(taskSeComCd),
staleTime: Infinity,
});
@@ -479,7 +476,7 @@ function CnfmRegistPage() {
/* ── 검증 ── */
function fnValidation(mode: "SAVE" | "PRESENTN") {
/*
if (!bzNm.trim()) {
alert("사업명을 입력하세요.");
return false;
@@ -506,9 +503,19 @@ function CnfmRegistPage() {
alert("체크리스트를 모두 체크하세요.");
return false;
}
// 저장 시: 사용 항목(Y)은 검토 결과가 반드시 예(Y)여야 함
*/
// 저장 시: 필수 첨부파일(esntlAtchYn=Y) 업로드 여부 검증
if (mode === "SAVE") {
const missingRequiredFile = requiredAttachmentDefs.find((item) => {
if (item.esntlAtchYn !== "Y") return false;
const uploaded = requiredFiles[item.esntlAtchFileId];
return !(uploaded?.atflCd && uploaded?.atflId);
});
if (missingRequiredFile) {
alert(`${missingRequiredFile.esntlAtchFileNm} 파일을 첨부하세요.`);
return false;
}
// 저장 시: 사용 항목(Y)은 검토 결과가 반드시 예(Y)여야 함
const invalidYesRow = cklstRows.find(
(row) => row.bchckUseYn === "Y" && checkResults[cklstKey(row)] !== "Y",
);
@@ -1116,39 +1123,58 @@ function CnfmRegistPage() {
</tr>
</thead>
<tbody>
{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">
<input
type="text"
placeholder="첨부문서를 올려주세요"
className="cnfm-input flex-1"
value={requiredFiles[key]?.uploading ? "업로드 중..." : requiredFiles[key]?.fileName || ""}
readOnly
/>
<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>
{requiredAttachmentDefs.length === 0 ? (
<tr>
<td colSpan={3} className="cnfm-td-empty">
조회된 필수 첨부파일 항목이 없습니다.
</td>
</tr>
))}
) : (
requiredAttachmentDefs.map((item) => {
const key = item.esntlAtchFileId;
const requiredFile = requiredFiles[key];
const fileFormat = (item.esntlAtchFileRmrk || "").replaceAll(",", "/") || "-";
return (
<tr key={key}>
<td className="pl-2">
<div className="w-full text-left">
{item.esntlAtchFileNm}
{item.esntlAtchYn === "Y" ? <span className="ml-1 text-red-500">*</span> : null}
</div>
</td>
<td>{fileFormat}</td>
<td>
<div className="flex items-center justify-center gap-2">
<input
type="text"
placeholder="첨부문서를 올려주세요"
className="cnfm-input flex-1"
value={requiredFile?.uploading ? "업로드 중..." : requiredFile?.fileName || ""}
readOnly
/>
<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={requiredFile?.uploading}
>
{requiredFile?.uploading ? "업로드중" : "첨부하기"}
</button>
</div>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
+1
View File
@@ -6,6 +6,7 @@ export const ENDPOINT = {
AS_COMMON_SERVICE: {
ACNTG_SE: "/as-common-service/api/v1/acntg-se",
CNSTP_SE: "/as-common-service/api/v1/cnstp-se",
ESNTL_ATCH_FILE: "/as-common-service/api/v1/esntl-atch-file",
},
AS_INSTL_SERVICE: {
INSTL_SNCN: "/as-instl-service/api/v1/instl-sncn",