diff --git a/apps/as/app/(with-base-layout)/acntg-code-management/page.tsx b/apps/as/app/(with-base-layout)/acntg-code-management/page.tsx new file mode 100644 index 000000000..c22d0f0b4 --- /dev/null +++ b/apps/as/app/(with-base-layout)/acntg-code-management/page.tsx @@ -0,0 +1 @@ +export { AcntgCodeManagementPage as default } from "@/pages/acntg-code-management"; diff --git a/apps/as/app/(with-base-layout)/cnstp-code-management/page.tsx b/apps/as/app/(with-base-layout)/cnstp-code-management/page.tsx new file mode 100644 index 000000000..d20a49e4f --- /dev/null +++ b/apps/as/app/(with-base-layout)/cnstp-code-management/page.tsx @@ -0,0 +1 @@ +export { CnstpCodeManagementPage as default } from "@/pages/cnstp-code-management"; diff --git a/apps/as/app/(with-base-layout)/code-management/page.tsx b/apps/as/app/(with-base-layout)/code-management/page.tsx deleted file mode 100644 index 7a4daf43f..000000000 --- a/apps/as/app/(with-base-layout)/code-management/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { CodeManagementPage as default } from "@/pages/code-management"; diff --git a/apps/as/src/entities/cklst/model/cklst-mng-interface.ts b/apps/as/src/entities/cklst/model/cklst-mng-interface.ts index 7e3187ce7..f6c80e630 100644 --- a/apps/as/src/entities/cklst/model/cklst-mng-interface.ts +++ b/apps/as/src/entities/cklst/model/cklst-mng-interface.ts @@ -24,6 +24,8 @@ export interface CklstTreeNode { rvwCn: string; bchckUseYn: "Y" | "N"; sortSeq: number; + // optional: backend may include the taskSeComCd per node + taskSeComCd?: string; } export interface CklstTreeSearchParams { diff --git a/apps/as/src/entities/instl-sncn/api/instl-sncn-service.ts b/apps/as/src/entities/instl-sncn/api/instl-sncn-service.ts index 434a77918..208e0172d 100644 --- a/apps/as/src/entities/instl-sncn/api/instl-sncn-service.ts +++ b/apps/as/src/entities/instl-sncn/api/instl-sncn-service.ts @@ -29,6 +29,13 @@ export class InstlSncnApiService implements InstlSncnMngService { ); return response.data; } + public async techReview(payload: InstlSncnApplySaveRequestDto): Promise { + const response = await client.post( + `${ENDPOINT.AS_INSTL_SERVICE.INSTL_SNCN}/tech-review`, + payload, + ); + return response.data; + } } export const InstlSncnApi = new InstlSncnApiService(); 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 b2872fea7..e9a4060ab 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,6 +88,7 @@ export interface InstlSncnCklstResultSaveDto { cklstRvwSn: number; rvwStpComCd: string; selected: "Y" | "N" | null; + picRvwCn?: string | null; } /** 설치인가 신청 - 필수첨부파일 저장 DTO(배열 원소) */ @@ -111,7 +112,7 @@ export interface InstlSncnMngDetResponse { detDto: InstlSncnMng; bizSclList: any[]; bizCstList: any[]; - cklstResultMap?: Record; + cklstResultList?: any[]; esntlFileList: any[]; etcFileList: any[]; } diff --git a/apps/as/src/pages/acntg-code-management/index.ts b/apps/as/src/pages/acntg-code-management/index.ts new file mode 100644 index 000000000..ee4bfa445 --- /dev/null +++ b/apps/as/src/pages/acntg-code-management/index.ts @@ -0,0 +1 @@ +export { AcntgCodeManagementPage } from "./ui/acntg-code-management-page"; diff --git a/apps/as/src/pages/code-management/model/acntg-se-table-columns.tsx b/apps/as/src/pages/acntg-code-management/model/acntg-se-table-columns.tsx similarity index 100% rename from apps/as/src/pages/code-management/model/acntg-se-table-columns.tsx rename to apps/as/src/pages/acntg-code-management/model/acntg-se-table-columns.tsx diff --git a/apps/as/src/pages/code-management/ui/acntg-se-section.tsx b/apps/as/src/pages/acntg-code-management/ui/acntg-code-management-page.tsx similarity index 70% rename from apps/as/src/pages/code-management/ui/acntg-se-section.tsx rename to apps/as/src/pages/acntg-code-management/ui/acntg-code-management-page.tsx index f0daf9d4f..d3fe311a5 100644 --- a/apps/as/src/pages/code-management/ui/acntg-se-section.tsx +++ b/apps/as/src/pages/acntg-code-management/ui/acntg-code-management-page.tsx @@ -20,13 +20,15 @@ import { createAcntgSeColumns } from "../model/acntg-se-table-columns"; import { AcntgSeFormDialog, type AcntgSeFormValues } from "./acntg-se-form-dialog"; -interface AcntgSeSectionProps { - pagination: PaginationState; - onPaginationChange: (pagination: PaginationState) => void; -} +const DEFAULT_PAGE_INDEX = 0; +const DEFAULT_PAGE_SIZE = 10; -function AcntgSeSection({ pagination, onPaginationChange }: AcntgSeSectionProps) { +function AcntgCodeManagementPage() { const { confirm } = useConfirm(); + const [pagination, setPagination] = useState({ + pageIndex: DEFAULT_PAGE_INDEX, + pageSize: DEFAULT_PAGE_SIZE, + }); const [dialogMode, setDialogMode] = useState<"create" | "edit" | null>(null); const [editingCd, setEditingCd] = useState(null); @@ -67,7 +69,7 @@ function AcntgSeSection({ pagination, onPaginationChange }: AcntgSeSectionProps) state: { pagination }, onPaginationChange: (updater) => { const next = typeof updater === "function" ? updater(pagination) : updater; - onPaginationChange(next); + setPagination(next); }, getCoreRowModel: getCoreRowModel(), manualPagination: true, @@ -114,31 +116,33 @@ function AcntgSeSection({ pagination, onPaginationChange }: AcntgSeSectionProps) }; return ( -
-
-

회계코드 관리

- -
- {tableComponent} - {dialogMode ? ( - { - if (!open) { - setDialogMode(null); - setEditingCd(null); - } - }} - isSubmitting={saveMutation.isPending || updateMutation.isPending} - onSubmit={handleFormSubmit} - /> - ) : null} -
+
+
+
+

회계코드 관리

