[as instl] 설치인가 필수 첨부 파일 수정
This commit is contained in:
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user