diff --git a/apps/st/src/components/css/DataGrid.css b/apps/st/src/components/css/DataGrid.css index 2d18921f9..2d8eab48c 100644 --- a/apps/st/src/components/css/DataGrid.css +++ b/apps/st/src/components/css/DataGrid.css @@ -32,6 +32,9 @@ .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-total { @apply !bg-[#e3eaf2]; } + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-selected-row { + @apply !bg-[#dbeafe]; + } .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-rowtype-biz-sum { @apply !bg-[#ffe9cf]; } diff --git a/apps/st/src/components/module/SplitDataGrid.tsx b/apps/st/src/components/module/SplitDataGrid.tsx index f275b83a2..5dd359ded 100644 --- a/apps/st/src/components/module/SplitDataGrid.tsx +++ b/apps/st/src/components/module/SplitDataGrid.tsx @@ -1,6 +1,7 @@ -import CompactDataGrid from "./CompactDataGrid"; import type { SplitDataGridProps } from "../type/DataGrid.type"; +import CompactDataGrid from "./CompactDataGrid"; + const SplitDataGrid = ({ leftColumnInfo, rightColumnInfo, @@ -11,6 +12,8 @@ const SplitDataGrid = ({ rightRowData, setLeftRowData, setRightRowData, + leftOnRowClick, + leftCellStyleCallback, enableSort = false, }: SplitDataGridProps) => { return ( @@ -18,10 +21,12 @@ const SplitDataGrid = ({ { + if (statusInfo?.laeSbmsnSttsCd) { + if (['2', '3'].includes(statusInfo.laeSbmsnSttsCd)) { + return false; + } + return periodEnterable; + } + return periodEnterable; +}; + +function computeTabToolbarSnapshot( + statusInfo: SewerageStatusInfo | null, + hasRowData: boolean, + treatAsSearched: boolean, + searchContext: SearchFormValues | null, + periodEnterable: boolean, + isAdminInfo: boolean, +): TabToolbarSnapshot { + const gugunOk = buildGugunOkForToolbar(searchContext); + const enterable = computeTabEnterable(statusInfo, periodEnterable); + const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; + const showEnterableToolbarButtons = enterable && gugunOk; + const showActionToolbarCard = + (hasRowData || treatAsSearched) && + (showSaveInToolbar || + (statusInfo?.laeSbmsnSttsCd === '2' && + (isAdminInfo || (enterable && gugunOk)))); + return { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard }; +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function waitForImrJobCompleted( + jobId: string, + getStatus: (id: string) => Promise, + options?: { + pollIntervalMs?: number; + maxAttempts?: number; + }, +): Promise { + const pollIntervalMs = options?.pollIntervalMs ?? 2000; + const maxAttempts = options?.maxAttempts ?? 150; + for (let i = 0; i < maxAttempts; i++) { + const res = await getStatus(jobId); + if (res.status === 'COMPLETED' && Number(res.job_prog) === 100) return; + if (res.status === 'FAILED' || res.status === 'ERROR' || res.status === 'CANCELLED') { + throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); + } + await sleep(pollIntervalMs); + } + throw new Error('IMR 검증 완료 대기 시간이 초과되었습니다.'); +} // 하수도 통계 관리 > 06. 공공하수처리시설 const PublicSewageFacilityInp = () => { const { setMessage: alert } = useAlertStore((state) => state); + const { confirm } = useConfirm(); + const { isAdminInfo } = useAdminStore(); const [isLoading, setIsLoading] = useState(false); const [hasSearched, setHasSearched] = useState(false); + const [isNotApplicable, setIsNotApplicable] = useState(false); + const [showIsNotApplicable, setShowIsNotApplicable] = useState(false); + const [searchValues, setSearchValues] = useState(null); + const [lastSearchedValues, setLastSearchedValues] = useState(null); + const [tabStatusInfoMap, setTabStatusInfoMap] = useState>(new Map()); + const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); + const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState>(() => new Map()); + const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); + const [periodCheckList, setPeriodCheckList] = useState([]); + const [showDailog, setShowDailog] = useState(false); + const [selectedMainFacilitySn, setSelectedMainFacilitySn] = useState(null); + + const periodEnterableRef = useRef(true); + const searchDoneRef = useRef(false); const submitFunc = () => { - console.log('submit'); - // 제출 로직 + void (async () => { + if (activeTab !== 'ST00060001') { + alert('ST00060002 제출 API 연동 전입니다.'); + return; + } + if (!lastSearchedValues || !searchParamRef.current) { + alert('먼저 조회를 진행해주세요.'); + return; + } + const currentData = rowDataMap.get('ST00060001') ?? []; + if (!currentData || currentData.length === 0) { + alert('제출할 데이터가 없습니다.'); + return; + } + const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList); + if (verificationBlocked) { + alert(verificationBlocked); + return; + } + if (await confirm('제출 하시겠습니까?')) { + setShowDailog(true); + } + })(); + }; + + const buildFacilitySaveList = (currentData: Record[]): PublicSewageFacilitySaveItem[] => { + const toNumber = (v: unknown): number => { + if (v === null || v === undefined) return 0; + if (v === '') return 0; + const num = typeof v === 'string' ? parseFloat(v.replace(/,/g, '')) : (v as number); + return Number.isFinite(num) ? num : 0; + }; + + const toNullableString = (v: unknown): string | null => { + if (v === null || v === undefined) return null; + const s = typeof v === 'string' ? v : String(v); + return s.trim() === '' ? null : s; + }; + + return currentData.map((d) => ({ + admdstCd: String(d.admdstCd ?? ''), + admdstNm: String(d.admdstNm ?? ''), + swtrmtFcltCd: String(d.swtrmtFcltCd ?? ''), + swtrmtFcltNm: String(d.swtrmtFcltNm ?? ''), + fcltSn: String(d.fcltSn ?? ''), + fcltPstnCn: d.fcltPstnCn === '' ? null : toNullableString(d.fcltPstnCn), + inflwSwagePhysQnt: toNumber(d.inflwSwagePhysQnt), + inflwSwageBoilQnt: toNumber(d.inflwSwageBoilQnt), + inflwSwageAltdQnt: toNumber(d.inflwSwageAltdQnt), + prcsPhysCpct: toNumber(d.prcsPhysCpct), + prcsBoilCpct: toNumber(d.prcsBoilCpct), + prcsAltdCpct: toNumber(d.prcsAltdCpct), + prxyCtrtCn: d.prxyCtrtCn === '' ? null : toNullableString(d.prxyCtrtCn), + sftyAcdntEvntcsCnt: toNumber(d.sftyAcdntEvntcsCnt), + rmrk: toNullableString(d.rmrk), + })); + }; + + const getNoApplicableForRequest = (): boolean => { + const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; + const searchContextForToolbar = lastSearchedValues ?? searchValues; + const gugunOk = buildGugunOkForToolbar(searchContextForToolbar); + const enterable = computeTabEnterable(activeTabStatus, inputPeriodEnterable); + const showEnterableToolbarButtons = enterable && gugunOk; + const noApplicableVisible = showEnterableToolbarButtons && showIsNotApplicable; + return noApplicableVisible ? Boolean(isNotApplicable) : false; + }; + + const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { + const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList); + if (verificationBlocked) { + alert(verificationBlocked); + return; + } + if (activeTab !== 'ST00060001') { + alert('ST00060002 제출 API 연동 전입니다.'); + return; + } + if (!lastSearchedValues || !searchParamRef.current) { + alert('먼저 조회를 진행해주세요.'); + return; + } + const currentData = rowDataMap.get('ST00060001') ?? []; + if (!currentData || currentData.length === 0) { + alert('제출할 데이터가 없습니다.'); + return; + } + + const userInfo = await getUserInfo(); + const submitList = buildFacilitySaveList(currentData); + const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; + const statsCdForTab = tabStatsCdMap.get(activeTab) ?? activeTab; + const requestData = { + statsYr: lastSearchedValues.year ?? '', + ctpvNm: lastSearchedValues.sido ?? '', + sggNm: lastSearchedValues.gugun === 'ALL' ? '' : lastSearchedValues.gugun ?? '', + sewerBzmnCd: lastSearchedValues.bsnmCd ?? '', + userId: userInfo.userUnqId, + noApplicable: getNoApplicableForRequest(), + laePicNm: submitInfo.laePicNm, + laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, + laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, + laePicTelNo: submitInfo.laePicTelNo, + laeIdfrNm: submitInfo.laeIdfrNm, + laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, + laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, + laeIdfrTelNo: submitInfo.laeIdfrTelNo, + submitList, + }; + + try { + setIsLoading(true); + const response = await statisticsInputPublicSewageFacilityApiService.submitPublicSewageFacility(requestData); + if (response.status === 'success') { + if (response.data?.finalStatus === '5') { + const imrParam = { + user_id: userInfo.userUnqId, + sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? '', + stats_cd: statsCdForTab, + stats_yr: lastSearchedValues.year ?? '', + lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === '4' ? '4' : '2', + gen_verify_stts_cd: response.data?.nvpassed ? '1' : '2', + }; + try { + const imrResponse = await statisticsApi.callImrService(imrParam); + if (imrResponse.job_id) { + await waitForImrJobCompleted( + imrResponse.job_id, + (id) => statisticsApi.getImrStatus(id), + ); + } + alert('제출되었습니다.'); + } catch { + alert('제출은 완료되었으나 IMR 검증 처리 중 오류가 발생했습니다.'); + } + } else { + alert(response.message || '제출 처리 결과를 확인해주세요.'); + } + await loadPublicSewageFacilityActiveTabData( + 'ST00060001', + searchParamRef.current, + lastSearchedValues, + ); + } else { + alert(response.message || '데이터 제출 중 오류가 발생했습니다.'); + } + } finally { + setIsLoading(false); + } }; const saveFunc = () => { - console.log('save'); - // 저장 로직 - }; + void (async () => { + if (activeTab !== 'ST00060001') { + alert('ST00060002 저장 API 연동 전입니다.'); + return; + } - const getOperateDataFunc = () => { - console.log('get operate data'); - // 운영 데이터 가져오기 + if (!lastSearchedValues || !searchParamRef.current) { + alert('먼저 조회를 진행해주세요.'); + return; + } + + const currentData = rowDataMap.get('ST00060001') ?? []; + if (!currentData || currentData.length === 0) { + alert('저장할 데이터가 없습니다.'); + return; + } + + if (!(await confirm("저장 하시겠습니까?"))) return; + + const userInfo = await getUserInfo(); + const saveList = buildFacilitySaveList(currentData); + + const param: PublicSewageFacilitySaveRequest = { + statsYr: lastSearchedValues.year ?? '', + ctpvNm: lastSearchedValues.sido ?? '', + sggNm: lastSearchedValues.gugun === 'ALL' ? '' : lastSearchedValues.gugun ?? '', + sewerBzmnCd: lastSearchedValues.bsnmCd ?? '', + userId: userInfo.userUnqId, + superAdmin: isAdminInfo ? 'superAdmin' : '', + noApplicable: getNoApplicableForRequest(), + saveList, + }; + + try { + setIsLoading(true); + const response = await statisticsInputPublicSewageFacilityApiService.savePublicSewageFacility(param); + if (response.status === 'success') { + alert('저장되었습니다.'); + await loadPublicSewageFacilityActiveTabData( + 'ST00060001', + searchParamRef.current, + lastSearchedValues, + ); + } else { + alert(response.message || '데이터 저장 중 오류가 발생했습니다.'); + } + } finally { + setIsLoading(false); + } + })(); }; const updateOperateDataButtonFunc = () => { @@ -51,6 +348,29 @@ const PublicSewageFacilityInp = () => { const firstTabKey = PublicSewageFactInpColumnList[0]?.tabKey ?? 'ST00060001'; const searchParamRef = useRef(null); + const commitToolbarSnapshotForLoadedTab = ( + tabKey: string, + statusInfo: SewerageStatusInfo | null, + hasRowData: boolean, + searchContext: SearchFormValues | null, + ) => { + setToolbarSnapshotByTab((prev) => { + const next = new Map(prev); + next.set( + tabKey, + computeTabToolbarSnapshot( + statusInfo, + hasRowData, + searchDoneRef.current, + searchContext, + periodEnterableRef.current, + isAdminInfo, + ), + ); + return next; + }); + }; + const buildPublicSewageFacilityParams = (values: SearchFormValues): PublicSewageFacilityRequestParams => ({ ctpvNm: values.sido, sggNm: values.gugun === 'ALL' ? '' : values.gugun, @@ -71,6 +391,7 @@ const PublicSewageFacilityInp = () => { const loadPublicSewageFacilityActiveTabData = async ( tabKey: string, param: PublicSewageFacilityRequestParams, + searchContext: SearchFormValues | null, ): Promise => { try { if (tabKey === 'ST00060001') { @@ -79,6 +400,9 @@ const PublicSewageFacilityInp = () => { alert(response.message || '데이터 조회 중 오류가 발생했습니다.'); return false; } + setSelectedMainFacilitySn(null); + const statusInfo = response.data.statusInfo ?? null; + const statsCd = response.data.commonInfo.statsCd || tabKey; const commonInfo = response.data.commonInfo; const list = response.data.publicSewageFacilityList ?? []; const convertedData = list.map((item) => ({ @@ -86,11 +410,30 @@ const PublicSewageFacilityInp = () => { ...item, rmrk: item.rmrk ?? null, })); - setRowDataMap((prev) => { + + if (list.length === 0) { + setShowIsNotApplicable(true); + } else { + setShowIsNotApplicable(false); + } + + const gridRows = toGridRows(convertedData); + setTabStatusInfoMap((prev) => { const next = new Map(prev); - next.set('ST00060001', toGridRows(convertedData)); + next.set(tabKey, statusInfo); return next; }); + setTabStatsCdMap((prev) => { + const next = new Map(prev); + next.set(tabKey, statsCd); + return next; + }); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set('ST00060001', gridRows); + return next; + }); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); return true; } @@ -100,6 +443,9 @@ const PublicSewageFacilityInp = () => { alert(response.message || '데이터 조회 중 오류가 발생했습니다.'); return false; } + setSelectedMainFacilitySn(null); + const statusInfo = response.data.statusInfo ?? null; + const statsCd = response.data.commonInfo.statsCd || tabKey; const list = response.data.mainFacilityList ?? []; const leftRows = list.map((item) => ({ swtrmtFcltNm: item.swtrmtFcltNm, @@ -115,12 +461,24 @@ const PublicSewageFacilityInp = () => { sggNm: item.sggNm ?? '', admdstNm: item.admdstNm, })); + const leftGridRows = toGridRows(leftRows); + setTabStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(tabKey, statusInfo); + return next; + }); + setTabStatsCdMap((prev) => { + const next = new Map(prev); + next.set(tabKey, statsCd); + return next; + }); setRowDataMap((prev) => { const next = new Map(prev); - next.set('MainFacility', toGridRows(leftRows)); + next.set('MainFacility', leftGridRows); next.set('MainFacilityTarget', []); return next; }); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, leftGridRows.length > 0, searchContext); return true; } @@ -137,22 +495,44 @@ const PublicSewageFacilityInp = () => { return; } try { - setIsLoading(true); - const param = buildPublicSewageFacilityParams(values); + const periodResponse = await statisticsInputSewSupplyRateApi.checkInputPeriod(values.year || ''); + if (periodResponse.status !== 'success') { + alert(periodResponse.message); + return; + } + const periods = Array.isArray(periodResponse.data) ? periodResponse.data : []; + setPeriodCheckList(periods); + const periodOk = isInputPeriodCurrent(periods); + periodEnterableRef.current = periodOk; + setInputPeriodEnterable(periodOk); + const yearChangedFromLastSearch = - hasSearched && - (searchParamRef.current?.statsYr ?? '') !== (param.statsYr ?? ''); + lastSearchedValues != null && + (lastSearchedValues.year ?? '') !== (values.year ?? ''); + + // 조회 시 이전 선택 로우 강조 초기화 + setSelectedMainFacilitySn(null); if (yearChangedFromLastSearch) { setRowDataMap(new Map()); + setTabStatusInfoMap(new Map()); + setTabStatsCdMap(new Map()); + setToolbarSnapshotByTab(new Map()); activeTabRef.current = firstTabKey; setActiveTab(firstTabKey); } - searchParamRef.current = param; + setLastSearchedValues(values); + searchDoneRef.current = true; setHasSearched(true); + + setIsLoading(true); + const param = buildPublicSewageFacilityParams(values); + searchParamRef.current = param; const tabKeyToLoad = yearChangedFromLastSearch ? firstTabKey : activeTabRef.current; - await loadPublicSewageFacilityActiveTabData(tabKeyToLoad, param); + await loadPublicSewageFacilityActiveTabData(tabKeyToLoad, param, values); + } catch { + alert('데이터 조회 중 오류가 발생했습니다.'); } finally { setIsLoading(false); } @@ -165,12 +545,76 @@ const PublicSewageFacilityInp = () => { if (!hasSearched || tabRowDataExists(tabKey) || !searchParamRef.current) return; try { setIsLoading(true); - await loadPublicSewageFacilityActiveTabData(tabKey, searchParamRef.current); + await loadPublicSewageFacilityActiveTabData(tabKey, searchParamRef.current, lastSearchedValues); } finally { setIsLoading(false); } }; + const handleReset = () => { + setIsLoading(false); + setHasSearched(false); + setIsNotApplicable(false); + setShowIsNotApplicable(false); + + setRowDataMap(new Map()); + setTabStatusInfoMap(new Map()); + setTabStatsCdMap(new Map()); + setToolbarSnapshotByTab(new Map()); + + setInputPeriodEnterable(true); + periodEnterableRef.current = true; + setPeriodCheckList([]); + + searchParamRef.current = null; + searchDoneRef.current = false; + + activeTabRef.current = firstTabKey; + setActiveTab(firstTabKey); + setLastSearchedValues(null); + setSelectedMainFacilitySn(null); + }; + + const cnacelSubmitFunc = async () => { + try { + if (await confirm('제출된 내용을 취소하시겠습니까?')) { + const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList); + if (verificationBlocked) { + alert(verificationBlocked); + return; + } + + const userInfo = await getUserInfo(); + const statsCdForCancel = tabStatsCdMap.get(activeTab) ?? activeTab; + const requestData: cancelSubmitRequest = { + sewerBzmnCd: lastSearchedValues?.bsnmCd ?? '', + ctpvNm: lastSearchedValues?.sido ?? '', + sggNm: lastSearchedValues?.gugun ?? '', + statsYr: lastSearchedValues?.year ?? '', + mdfrId: userInfo.userUnqId, + statsCd: statsCdForCancel, + }; + + const response = await statisticsApi.cancelSubmit(requestData); + + if (response.status === 'success') { + if (searchParamRef.current) { + await loadPublicSewageFacilityActiveTabData( + activeTab, + searchParamRef.current, + lastSearchedValues, + ); + } + alert('제출 취소되었습니다.'); + } else { + alert(response.message ?? '제출 취소 중 오류가 발생했습니다.'); + } + } + } catch { + alert('제출 취소 중 오류가 발생했습니다.'); + } + }; + const getGridColumnInfo = ( tab: ColumnInfoList, ): ColumnInfo => ({ @@ -178,18 +622,95 @@ const PublicSewageFacilityInp = () => { columnLayout: tab.columnLayout ?? [], }); + const handleMainFacilityRowClick = async (rowData: Record) => { + if (!searchParamRef.current) { + alert('먼저 조회를 진행해주세요.'); + return; + } + const fcltSn = String(rowData.fcltSn ?? ''); + if (!fcltSn) { + alert('시설 일련번호가 없습니다.'); + return; + } + setSelectedMainFacilitySn(fcltSn); + + const param: PublicSewageMappingTargetsRequestParams = { + sewerBzmnCd: searchParamRef.current.sewerBzmnCd ?? '', + ctpvNm: searchParamRef.current.ctpvNm ?? '', + sggNm: searchParamRef.current.sggNm ?? '', + statsYr: searchParamRef.current.statsYr ?? '', + fcltSn, + }; + + try { + setIsLoading(true); + const response = await statisticsInputPublicSewageFacilityApiService.getMappingTargets(param); + if (response.status !== 'success') { + alert(response.message || '매핑 대상 조회 중 오류가 발생했습니다.'); + return; + } + const mappedTargetRows = (response.data ?? []).map((item) => ({ + ...item, + __checked: item.mappedYn === 'Y' ? 'Y' : '', + })); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set('MainFacilityTarget', toGridRows(mappedTargetRows)); + return next; + }); + } catch { + alert('매핑 대상 조회 중 오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } + }; + + const leftMainFacilityCellStyleCallback = ({ row }: CellStyleCallbackParams) => { + if (!selectedMainFacilitySn) return; + return String(row.fcltSn ?? '') === selectedMainFacilitySn ? 'rg-cell-selected-row' : undefined; + }; + + const searchContextForToolbar = lastSearchedValues ?? searchValues; + const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; + const activeTabToolbar = + toolbarSnapshotByTab.get(activeTab) ?? + computeTabToolbarSnapshot( + activeTabStatus, + tabRowDataExists(activeTab), + hasSearched, + searchContextForToolbar, + inputPeriodEnterable, + isAdminInfo, + ); + const { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard } = activeTabToolbar; + const isMainFacilityTab = activeTab === 'ST00060002'; + return ( <>
- -
-
- - - - + + {showActionToolbarCard && ( +
+
+ {showEnterableToolbarButtons && !isMainFacilityTab && ( + <> + {showIsNotApplicable && } + + + + )} + {!isMainFacilityTab && activeTabStatus?.laeSbmsnSttsCd === '2' && ( + + )} + {showSaveInToolbar && } +
-
+ )} {isLoading &&
@@ -216,10 +737,22 @@ const PublicSewageFacilityInp = () => { rightColumnInfo={getGridColumnInfo(tab.right)} leftFixedColumns={tab.left.fixedColumns} rightFixedColumns={tab.right.fixedColumns} - showHeaderCheckbox={false} + showHeaderCheckbox={true} enableSort={false} leftRowData={rowDataMap.get(leftKey) ?? []} rightRowData={rowDataMap.get(rightKey) ?? []} + leftOnRowClick={ + tab.tabKey === 'ST00060002' + ? (row) => { + void handleMainFacilityRowClick(row); + } + : undefined + } + leftCellStyleCallback={ + tab.tabKey === 'ST00060002' + ? leftMainFacilityCellStyleCallback + : undefined + } setLeftRowData={(updater) => { setRowDataMap((prev) => { const next = new Map(prev); @@ -256,6 +789,7 @@ const PublicSewageFacilityInp = () => { columnInfo={gridColumnInfo} rowStartNum={tab.rowStartNum} fixedColumns={tab.fixedColumns} + statusInfo={activeTabStatus} rowData={rowDataMap.get(tab.tabKey) ?? []} setRowData={(updater) => { setRowDataMap((prev) => { @@ -277,6 +811,14 @@ const PublicSewageFacilityInp = () => { })}
+ ); }; diff --git a/apps/st/src/components/statistics/input/SewagePipeInp.tsx b/apps/st/src/components/statistics/input/SewagePipeInp.tsx index e833c60bd..fb39754ce 100644 --- a/apps/st/src/components/statistics/input/SewagePipeInp.tsx +++ b/apps/st/src/components/statistics/input/SewagePipeInp.tsx @@ -1756,7 +1756,7 @@ const SewagePipeInp = () => { /> {showEnterableToolbarButtons && ( <> - void getLastYearDataFunc()} /> + {activeTab !== 'ST00020002' && void getLastYearDataFunc()} />} void submitFunc(false)} /> )} diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index ad1f4faa8..071d86bca 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -295,9 +295,9 @@ const SeweragePenetrationRateInp = () => { statsCd: statsCd, }; - // if (isAdminInfo) { - // params.superAdmin = 'superAdmin'; - // } + if (isAdminInfo) { + params.superAdmin = 'superAdmin'; + } try { setIsLoading(true); diff --git a/apps/st/src/components/statistics/total/SewagePipeTot.tsx b/apps/st/src/components/statistics/total/SewagePipeTot.tsx index 0ee0aac2b..2553fce19 100644 --- a/apps/st/src/components/statistics/total/SewagePipeTot.tsx +++ b/apps/st/src/components/statistics/total/SewagePipeTot.tsx @@ -11,16 +11,21 @@ import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { SewagePipeColumnInfoListTot } from "@/components/util/column/total/SewagePipeTotColumn"; import { toGridRows } from "@/components/util/grid/gridFunction"; import type { SewagePipeTotalRequest } from "@/entities/statistics/model/statistics-total/statistics-sewage-pipe-total-interface"; +import CellColorInfo from "./CellColorInfo"; const firstTabKey = SewagePipeColumnInfoListTot[0]?.tabKey ?? "ST00020001"; function buildSewagePipeTotSearchParam(values: SearchFormValues): SewagePipeTotalRequest { + const isSummaryEnabled = values.isSummary !== 'ALL'; + const summaryYearCnt = isSummaryEnabled ? Number(values.isSummary) : undefined; + return { ctpvNm: values.sido === "ALL" ? "" : values.sido, sggNm: values.gugun === "ALL" ? "" : values.gugun, sewerBzmnCd: values.bsnmCd, statsYr: values.year, rowDataYn: values.isDetail, + ...(isSummaryEnabled ? { summaryYearCnt } : {}), }; } @@ -164,7 +169,7 @@ const SewagePipeTot = () => { return (
- + {isLoading && (
@@ -172,6 +177,7 @@ const SewagePipeTot = () => {
)} +
{ fixedColumns={info.fixedColumns} enableSort={false} cellStyleCallback={({ row }) => { - const rowType = String(row.rowType ?? ""); - if (rowType === "DETAIL") return undefined; - if (rowType === "BIZ_SUM") return "rg-cell-rowtype-biz-sum"; - return "rg-cell-rowtype-other"; + const rowType = String(row.rowType ?? ''); + if (rowType === 'DETAIL') return undefined; + if (rowType === 'BIZ_SUM') return 'rg-cell-rowtype-biz-sum'; + if (rowType === 'SUMMARY') return 'rg-cell-rowtype-summary'; + return 'rg-cell-rowtype-other'; }} /> diff --git a/apps/st/src/components/statistics/verification/generalVerification/DetailComponent.tsx b/apps/st/src/components/statistics/verification/generalVerification/DetailComponent.tsx index c27387e85..1643a9f89 100644 --- a/apps/st/src/components/statistics/verification/generalVerification/DetailComponent.tsx +++ b/apps/st/src/components/statistics/verification/generalVerification/DetailComponent.tsx @@ -1,6 +1,11 @@ import { useEffect, useRef, useState } from 'react'; +import { useConfirm } from '@frontend-opensource/use-react-hooks'; + +import { useAlertStore } from '@/shared/lib'; import { NoData } from '@/shared/ui/no-data'; +import { generalverificationApiService } from '@/entities/statistics/api/general-verification-service'; +import type { VerifyDetailData, VerifyDetailResponseDto } from '@/entities/statistics/model/general-verification-interface'; import { Button, Table, @@ -9,17 +14,13 @@ import { TableHead, TableRow, } from "@common/components"; -import FormulaComponents from './FormulaComponents'; import { LoadingWithAnimation } from '@common/components'; -import FomulaDialog from './FomulaDialog'; -import { useAlertStore } from '@/shared/lib'; -import { useConfirm } from '@frontend-opensource/use-react-hooks'; - -import { generalverificationApiService } from '@/entities/statistics/api/general-verification-service'; -import type { VerifyDetailData, VerifyDetailResponseDto } from '@/entities/statistics/model/general-verification-interface'; import { getUserInfo } from '../../../../../../../packages/common/components/src/api'; +import FomulaDialog from './FomulaDialog'; +import FormulaComponents from './FormulaComponents'; + interface DetailComponentProps { selectedVrfcRow: number | null; } @@ -107,19 +108,16 @@ const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => { } } - // 여러 항목일 때 마지막이 단항 연산자면: 이전 항목들 연산자는 무조건 null + // 여러 항목일 때 마지막이 단항 연산자면: + // 이전 항목들의 연산자는 '?'일 때만 등록 허용 (자동 null 보정하지 않음) if (rows.length > 1 && lastIsUnary) { - for (let i = 0; i < rows.length - 1; i++) { - const prev = rows[i]; - if (prev) prev.cmprOpert = null; - } - setFormulaList((prev) => { - if (prev.length <= 1) return prev; - const next = prev.map((item, idx) => - idx < prev.length - 1 && item != null ? { ...item, cmprOpert: null } : item, + const invalidUnaryPrevIdx = rows.slice(0, -1).findIndex((r) => r.cmprOpert !== '?'); + if (invalidUnaryPrevIdx !== -1) { + alert( + `마지막 항목이 단항 연산자(is_null, !is_null, floor)일 때는 이전 항목의 연산자를 '?'로 선택해야 합니다.\n(순번: ${invalidUnaryPrevIdx + 1})`, ); - return next; - }); + return; + } } // 마지막 항목의 연산자는 (단항 연산자 케이스 제외) 무조건 null 처리 diff --git a/apps/st/src/components/type/DataGrid.type.ts b/apps/st/src/components/type/DataGrid.type.ts index 432cf68e4..1fef46df3 100644 --- a/apps/st/src/components/type/DataGrid.type.ts +++ b/apps/st/src/components/type/DataGrid.type.ts @@ -365,6 +365,10 @@ export type SplitDataGridProps = { setRightRowData: React.Dispatch< React.SetStateAction[]> >; + /** 좌측 분할 그리드 행 클릭 이벤트 */ + leftOnRowClick?: (rowData: Record) => void; + /** 좌측 분할 그리드 셀 스타일 콜백 */ + leftCellStyleCallback?: CellStyleCallback; /** 전체 정렬 기능 on/off (기본값: false) */ enableSort?: boolean; }; diff --git a/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactColumn.ts b/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactColumn.ts index a97ba549f..8cb23828f 100644 --- a/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactColumn.ts +++ b/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactColumn.ts @@ -1,6 +1,7 @@ -import { footerInt, footerDecimal1, footerDecimal3 } from "@/components/util/grid/footerSummary"; import { ValueType } from "realgrid"; +import { footerDecimal1, footerDecimal3,footerInt } from "@/components/util/grid/footerSummary"; + /** * 하수도 통계 관리 > 06. 공공하수처리시설 */ @@ -112,6 +113,7 @@ export const PublicSewageFactColumn = { { name: 'sftyAcdntEvntcsCnt', fieldName: 'sftyAcdntEvntcsCnt', header: { text: '안전사고건수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false, editable: false, styleName: 'rg-cell-readonly' }, { name: 'fcltSn', fieldName: 'fcltSn', header: { text: '처리시설일련번호' }, visible: false, editable: false, valueType: ValueType.TEXT, styleName: 'rg-cell-readonly' }, { name: 'swtrmtFcltCd', fieldName: 'swtrmtFcltCd', header: { text: '하수처리시설코드' }, visible: false, editable: false, styleName: 'rg-cell-readonly' }, ], diff --git a/apps/st/src/entities/statistics/api/input/statistics-public-sewage-facility-input-service.ts b/apps/st/src/entities/statistics/api/input/statistics-public-sewage-facility-input-service.ts index d2a900077..d77b875c3 100644 --- a/apps/st/src/entities/statistics/api/input/statistics-public-sewage-facility-input-service.ts +++ b/apps/st/src/entities/statistics/api/input/statistics-public-sewage-facility-input-service.ts @@ -1,9 +1,16 @@ -import { ENDPOINT } from "@/shared/config"; import { stClient } from "@/shared/api/client"; +import { ENDPOINT } from "@/shared/config"; + import type { - PublicSewageMainFacilityResponse, PublicSewageFacilityRequestParams, PublicSewageFacilityResponse, + PublicSewageFacilitySaveRequest, + PublicSewageFacilitySaveResponse, + PublicSewageFacilitySubmitRequest, + PublicSewageFacilitySubmitResponse, + PublicSewageMainFacilityResponse, + PublicSewageMappingTargetsRequestParams, + PublicSewageMappingTargetsResponse, StatisticsInputPublicSewageFacilityService, } from "../../model/input/statistics-public-sewage-facility-input-interface"; @@ -23,6 +30,36 @@ export class StatisticsInputPublicSewageFacilityApiService implements Statistics ); return response.data; } + + public async getMappingTargets( + param: PublicSewageMappingTargetsRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.INPUT}/public-sewage-facility/main-facility/mapping-targets`, + { params: param }, + ); + return response.data; + } + + public async savePublicSewageFacility( + param: PublicSewageFacilitySaveRequest, + ): Promise { + const response = await stClient.post( + `${ENDPOINT.ST_SERVICE.INPUT}/public-sewage-facility/facility/save`, + param, + ); + return response.data; + } + + public async submitPublicSewageFacility( + param: PublicSewageFacilitySubmitRequest, + ): Promise { + const response = await stClient.post( + `${ENDPOINT.ST_SERVICE.INPUT}/public-sewage-facility/facility/submit`, + param, + ); + return response.data; + } } export const statisticsInputPublicSewageFacilityApiService = new StatisticsInputPublicSewageFacilityApiService(); diff --git a/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts index e56755f83..19c86e20c 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts @@ -139,7 +139,116 @@ export interface PublicSewageMainFacilityResponse { data: PublicSewageMainFacilityResponseData; } +/** ST00060002 메인시설 클릭 시 우측 매핑대상 조회 요청 */ +export interface PublicSewageMappingTargetsRequestParams { + sewerBzmnCd: string; + ctpvNm: string; + sggNm: string; + statsYr: string; + fcltSn: string; +} + +/** ST00060002 메인시설 클릭 시 우측 매핑대상 조회 항목 */ +export interface PublicSewageMappingTargetItem { + fcltSn: string; + admdstNm: string; + swtrmtFcltCd: string; + swtrmtFcltNm: string; + fcltPstnCn: string; + mappedYn: string; + mainFcltSn: string; + isMainYn: string; +} + +/** ST00060002 메인시설 클릭 시 우측 매핑대상 조회 응답 */ +export interface PublicSewageMappingTargetsResponse { + status: string; + message?: string; + data: PublicSewageMappingTargetItem[]; +} + +/** ST00060001 공공하수처리시설 저장 — saveList 항목 */ +export interface PublicSewageFacilitySaveItem { + admdstCd: string; + admdstNm: string; + swtrmtFcltCd: string; + swtrmtFcltNm: string; + fcltSn: string; + fcltPstnCn: string | null; + + inflwSwagePhysQnt: number; + inflwSwageBoilQnt: number; + inflwSwageAltdQnt: number; + + prcsPhysCpct: number; + prcsBoilCpct: number; + prcsAltdCpct: number; + + prxyCtrtCn: string | null; + sftyAcdntEvntcsCnt: number; + rmrk: string | null; +} + +/** ST00060001 공공하수처리시설 저장 API 요청 */ +export interface PublicSewageFacilitySaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin: string; + + /** + * 저장 요청에서 "해당없음" 처리 여부가 함께 전달되는 경우를 고려합니다. + * (백엔드 스펙에 따라 필수 여부는 달라질 수 있어 optional로 둡니다.) + */ + noApplicable?: boolean; + + saveList: PublicSewageFacilitySaveItem[]; +} + +export interface PublicSewageFacilitySaveResponse { + data?: string; + message?: string; + status: string; +} + +/** 공공하수처리시설 입력 제출 공통 본문 (`submitList` 제외) */ +export interface PublicSewageFacilitySubmitRequestBase { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + noApplicable?: boolean; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; +} + +/** ST00060001 공공하수처리시설 제출 API 요청 (`submitList` 행은 저장과 동일) */ +export interface PublicSewageFacilitySubmitRequest extends PublicSewageFacilitySubmitRequestBase { + submitList: PublicSewageFacilitySaveItem[]; +} + +export interface PublicSewageFacilitySubmitResponse { + data: { + finalStatus: string; + nvpassed: boolean; + } + message?: string; + status: string; +} + export interface StatisticsInputPublicSewageFacilityService { getPublicSewageFacility(param: PublicSewageFacilityRequestParams): Promise; getPublicSewageMainFacility(param: PublicSewageFacilityRequestParams): Promise; + getMappingTargets(param: PublicSewageMappingTargetsRequestParams): Promise; + savePublicSewageFacility(param: PublicSewageFacilitySaveRequest): Promise; + submitPublicSewageFacility(param: PublicSewageFacilitySubmitRequest): Promise; }