diff --git a/apps/as/src/entities/esntl/api/esntl-service.ts b/apps/as/src/entities/esntl/api/esntl-service.ts new file mode 100644 index 000000000..a8e60bc57 --- /dev/null +++ b/apps/as/src/entities/esntl/api/esntl-service.ts @@ -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 { + const response = await client.get( + `${ENDPOINT.AS_COMMON_SERVICE.ESNTL_ATCH_FILE}/list/tree`, + { params }, + ); + return response.data; +} + +export const esntlAtchFileMngService = { + findTree, +} satisfies EsntlAtchFileMngService; diff --git a/apps/as/src/entities/esntl/index.ts b/apps/as/src/entities/esntl/index.ts new file mode 100644 index 000000000..dfe42c24b --- /dev/null +++ b/apps/as/src/entities/esntl/index.ts @@ -0,0 +1,6 @@ +export type { + EsntlAtchFileMngSearchParams, + EsntlAtchFileMngService, + EsntlAtchFileMngTreeResponseDto, +} from "./model/esntl-interface"; +export { esntlAtchFileMngService } from "./api/esntl-service"; diff --git a/apps/as/src/entities/esntl/model/esntl-interface.ts b/apps/as/src/entities/esntl/model/esntl-interface.ts new file mode 100644 index 000000000..74f77ba72 --- /dev/null +++ b/apps/as/src/entities/esntl/model/esntl-interface.ts @@ -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; +} diff --git a/apps/as/src/pages/instl-sncn/ui/cnfm-regist-page.tsx b/apps/as/src/pages/instl-sncn/ui/cnfm-regist-page.tsx index 6424f9f17..866b37a8c 100644 --- a/apps/as/src/pages/instl-sncn/ui/cnfm-regist-page.tsx +++ b/apps/as/src/pages/instl-sncn/ui/cnfm-regist-page.tsx @@ -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>(() => - REQUIRED_ATTACHMENTS.reduce( - (acc, item) => { - acc[item.key] = { - key: item.key, - label: item.label, - atflCd: "", - atflId: "", - fileName: "", - uploading: false, - }; - return acc; - }, - {} as Record, - ), - ); - const requiredFileInputRefs = useRef>({ - applyForm: null, - planGoalYear: null, - adequacyReview: null, - supervisionCost: null, - costBreakdown: null, - designDrawing: null, - etc: null, + const { data: requiredAttachmentDefs = [] } = useQuery({ + queryKey: ["esntlAtchFileTree", docSeComCd], + queryFn: () => esntlAtchFileMngService.findTree({ docSeComCd }), + enabled: Boolean(docSeComCd), + staleTime: Infinity, }); + const [requiredFiles, setRequiredFiles] = useState>({}); + const requiredFileInputRefs = useRef>({}); + + useEffect(() => { + if (requiredAttachmentDefs.length === 0) return; + + setRequiredFiles((prev) => { + const next: Record = {}; + 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>({}); const { data: cklstTree = [] } = useQuery({ - 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() { - {REQUIRED_ATTACHMENTS.map(({ key, label }) => ( - - {label} - hwp/pdf - -
- - { - requiredFileInputRefs.current[key] = el; - }} - type="file" - className="hidden" - onChange={(e) => handleRequiredFileChange(key, e)} - /> - -
+ {requiredAttachmentDefs.length === 0 ? ( + + + 조회된 필수 첨부파일 항목이 없습니다. - ))} + ) : ( + requiredAttachmentDefs.map((item) => { + const key = item.esntlAtchFileId; + const requiredFile = requiredFiles[key]; + const fileFormat = (item.esntlAtchFileRmrk || "").replaceAll(",", "/") || "-"; + + return ( + + +
+ {item.esntlAtchFileNm} + {item.esntlAtchYn === "Y" ? * : null} +
+ + {fileFormat} + +
+ + { + requiredFileInputRefs.current[key] = el; + }} + type="file" + className="hidden" + onChange={(e) => handleRequiredFileChange(key, e)} + /> + +
+ + + ); + }) + )} diff --git a/apps/as/src/shared/config/api.ts b/apps/as/src/shared/config/api.ts index 440f3e86b..bb6c53f53 100644 --- a/apps/as/src/shared/config/api.ts +++ b/apps/as/src/shared/config/api.ts @@ -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",