From e3d022fcd73c689fbc8c54471697ed0444964f81 Mon Sep 17 00:00:00 2001 From: jangwoong Date: Thu, 21 May 2026 18:58:51 +0900 Subject: [PATCH] =?UTF-8?q?[as=20instl]=20=ED=8C=8C=EC=9D=BC=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/as/package.json | 1 + .../instl-sncn/model/instl-sncn-interface.ts | 7 + .../pages/instl-sncn/ui/cnfm-regist-page.tsx | 166 ++++++++++++++++-- apps/as/src/shared/config/api.ts | 4 + 4 files changed, 165 insertions(+), 13 deletions(-) diff --git a/apps/as/package.json b/apps/as/package.json index 0ca386f38..b30e4e45e 100644 --- a/apps/as/package.json +++ b/apps/as/package.json @@ -28,6 +28,7 @@ "lucide-react": "^0.474.0", "next": "15.3.8", "react-hook-form": "^7.54.2", + "tus-js-client": "^4.3.1", "zod": "^3.24.2" }, "devDependencies": { diff --git a/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts b/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts index 0209bfef3..63d280d86 100644 --- a/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts +++ b/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts @@ -88,12 +88,19 @@ export interface InstlSncnCklstResultSaveDto { selected: "Y" | "N" | null; } +/** 설치인가 신청 - 필수첨부파일 저장 DTO(배열 원소) */ +export interface InstlSncnEsntlAtchFileSaveDto { + esntlAtchFileId: string; + atchFileGroupId: string; +} + /** 설치인가 신청 - 통합 저장 요청 DTO */ export interface InstlSncnApplySaveRequestDto { instlSncnMng: InstlSncnMng; bzScaleList: InstlSncnBzScaleSaveDto[]; bizCstList: InstlSncnBizCstSaveRequestDto[]; cklstResultList: InstlSncnCklstResultSaveDto[]; + esntlAtchFileList: InstlSncnEsntlAtchFileSaveDto[]; } export interface InstlSncnMngService { 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 b3414a652..e7f2e1261 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 @@ -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([]); 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 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) => { + const file = e.target.files?.[0]; + if (!file) return; + uploadRequiredFile(file, key); + e.target.value = ""; + }; + /* ── 체크리스트 ── */ type CheckResultVal = "Y" | "N" | null; const [checkResults, setCheckResults] = useState>({}); @@ -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>({}); @@ -436,6 +567,7 @@ function CnfmRegistPage() { bzScaleList: toBzScaleSaveDtoList(), bizCstList: toBizCstSaveDtoList(), cklstResultList: toCklstSaveDtoList(), + esntlAtchFileList: toEsntlAtchFileSaveDtoList(), }; try { @@ -966,7 +1098,7 @@ function CnfmRegistPage() { - {/* ── 첨부파일 ── */} + {/* ── 필수 첨부파일 ── */}

첨부파일

@@ -979,17 +1111,9 @@ function CnfmRegistPage() { - {[ - "인가신청서", - "계획수립 기준 및 목표연도 계획", - "하수도 시설계획 적정성 검토 결과", - "관로설계시설물 감리비용 산출 계획서", - "사업비 내역서", - "설계도면", - "기타 첨부자료", - ].map((name) => ( - - {name} + {REQUIRED_ATTACHMENTS.map(({ key, label }) => ( + + {label} hwp/pdf
@@ -997,9 +1121,25 @@ function CnfmRegistPage() { type="text" placeholder="첨부문서를 올려주세요" className="cnfm-input flex-1" + value={requiredFiles[key]?.uploading ? "업로드 중..." : requiredFiles[key]?.fileName || ""} readOnly /> - + { + 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 5963f4c84..440f3e86b 100644 --- a/apps/as/src/shared/config/api.ts +++ b/apps/as/src/shared/config/api.ts @@ -1,4 +1,8 @@ export const ENDPOINT = { + ATTACH_SERVICE: { + TUS_UPLOAD: "/attach-service/api/v1/tus/upload", + ATTACHMENTS: "/attach-service/api/v1/atfl/attachments", + }, AS_COMMON_SERVICE: { ACNTG_SE: "/as-common-service/api/v1/acntg-se", CNSTP_SE: "/as-common-service/api/v1/cnstp-se",