+ +
+ {tableComponent} + {dialogMode ? ( + { + if (!open) { + setDialogMode(null); + setEditingCd(null); + } + }} + isSubmitting={saveMutation.isPending || updateMutation.isPending} + onSubmit={handleFormSubmit} + /> + ) : null} +
+
); } -export { AcntgSeSection }; +export { AcntgCodeManagementPage }; diff --git a/apps/as/src/pages/code-management/ui/acntg-se-form-dialog.tsx b/apps/as/src/pages/acntg-code-management/ui/acntg-se-form-dialog.tsx similarity index 100% rename from apps/as/src/pages/code-management/ui/acntg-se-form-dialog.tsx rename to apps/as/src/pages/acntg-code-management/ui/acntg-se-form-dialog.tsx diff --git a/apps/as/src/pages/cnstp-code-management/index.ts b/apps/as/src/pages/cnstp-code-management/index.ts new file mode 100644 index 000000000..2ed1cb4ba --- /dev/null +++ b/apps/as/src/pages/cnstp-code-management/index.ts @@ -0,0 +1 @@ +export { CnstpCodeManagementPage } from "./ui/cnstp-code-management-page"; diff --git a/apps/as/src/pages/code-management/model/cnstp-se-table-columns.tsx b/apps/as/src/pages/cnstp-code-management/model/cnstp-se-table-columns.tsx similarity index 100% rename from apps/as/src/pages/code-management/model/cnstp-se-table-columns.tsx rename to apps/as/src/pages/cnstp-code-management/model/cnstp-se-table-columns.tsx diff --git a/apps/as/src/pages/code-management/ui/cnstp-se-section.tsx b/apps/as/src/pages/cnstp-code-management/ui/cnstp-code-management-page.tsx similarity index 71% rename from apps/as/src/pages/code-management/ui/cnstp-se-section.tsx rename to apps/as/src/pages/cnstp-code-management/ui/cnstp-code-management-page.tsx index 23b9d792f..6244337b8 100644 --- a/apps/as/src/pages/code-management/ui/cnstp-se-section.tsx +++ b/apps/as/src/pages/cnstp-code-management/ui/cnstp-code-management-page.tsx @@ -23,13 +23,12 @@ import { CnstpSeFormDialog, type CnstpSeFormValues } from "./cnstp-se-form-dialo const DEFAULT_PAGE_INDEX = 0; const DEFAULT_PAGE_SIZE = 10; -interface CnstpSeSectionProps { - pagination: PaginationState; - onPaginationChange: (pagination: PaginationState) => void; -} - -function CnstpSeSection({ pagination, onPaginationChange }: CnstpSeSectionProps) { +function CnstpCodeManagementPage() { const { confirm } = useConfirm(); + const [pagination, setPagination] = useState({ + pageIndex: DEFAULT_PAGE_INDEX, + pageSize: DEFAULT_PAGE_SIZE, + }); const [dialogMode, setDialogMode] = useState<"create" | "edit" | null>(null); const [editingCd, setEditingCd] = useState(null); @@ -70,7 +69,7 @@ function CnstpSeSection({ pagination, onPaginationChange }: CnstpSeSectionProps) state: { pagination }, onPaginationChange: (updater) => { const next = typeof updater === "function" ? updater(pagination) : updater; - onPaginationChange(next); + setPagination(next); }, getCoreRowModel: getCoreRowModel(), manualPagination: true, @@ -117,31 +116,33 @@ function CnstpSeSection({ pagination, onPaginationChange }: CnstpSeSectionProps) }; return ( -
-
-

공종코드 관리

- -
- {tableComponent} - {dialogMode ? ( - { - if (!open) { - setDialogMode(null); - setEditingCd(null); - } - }} - isSubmitting={saveMutation.isPending || updateMutation.isPending} - onSubmit={handleFormSubmit} - /> - ) : null} -
+
+
+
+

공종코드 관리

+ +
+ {tableComponent} + {dialogMode ? ( + { + if (!open) { + setDialogMode(null); + setEditingCd(null); + } + }} + isSubmitting={saveMutation.isPending || updateMutation.isPending} + onSubmit={handleFormSubmit} + /> + ) : null} +
+
); } -export { CnstpSeSection, DEFAULT_PAGE_INDEX, DEFAULT_PAGE_SIZE }; +export { CnstpCodeManagementPage }; diff --git a/apps/as/src/pages/code-management/ui/cnstp-se-form-dialog.tsx b/apps/as/src/pages/cnstp-code-management/ui/cnstp-se-form-dialog.tsx similarity index 100% rename from apps/as/src/pages/code-management/ui/cnstp-se-form-dialog.tsx rename to apps/as/src/pages/cnstp-code-management/ui/cnstp-se-form-dialog.tsx diff --git a/apps/as/src/pages/code-management/index.ts b/apps/as/src/pages/code-management/index.ts deleted file mode 100644 index 3b00522e0..000000000 --- a/apps/as/src/pages/code-management/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CodeManagementPage } from "./ui/code-management-page"; diff --git a/apps/as/src/pages/code-management/ui/code-management-page.tsx b/apps/as/src/pages/code-management/ui/code-management-page.tsx deleted file mode 100644 index 95e7dcd9f..000000000 --- a/apps/as/src/pages/code-management/ui/code-management-page.tsx +++ /dev/null @@ -1,30 +0,0 @@ -"use client"; - -import { useState } from "react"; - -import type { PaginationState } from "@tanstack/react-table"; - -import { AcntgSeSection } from "./acntg-se-section"; -import { CnstpSeSection, DEFAULT_PAGE_INDEX, DEFAULT_PAGE_SIZE } from "./cnstp-se-section"; - -function CodeManagementPage() { - const [cnstpPagination, setCnstpPagination] = useState({ - pageIndex: DEFAULT_PAGE_INDEX, - pageSize: DEFAULT_PAGE_SIZE, - }); - const [acntgPagination, setAcntgPagination] = useState({ - pageIndex: DEFAULT_PAGE_INDEX, - pageSize: DEFAULT_PAGE_SIZE, - }); - - return ( -
-
- - -
-
- ); -} - -export { CodeManagementPage }; diff --git a/apps/as/src/pages/instl-sncn/ui/cnfm-list-page.tsx b/apps/as/src/pages/instl-sncn/ui/cnfm-list-page.tsx index a5ff158dd..0f5d24a62 100644 --- a/apps/as/src/pages/instl-sncn/ui/cnfm-list-page.tsx +++ b/apps/as/src/pages/instl-sncn/ui/cnfm-list-page.tsx @@ -71,15 +71,15 @@ function CnfmListPage() { const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(10); const [taskDialogOpen, setTaskDialogOpen] = useState(false); - const [selectedTaskSeComCd, setSelectedTaskSeComCd] = useState(""); + const [selectedTaskSeComCd, setSelectedTaskSeComCd] = useState([]); const { data: fnrsCodeList = [] } = useCodeList("INSTL"); useEffect(() => { if (!taskDialogOpen) return; - if (selectedTaskSeComCd) return; - if (fnrsCodeList.length === 0) return; - setSelectedTaskSeComCd(fnrsCodeList[0].lwpoCd); + if (selectedTaskSeComCd.length) return; + if (fnrsCodeList.length === 0) return; + // setSelectedTaskSeComCd([fnrsCodeList[0].lwpoCd]); }, [taskDialogOpen, selectedTaskSeComCd, fnrsCodeList]); const query = useQuery({ @@ -108,20 +108,25 @@ function CnfmListPage() { }; const handleOpenTaskDialog = () => { - setSelectedTaskSeComCd((prev) => prev || fnrsCodeList[0]?.lwpoCd || ""); + //setSelectedTaskSeComCd((prev) => (prev && prev.length ? prev : [fnrsCodeList[0]?.lwpoCd || ""])); setTaskDialogOpen(true); }; const handleConfirmTaskDialog = () => { - const selected = fnrsCodeList.find((item) => item.lwpoCd === selectedTaskSeComCd); - if (!selected) return; + if (!selectedTaskSeComCd || selectedTaskSeComCd.length === 0) { + alert("하나 이상의 업무구분을 선택하세요."); + return; + } + // prepare arrays of selected codes and names + const selectedCodes = selectedTaskSeComCd; + const selectedNames = selectedTaskSeComCd.map((code) => fnrsCodeList.find((item) => item.lwpoCd === code)?.lwpoCdNm || ""); sessionStorage.setItem( INSTL_SNCN_REGISTER_SELECTION_KEY, JSON.stringify({ - taskSeComCd: selected.lwpoCd, - taskSeComCdNm: selected.lwpoCdNm, - }), + taskSeComCdList: selectedCodes, + taskSeComCdNmList: selectedNames + }) ); setTaskDialogOpen(false); router.push("/instl-sncn/register"); @@ -367,11 +372,15 @@ function CnfmListPage() { {fnrsCodeList.map((item) => ( @@ -387,11 +396,11 @@ function CnfmListPage() { > 취소 - 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 7f815ea84..5bcd8925c 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 @@ -10,13 +10,18 @@ import { type HttpResponse, Upload } from "tus-js-client"; import { AUTH_USER_ID, CLAIM_NAME, ENDPOINT, PROXY_HOST } from "@/shared/config"; import { getCookie, setCookie, useAlertStore, useUserStore } from "@/shared/lib"; -import { deleteAttachment } from "@/shared/lib/attach"; +import { deleteAttachment } from "@/shared/lib/attach"; import { formatFileSize } from "@/shared/lib/file-utils"; -import { type AcntgSeMngTreeDto, useAcntgSeMngTree } from "@/entities/acntg"; +import type { AcntgSeMngTreeDto } from "@/entities/acntg"; +import { acntgSeMngService } from "@/entities/acntg"; import type { CklstTreeNode } from "@/entities/cklst"; import { cklstMngService } from "@/entities/cklst"; -import { type CnstpSeMngTreeDto, useCnstpSeMngTree } from "@/entities/cnstp"; -import { esntlAtchFileMngService, type EsntlAtchFileMngTreeResponseDto } from "@/entities/esntl"; +import type { CnstpSeMngTreeDto } from "@/entities/cnstp"; +import { cnstpSeMngService } from "@/entities/cnstp"; +import { + esntlAtchFileMngService, + type EsntlAtchFileMngTreeResponseDto, +} from "@/entities/esntl"; import { INSTL_SNCN_REGISTER_SELECTION_KEY, InstlSncnApi, @@ -25,7 +30,7 @@ import { type InstlSncnBzScaleSaveDto, type InstlSncnMng, } from "@/entities/instl-sncn"; -import { EtcAttachments, NumCell, PrivacyConsent, ReadCell } from "@/widgets/cnfm"; +import { EtcAttachments,NumCell, PrivacyConsent, ReadCell } from "@/widgets/cnfm"; import { Button, Input } from "@common/components"; import "./cnfm-page.css"; @@ -97,7 +102,7 @@ const INSTL_PROGRESS_ORDER = [ const getProgressStepCount = (prgrsSttsComCd?: string): number => { const idx = INSTL_PROGRESS_ORDER.indexOf(prgrsSttsComCd ?? "ST"); if (idx <= INSTL_PROGRESS_ORDER.indexOf("ST")) return 1; - if (idx <= INSTL_PROGRESS_ORDER.indexOf("TP")) return 2; + if (idx <= INSTL_PROGRESS_ORDER.indexOf("ER")) return 2; return 3; }; @@ -106,21 +111,24 @@ const getRoleStepCount = (dtlInstCd?: string): number => { return mapped ? REVIEW_STEP_ROLE_RANK[mapped] : 0; }; +// 업무구분 코드 -> 표시명 매핑 (체크리스트 그룹 표시용) +const TASK_CODE_LABELS: Record = { + RE: "재이용수", + SP: "하수관로", + SF: "하수처리시설", +}; + const normalizeChecklistResultRows = (payload: any): any[] => { - // support new API field `cklstResultMap` while keeping backward-compatibility - const raw = payload?.cklstResultMap ?? payload?.cklstResultList; - if (Array.isArray(raw)) return raw; - if (raw && typeof raw === "object") { - return Object.values(raw).flatMap((rows) => (Array.isArray(rows) ? rows : [])); - } - return []; + // expect flat `cklstResultList` only + const raw = payload?.cklstResultList ?? []; + return Array.isArray(raw) ? raw : []; }; /* ------------------------------------------------------------------ */ /* 메인 컴포넌트 */ /* ------------------------------------------------------------------ */ /** - * initialData 매핑 안내 + * initialData 매핑 안내 * - initialData 데이터는 cnfm-modify-page에서 서버로부터 받아온 상세 데이터를 그대로 전달받거나, cnfm-regist-page에서 수정 모드로 진입할 때 전달받을 수 있습니다. * - 이 컴포넌트는 수정 모드로 호출될 때 `initialData`를 받습니다. * - 서버 응답은 두 가지 형태 중 하나일 수 있습니다: @@ -147,7 +155,12 @@ const normalizeChecklistResultRows = (payload: any): any[] => { */ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { const router = useRouter(); - const userInfo = useUserStore((state) => state.userInfo); + const userInfo2 = useUserStore((state) => state.userInfo); + // userInfo?.dtlInstCd 테스트 코드 ENV.ROLE_USER03 , KECO 권한 테스트용 (실제 권한에 맞게 변경 필요) + const userInfo = { + ...userInfo2, + dtlInstCd: "ROLE_USER03", //ROLE_USER03, ROLE_USER02, ROLE_USER01 KECO 권한 테스트용 (실제 권한에 맞게 변경 필요) + }; const alert = useAlertStore((state) => state.setMessage); const qc = useQueryClient(); const currentDet = useMemo(() => initialData?.detDto ?? initialData, [initialData]); @@ -169,40 +182,53 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { return visibleReviewStepCodes.includes(step) ? step : null; }, [isModifyMode, userInfo?.dtlInstCd, visibleReviewStepCodes]); const [selectedReviewStep, setSelectedReviewStep] = useState("LOCAL"); + useEffect(() => { if (visibleReviewStepCodes.length === 0) return; - if (editableReviewStep && selectedReviewStep !== editableReviewStep) { - setSelectedReviewStep(editableReviewStep); - return; - } - if (!visibleReviewStepCodes.includes(selectedReviewStep)) { - setSelectedReviewStep(visibleReviewStepCodes[visibleReviewStepCodes.length - 1]); - } - }, [editableReviewStep, selectedReviewStep, visibleReviewStepCodes]); + // Only set default/initial selection when visible steps or editable step change. + // Do NOT include `selectedReviewStep` in deps to avoid overwriting user clicks. + setSelectedReviewStep((current) => { + // keep current if still visible + if (current && visibleReviewStepCodes.includes(current)) return current; + // prefer editable step if available + if (editableReviewStep) return editableReviewStep; + // otherwise pick the last visible step + return visibleReviewStepCodes[visibleReviewStepCodes.length - 1]; + }); + }, [editableReviewStep, visibleReviewStepCodes]); const docId = useMemo(() => { return currentDet?.instlSncnDocNo ?? ""; }, [currentDet]); const [docSeComCd] = useState("INSTL"); - const [taskSeComCdNm, setTaskSeComCdNm] = useState("재이용수"); - const [taskSeComCd, setTaskSeComCd] = useState("RE"); + const [taskSeComCdNm, setTaskSeComCdNm] = useState( + () => (currentDet?.taskSeComCdNm as string) ?? "재이용수", + ); + const [taskSeComCd, setTaskSeComCd] = useState(() => (currentDet?.taskSeComCd as string) ?? "RE"); useEffect(() => { + // do not override server-provided initialData (modify mode) + if (currentDet) return; + const raw = sessionStorage.getItem(INSTL_SNCN_REGISTER_SELECTION_KEY); if (!raw) return; try { - const parsed = JSON.parse(raw) as { taskSeComCd?: string; taskSeComCdNm?: string }; - setTaskSeComCd(parsed.taskSeComCd || "RE"); - setTaskSeComCdNm(parsed.taskSeComCdNm || "재이용수"); + const parsed = JSON.parse(raw) as { + taskSeComCdList?: string[]; + taskSeComCdNmList?: string[]; + }; + + setTaskSeComCd(parsed.taskSeComCdList?.join(",") || "RE"); + setTaskSeComCdNm(parsed.taskSeComCdNmList?.join(",") || "재이용수"); } catch { // noop } finally { sessionStorage.removeItem(INSTL_SNCN_REGISTER_SELECTION_KEY); } - }, []); + }, [currentDet]); /* ── 기본 정보 ── */ const [bzNm, setBzNm] = useState(""); const [bzBgngYr, setBzBgngYr] = useState(""); @@ -232,9 +258,15 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { staleTime: Infinity, }); - const [requiredFiles, setRequiredFiles] = useState>( - {}, - ); + // selected task code for header filtering (null = none selected) + const [selectedTaskCode, setSelectedTaskCode] = useState(null); + + + + + + + const [requiredFiles, setRequiredFiles] = useState>({}); const requiredFileInputRefs = useRef>({}); useEffect(() => { @@ -318,22 +350,22 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { upload.start(); }; - /* ── 기타 첨부파일 ── */ - type EtcFile = { - id: string; - name: string; - ext: string; - size: string; - uploader: string; - atflCd: string; - atflId: string; - }; - const [etcFiles, setEtcFiles] = useState([]); - const [etcNoticeOpen, setEtcNoticeOpen] = useState(true); - const [etcFileGroupId, setEtcFileGroupId] = useState(""); - const [etcUploading, setEtcUploading] = useState(false); - const etcFileGroupIdRef = useRef(""); - const initialMappedRef = useRef(false); + /* ── 기타 첨부파일 ── */ + type EtcFile = { + id: string; + name: string; + ext: string; + size: string; + uploader: string; + atflCd: string; + atflId: string; + }; + const [etcFiles, setEtcFiles] = useState([]); + const [etcNoticeOpen, setEtcNoticeOpen] = useState(true); + const [etcFileGroupId, setEtcFileGroupId] = useState(""); + const [etcUploading, setEtcUploading] = useState(false); + const etcFileGroupIdRef = useRef(""); + const initialMappedRef = useRef(false); // 초기 데이터가 주어지면 폼 상태로 매핑 (수정 모드에서 사용) useEffect(() => { @@ -348,6 +380,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { //const cklstResultList = initialData.cklstResultList ?? []; const esntlFileList = initialData.esntlFileList ?? []; const etcFileList = initialData.etcFileList ?? []; + // 기본 정보 및 설계자 정보 setBzNm(det.bizNm ?? ""); @@ -377,12 +410,9 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { .filter((f: any) => f && typeof f === "object") .map((f: any) => { const fileName = f.orgnlFileNm ?? f.physFileNm ?? ""; - const ext = - fileName && typeof fileName === "string" && fileName.includes(".") - ? (fileName.split(".").pop() ?? "") - : ""; + const ext = fileName && typeof fileName === "string" && fileName.includes(".") ? fileName.split(".").pop() ?? "" : ""; return { - id: f.atflId, + id: f.atflId , name: fileName, ext, size: f.atflFlieSz ?? 0, @@ -396,14 +426,17 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { // 수정모드시 기타첨부 그룹아이디 적용 (중복 매핑 방지) // - det.atchFileGroupId 또는 det.atflCd 우선, 없으면 etcFileList 첫 항목의 atflCd 사용 - const existingGroup = - det.atchFileGroupId || (Array.isArray(etcFileList) && etcFileList[0]?.atflCd) || ""; + const existingGroup = (det.atchFileGroupId ) || (Array.isArray(etcFileList) && etcFileList[0]?.atflCd) || ""; //console.log("etcFileGroupIdRef.current 초기화1:", existingGroup); if (existingGroup && etcFileGroupIdRef.current !== existingGroup) { etcFileGroupIdRef.current = existingGroup; setEtcFileGroupId((prev) => prev || existingGroup); } - // console.log("etcFileGroupIdRef.current 초기화:", etcFileGroupIdRef.current); + // console.log("etcFileGroupIdRef.current 초기화:", etcFileGroupIdRef.current); + + // 수정모드인 경우 서버가 전달한 업무구분(단일 혹은 콤마로 결합된 복수)을 상태로 복원 + setTaskSeComCd(det.taskSeComCd); + setTaskSeComCdNm(det.taskSeComCdNm ); // 매핑 완료 플래그 설정 — 이후 동일 컴포넌트 인스턴스에서 재매핑 방지 initialMappedRef.current = true; @@ -465,14 +498,14 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { }; } setAmtMap(map); - } + } } catch (err) { console.error("initialData 매핑 실패", err); } }, [initialData]); - /** 기타첨부파일 삭제 핸들러: 백엔드에 DELETE 요청을 보내 소프트삭제 처리 후 상태 갱신 */ - const handleDeleteEtcFile = async (fileId: string, atflId: string) => { + /** 기타첨부파일 삭제 핸들러: 백엔드에 DELETE 요청을 보내 소프트삭제 처리 후 상태 갱신 */ + const handleDeleteEtcFile = async (fileId: string, atflId: string) => { try { if (atflId) { await deleteAttachment(atflId); @@ -493,21 +526,18 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { }); }; - const handleRequiredFileChange = ( - key: RequiredAttachKey, - e: React.ChangeEvent, - ) => { + const handleRequiredFileChange = (key: RequiredAttachKey, e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; uploadRequiredFile(file, key); e.target.value = ""; }; - + const uploadEtcFile = (file: File): Promise => new Promise((resolve, reject) => { let uploadedAtflCd = ""; let uploadedAtflId = ""; - console.log("기타첨부파일 업로드 시작:", file.name, etcFileGroupIdRef.current); + console.log("기타첨부파일 업로드 시작:", file.name ,etcFileGroupIdRef.current); const upload = new Upload(file, { endpoint: `${PROXY_HOST}${ENDPOINT.ATTACH_SERVICE.TUS_UPLOAD}`, chunkSize: 10 * 1024 * 1024, @@ -537,7 +567,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { } }, onSuccess() { - const ext = file.name.includes(".") ? (file.name.split(".").pop() ?? "") : ""; + const ext = file.name.includes(".") ? file.name.split(".").pop() ?? "" : ""; resolve({ id: uploadedAtflId || `${Date.now()}_${Math.random()}`, name: file.name, @@ -597,18 +627,48 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { /* ── 체크리스트 ── */ type CheckResultVal = "Y" | "N" | null; const [checkResults, setCheckResults] = useState>({}); + const [checkComments, setCheckComments] = useState>({}); const cklstRowRefs = useRef>({}); + const [pendingInvalidKey, setPendingInvalidKey] = useState(null); const { data: cklstTree = [] } = useQuery({ queryKey: ["cklstTree", docSeComCd, taskSeComCd], - queryFn: () => cklstMngService.findTree({ docSeComCd, taskSeComCd }), + queryFn: async () => { + console.log("Fetching checklist tree with", { docSeComCd, taskSeComCd }); + // support comma-separated task codes (e.g. "RE,SP") by splitting + const raw = taskSeComCd ?? ""; + const codes = raw + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + + if (codes.length === 0) return [] as CklstTreeNode[]; + if (codes.length === 1) { + return await cklstMngService.findTree({ docSeComCd, taskSeComCd: codes[0] }); + } + + const results = await Promise.all( + codes.map((code) => cklstMngService.findTree({ docSeComCd, taskSeComCd: code })), + ); + const flat = results.flat(); + // dedupe by cklstRvwSn, keep first occurrence + const map = new Map(); + for (const row of flat) { + if (!map.has(row.cklstRvwSn)) map.set(row.cklstRvwSn, row); + } + return Array.from(map.values()); + }, enabled: Boolean(docSeComCd) && Boolean(taskSeComCd), staleTime: Infinity, }); const cklstRows = useMemo(() => { // bchckUseYn 필터 제거: 부모가 있는 자식 노드(bchckUseYn=N)도 트리에 포함 - const useRows = cklstTree; + // cklstTree 내의 피드 taskSeComCd 에 따라서 그룹핑 되도록 해줘 + + const useRows = selectedTaskCode + ? cklstTree.filter((r) => (r as any).taskSeComCd === selectedTaskCode) + : cklstTree; if (useRows.length === 0) return [] as CklstTreeNode[]; // artcl 단위로 분리하지 않고 전체 rows에서 단일 childMap을 구성한다. @@ -642,11 +702,30 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { walk(null); return ordered; - }, [cklstTree]); + }, [cklstTree, selectedTaskCode]); + + // when cklstTree changes, default selectedTaskCode to first available code + useEffect(() => { + const codes = Array.from( + new Set((cklstTree ?? []).map((r) => (r as any).taskSeComCd).filter(Boolean)), + ); + + if (codes.length === 0) { + if (selectedTaskCode !== null) setSelectedTaskCode(null); + return; + } + + // if there's no selection yet, or the current selection is not in the new codes, + // pick the first available code + if (!selectedTaskCode || !codes.includes(selectedTaskCode)) { + setSelectedTaskCode(codes[0]); + } + }, [cklstTree, selectedTaskCode]); const cklstKey = (row: CklstTreeNode, rvwStpComCd?: string) => { const stepCode = rvwStpComCd ?? DEFAULT_RVW_STP_COM_CD; - return `${row.cklstSn}_${row.cklstArtclSn}_${row.cklstRvwSn}_${stepCode}`; + const taskCode = (row as any).taskSeComCd ?? ""; + return `${taskCode}_${row.cklstSn}_${row.cklstArtclSn}_${row.cklstRvwSn}_${stepCode}`; }; useEffect(() => { @@ -664,9 +743,10 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { }); }); - // 행이 없어졌을 때 stale key만 정리 (현재 화면에 노출되는 step 기준) + // 행이 없어졌을 때 stale key만 정리 + // build liveKeys from full cklstTree so inputs for other taskSeComCd are preserved const liveKeys = new Set(); - cklstRows.forEach((row) => { + (cklstTree ?? []).forEach((row) => { visibleReviewStepCodes.forEach((stepCode) => { liveKeys.add(cklstKey(row, stepCode)); }); @@ -701,22 +781,38 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { return changed ? next : prev; }); - }, [cklstRows, visibleReviewStepCodes]); + }, [cklstTree, visibleReviewStepCodes]); + + // when a previously-missing invalid key is set (due to switching task code), + // try to scroll/focus it after the rows have re-rendered + useEffect(() => { + if (!pendingInvalidKey) return; + const el = cklstRowRefs.current[pendingInvalidKey]; + if (el) { + el.scrollIntoView({ behavior: "smooth", block: "center" }); + const firstRadio = el.querySelector(`input[name="chk_${pendingInvalidKey}"]`); + firstRadio?.focus(); + setPendingInvalidKey(null); + } + // otherwise, wait for cklstRows update (effect will re-run when pendingInvalidKey remains set) + }, [pendingInvalidKey, cklstRows]); // 수정 모드에서 서버의 체크리스트 결과값을 폼 상태로 반영 useEffect(() => { - if (!initialData || cklstRows.length === 0) return; + if (!initialData || cklstRows.length === 0) return; const results = normalizeChecklistResultRows(initialData); if (!Array.isArray(results) || results.length === 0) return; const lookup = new Map(); + const commentLookup = new Map(); const stepCodesInResult = new Set(); for (const r of results) { const stepCode = (r.rvwStpComCd ?? DEFAULT_RVW_STP_COM_CD) as ReviewStepCode; if (!REVIEW_STEP_CODES.includes(stepCode)) continue; stepCodesInResult.add(stepCode); - const key = `${r.cklstSn}_${r.cklstArtclSn ?? ""}_${r.cklstRvwSn}_${stepCode}`; + const key = `${r.taskSeComCd ?? ""}_${r.cklstSn}_${r.cklstArtclSn ?? ""}_${r.cklstRvwSn}_${stepCode}`; if (r.rvwRsltAnmlyYn === "Y" || r.rvwRsltAnmlyYn === "N") lookup.set(key, r.rvwRsltAnmlyYn); + if (r.picRvwCn) commentLookup.set(key, String(r.picRvwCn)); } setCheckResults((prev) => { @@ -730,7 +826,8 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { targetStepCodes.forEach((stepCode) => { const k = cklstKey(row, stepCode); const v = lookup.get(k); - if (v && next[k] !== v) { + // only apply server-provided value when we don't already have a user-entered value + if (v && (next[k] === null || next[k] === undefined)) { next[k] = v as CheckResultVal; changed = true; } @@ -738,14 +835,29 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { } return changed ? next : prev; }); + // set comment state separately to avoid rerendering conflicts + setCheckComments((prev) => { + const next: Record = { ...prev }; + let changed = false; + for (const [k, v] of commentLookup.entries()) { + // only set comment if none exists yet (preserve user edits) + if (!next[k]) { + next[k] = v; + changed = true; + } + } + return changed ? next : prev; + }); }, [initialData, cklstRows, visibleReviewStepCodes]); const cklstStats = useMemo(() => { if (!selectedReviewStep) { return { total: 0, completed: 0, percent: 0 }; } - const total = cklstRows.length; - const completed = cklstRows.reduce((acc, row) => { + // only include rows that are marked for checkbox use (bchckUseYn === "Y") + const relevantRows = cklstRows.filter((row) => row.bchckUseYn === "Y"); + const total = relevantRows.length; + const completed = relevantRows.reduce((acc, row) => { const val = checkResults[cklstKey(row, selectedReviewStep)] ?? ""; return acc + (val ? 1 : 0); }, 0); @@ -753,54 +865,42 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { return { total, completed, percent }; }, [cklstRows, checkResults, selectedReviewStep]); + const toCklstSaveDtoList = () => { const persistedStepCodeSet = new Set(); normalizeChecklistResultRows(initialData).forEach((r: any) => { const code = r?.rvwStpComCd as ReviewStepCode | undefined; if (code && REVIEW_STEP_CODES.includes(code)) persistedStepCodeSet.add(code); }); + // Always persist currently visible review steps as well visibleReviewStepCodes.forEach((code) => persistedStepCodeSet.add(code)); - return Array.from(persistedStepCodeSet).flatMap((stepCode) => - cklstRows.map((row) => ({ - cklstSn: row.cklstSn, - cklstArtclSn: row.cklstArtclSn, - cklstRvwSn: row.cklstRvwSn, - rvwStpComCd: stepCode, - selected: checkResults[cklstKey(row, stepCode)] ?? null, - })), + // only persist rows that are enabled for checkbox (`bchckUseYn === "Y") + // only persist rows that are enabled for checkbox (`bchckUseYn === "Y") + // Use full cklstTree so items not currently visible (other task codes) are included + const relevantRows = (cklstTree ?? []).filter((row) => row.bchckUseYn === "Y"); + + // Ensure we generate DTOs grouped by all known task codes (TASK_CODE_LABELS keys) + const allTaskCodes = Object.keys(TASK_CODE_LABELS); + + return allTaskCodes.flatMap((taskCode) => + Array.from(persistedStepCodeSet).flatMap((rvwStpComCd) => + relevantRows + .filter((row) => ((row as any).taskSeComCd ?? "") === taskCode) + .map((row) => ({ + taskSeComCd: taskCode, + cklstSn: row.cklstSn, + cklstArtclSn: row.cklstArtclSn, + cklstRvwSn: row.cklstRvwSn, + rvwStpComCd: rvwStpComCd, + selected: checkResults[cklstKey(row, rvwStpComCd)] ?? null, + picRvwCn: checkComments[cklstKey(row, rvwStpComCd)] ?? null, + })), + ), ); }; - // 맵 형식으로 저장할 경우(업서트 처리용) - 키는 상세 코드(EX: LOCAL.지자체) 포맷 - const REVIEW_STEP_FULL: Record = { - LOCAL: "LOCAL.지자체", - ENV: "ENV.환경청", - KECO: "KECO.환경공단", - }; - - const toCklstSaveDtoMap = () => { - const persistedStepCodeSet = new Set(); - normalizeChecklistResultRows(initialData).forEach((r: any) => { - const code = r?.rvwStpComCd as ReviewStepCode | undefined; - if (code && REVIEW_STEP_CODES.includes(code)) persistedStepCodeSet.add(code); - }); - visibleReviewStepCodes.forEach((code) => persistedStepCodeSet.add(code)); - - const result: Record = {}; - Array.from(persistedStepCodeSet).forEach((stepCode) => { - const fullKey = REVIEW_STEP_FULL[stepCode]; - result[fullKey] = cklstRows.map((row) => ({ - cklstSn: row.cklstSn, - cklstArtclSn: row.cklstArtclSn, - cklstRvwSn: row.cklstRvwSn, - rvwStpComCd: fullKey, - selected: checkResults[cklstKey(row, stepCode)] ?? null, - })); - }); - - return result; - }; + // removed map-shaped payload generation — server now expects `cklstResultList` only const toEsntlAtchFileSaveDtoList = () => Object.values(requiredFiles) @@ -815,7 +915,11 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { const [bzScaleMap, setBzScaleMap] = useState>({}); /* ── 공종구분 트리 로드 ── */ - const { data: cnstpTree = [] } = useCnstpSeMngTree(); + const { data: cnstpTree = [] } = useQuery({ + queryKey: ["cnstpTree"], + queryFn: () => cnstpSeMngService.findTree(), + staleTime: Infinity, + }); /* ── 공종구분 리프 노드 집합 ── */ const cnstpLeafSet = useMemo(() => { @@ -825,9 +929,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { const subtotalScale = (node: CnstpSeMngTreeDto, col: keyof BzScaleRow): number => cnstpTree - .filter( - (n) => cnstpLeafSet.has(n.cnstpSeCd) && n.treePath.split("|").includes(node.cnstpSeCd), - ) + .filter((n) => cnstpLeafSet.has(n.cnstpSeCd) && n.treePath.split("|").includes(node.cnstpSeCd)) .reduce((sum, n) => sum + num((bzScaleMap[n.cnstpSeCd] ?? emptyScale())[col]), 0); const scaleNodeVal = (node: CnstpSeMngTreeDto, col: keyof BzScaleRow): number => @@ -865,7 +967,11 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { const [amtMap, setAmtMap] = useState>({}); /* ── 회계구분 트리 로드 ── */ - const { data: acntgTree = [] } = useAcntgSeMngTree(); + const { data: acntgTree = [] } = useQuery({ + queryKey: ["acntgTree"], + queryFn: () => acntgSeMngService.findTree(), + staleTime: Infinity, // 코드 데이터 — 앱 수명 동안 재요청 불필요 + }); /* ── 리프 노드 집합 (자식이 없는 노드 = 직접 입력 가능) ── */ const leafSet = useMemo(() => { @@ -939,6 +1045,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { /* ── 검증 ── */ function fnValidation(mode: "SAVE" | "PRESENTN") { + if (!bzNm.trim()) { alert("사업명을 입력하세요."); return false; @@ -955,20 +1062,23 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { alert("개인정보 제공에 동의하셔야 제출이 가능합니다."); return false; } - + + if (!editableReviewStep) { alert("현재 사용자 권한/진행상태 조합에서 입력 가능한 체크리스트 단계가 없습니다."); return false; } // 현재 입력 단계의 체크리스트 미선택 항목이 있으면 진행 불가 + /* const hasUncheckedChecklist = cklstRows.some( (row) => !checkResults[cklstKey(row, editableReviewStep)], ); if (hasUncheckedChecklist) { alert("체크리스트를 모두 체크하세요."); return false; - } + } + */ // 저장 시: 필수 첨부파일(esntlAtchYn=Y) 업로드 여부 검증 if (mode === "SAVE") { const missingRequiredFile = requiredAttachmentDefs.find((item) => { @@ -981,19 +1091,32 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { return false; } - // 저장 시: 사용 항목(Y)은 검토 결과가 반드시 예(Y)여야 함 - const invalidYesRow = cklstRows.find( - (row) => row.bchckUseYn === "Y" && checkResults[cklstKey(row, editableReviewStep)] !== "Y", + // 저장 시: bchckUseYn =="Y" 인 경우, 화면에 표시되는 모든 review step에 대해 + // 검토 결과가 선택되어 있어야 함 (visibleReviewStepCodes 기준) + // check across all checklist items (not only currently visible rows) + const allRelevant = (cklstTree ?? []).filter((row) => row.bchckUseYn === "Y"); + const missingRow = allRelevant.find((row) => + visibleReviewStepCodes.some((step) => !checkResults[cklstKey(row, step)]), ); - if (invalidYesRow) { - const invalidKey = cklstKey(invalidYesRow, editableReviewStep); + if (missingRow) { + const missingStep = visibleReviewStepCodes.find((step) => !checkResults[cklstKey(missingRow, step)])!; + const invalidKey = cklstKey(missingRow, missingStep); + console.warn("검토 결과 미선택 항목 발견:", invalidKey, missingRow, missingStep); + const invalidRowEl = cklstRowRefs.current[invalidKey]; - invalidRowEl?.scrollIntoView({ behavior: "smooth", block: "center" }); - const yesRadio = invalidRowEl?.querySelector( - `input[name="chk_${invalidKey}"][value="Y"]`, - ); - yesRadio?.focus(); - alert(`${invalidYesRow.rvwCn} 검토내용은 예(Y)여야 합니다.`); + if (invalidRowEl) { + invalidRowEl.scrollIntoView({ behavior: "smooth", block: "center" }); + const firstRadio = invalidRowEl.querySelector(`input[name="chk_${invalidKey}"]`); + firstRadio?.focus(); + } else { + // row not visible: switch header filter to the missing row's task code, + // then focus after render via pendingInvalidKey + const taskCode = (missingRow as any).taskSeComCd ?? null; + if (taskCode) setSelectedTaskCode(taskCode); + setPendingInvalidKey(invalidKey); + } + + alert(`체크리스트 항목 "${missingRow.rvwCn}"의 검토결과를 선택하세요.`); return false; } } @@ -1001,7 +1124,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { return true; } - const toInstlSncnMngPayload = (prgrsSttsComCd: "SV" | "ST"): InstlSncnMng => ({ + const toInstlSncnMngPayload = (prgrsSttsComCd: "SV" | "ST"| "ER"): InstlSncnMng => ({ instlSncnDocNo: docId ?? "", // 수정 시 기존 문서번호 유지, 신규 저장 시 빈값 admdstCd: "", sidoNm: "", @@ -1037,15 +1160,13 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { async function handleSave() { if (!fnValidation("SAVE")) return; - // 저장 시에는 하위 호환성을 위해 기존 배열 필드도 함께 전송하되, - // 추가로 서버에서 upsert 처리할 수 있도록 `cklstResultMap` 형태로도 전송합니다. - const payload: InstlSncnApplySaveRequestDto & { cklstResultMap?: Record } = { + // 저장 시에는 `cklstResultList`만 전송합니다 (map 필드 제거) + const payload: InstlSncnApplySaveRequestDto = { instlSncnMng: toInstlSncnMngPayload("SV"), // 저장 시 진행상태는 SV (임시저장) bzScaleList: toBzScaleSaveDtoList(), bizCstList: toBizCstSaveDtoList(), cklstResultList: toCklstSaveDtoList(), esntlAtchFileList: toEsntlAtchFileSaveDtoList(), - cklstResultMap: toCklstSaveDtoMap(), }; try { @@ -1061,16 +1182,28 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { } } - function handleSubmit() { - if (!fnValidation("PRESENTN")) return; - const payload = { - instlSncnMng: toInstlSncnMngPayload("ST"), + async function handleSubmit() { + if (!fnValidation("SAVE")) return; + + const payload: InstlSncnApplySaveRequestDto = { + instlSncnMng: toInstlSncnMngPayload("SV"), // 저장 시 진행상태는 SV (임시저장) bzScaleList: toBzScaleSaveDtoList(), bizCstList: toBizCstSaveDtoList(), - cklstRvwRsltList: toCklstSaveDtoList(), - }; - void payload; - alert("제출 API 연동 후 progrsSttus=ST 전송"); + cklstResultList: toCklstSaveDtoList(), + esntlAtchFileList: toEsntlAtchFileSaveDtoList(), + }; + + try { + const issuedDocNo = await InstlSncnApi.saveApply(payload); + alert(`제출 되었습니다. 문서번호: ${issuedDocNo}`); + } catch (error) { + console.error("저장 API 호출 실패", error); + const serverMessage = + axios.isAxiosError(error) && error.response?.data?.message + ? (error.response.data as { message: string }).message + : "저장 중 오류가 발생했습니다."; + alert(serverMessage); + } } /* ---------------------------------------------------------------- */ @@ -1128,7 +1261,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { - + * 사업기간 @@ -1150,7 +1283,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { - + * 사업내용 @@ -1163,12 +1296,12 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { - 구분 + 구분
{taskSeComCdNm}
- 신청 지자체 + 신청 지자체 {userInfo?.enoNm ?? "\u00A0"} @@ -1176,9 +1309,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { - - *사업구분 - + *사업구분
{[ @@ -1210,47 +1341,31 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { - - + + - + - + - - + + - - + + - + - - + +
- 신청자 - 이름신청자이름 - - {aplcntNm || userInfo?.userName || "\u00A0"} - + {aplcntNm || userInfo?.userName || "\u00A0"} 전화번호전화번호 - - {aplcntMblTelno || userInfo?.mphn || userInfo?.telno || "\u00A0"} - + {aplcntMblTelno || userInfo?.mphn || userInfo?.telno || "\u00A0"}
부서부서 - - {aplcntOgdpNm || userInfo?.instSeNm || "\u00A0"} - + {aplcntOgdpNm || userInfo?.instSeNm || "\u00A0"} 이메일이메일 - - {aplcntEmlAddr || userInfo?.email || "\u00A0"} - + {aplcntEmlAddr || userInfo?.email || "\u00A0"}
- 설계자 - 이름설계자이름 setDsgnrNm(e.target.value)} /> 전화번호전화번호 setDsgnrPsitn(e.target.value)} /> 이메일이메일

[ 개인정보 수집항목 ]

- * 필수항목 : 소속기관, 소속부서, 담당자, 직급, 사무실 번호, FAX 번호 -
- ․ 선택항목 : 휴대폰 번호, 이메일 주소 -
- ․ 기타항목 : IP Address, 쿠키, 시스템 접속 및 이용 기록 -
※ 시스템 이용과정에서 기타 항목의 정보들이 자동으로 생성되어 수집될 수 있습니다. + * 필수항목 : 소속기관, 소속부서, 담당자, 직급, 사무실 번호, FAX 번호
+ ․ 선택항목 : 휴대폰 번호, 이메일 주소
+ ․ 기타항목 : IP Address, 쿠키, 시스템 접속 및 이용 기록
+ ※ 시스템 이용과정에서 기타 항목의 정보들이 자동으로 생성되어 수집될 수 있습니다.

[ 개인정보 이용목적 ]

- ․ 국가하수도정보시스템 웹서비스 이용에 따른 본인 확인 절차에 이용 -
- ․ 국가하수도정보시스템 사용에 필요한 권한 부여 -
․ 사용자별 맞춤 컨텐츠 (처리시설의 운영 데이터 조회, 수질현황 데이터 조회, 예산 편성 - / 예산편성 등) 제공에 이용 + ․ 국가하수도정보시스템 웹서비스 이용에 따른 본인 확인 절차에 이용
+ ․ 국가하수도정보시스템 사용에 필요한 권한 부여
+ ․ 사용자별 맞춤 컨텐츠 (처리시설의 운영 데이터 조회, 수질현황 데이터 조회, 예산 편성 / 예산편성 등) 제공에 이용
● 개인정보의 보유 및 이용기간
- ․ 수집된 고객의 개인정보는 2년 동안 보유하며 그 이후에는 문서를 파쇄합니다. -
․ 정보제공자가 개인정보 파쇄를 요청할 경우 즉시 파쇄합니다. + ․ 수집된 고객의 개인정보는 2년 동안 보유하며 그 이후에는 문서를 파쇄합니다.
+ ․ 정보제공자가 개인정보 파쇄를 요청할 경우 즉시 파쇄합니다.

● 개인정보 제공 거부에 따른 제한사항
- ․ 귀하는 개인정보 제공 동의를 거부할 권리가 있으나, 동의 거부 시 국가하수도정보시스템 이용 - 서비스에 제한이 있을 수 있습니다. + ․ 귀하는 개인정보 제공 동의를 거부할 권리가 있으나, 동의 거부 시 국가하수도정보시스템 이용 서비스에 제한이 있을 수 있습니다.
@@ -1498,10 +1608,27 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {

체크리스트

- +
+ {Array.from(new Set((cklstTree ?? []).map((r) => (r as any).taskSeComCd).filter(Boolean))).map( + (code) => { + const name = TASK_CODE_LABELS[code] ?? code; + const active = selectedTaskCode === code; + return ( + + ); + }, + )} +
{`(${cklstStats.completed}/${cklstStats.total})`}
+
{visibleReviewStepCodes.length === 0 ? ( @@ -1531,9 +1658,8 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { )}
- 체크리스트 진행상황 [{selectedReviewStep}] — 총합: {cklstStats.total} · - 완료: {cklstStats.completed} · 진행률:{" "} - {cklstStats.percent}% + 체크리스트 진행상황 [{selectedReviewStep}] — 총합: {cklstStats.total} · 완료: {" "} + {cklstStats.completed} · 진행률: {cklstStats.percent}%
@@ -1543,7 +1669,12 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { 검토항목 - + + {selectedReviewStep !== "LOCAL" ? ( + + ) : null} {cklstRows.length === 0 ? ( - @@ -1593,7 +1724,8 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { cklstRows.map((row) => { const key = cklstKey(row, selectedReviewStep); const rvwPaddingLeft = `${Math.max(0, row.treeLevel - 1) * 16 + 8}px`; - + //bchckUseYn 의 값이 "Y"인 항목은 라디오 박스가 보이고 없으면 안보이게 처리 + const showRadios = row.bchckUseYn === "Y"; return ( - - + + + ) : ( + <> + + + + )} + + {showRadios && selectedReviewStep !== "LOCAL" ? ( +
주요 검토내용검토 결과(-)검토 결과(✓) + 기술검토 의견 +
@@ -1585,7 +1716,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
+ 조회된 체크리스트 항목이 없습니다.
{row.cklstArtclNm} + {row.treeLevel > 1 ? "└ " : ""} {row.rvwCn} - + + + + +