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 = () => { @@ -39,6 +344,276 @@ const PublicSewageFacilityInp = () => { const [activeTab, setActiveTab] = useState( PublicSewageFactInpColumnList[0]?.tabKey ?? 'ST00060001', ); + const activeTabRef = useRef(PublicSewageFactInpColumnList[0]?.tabKey ?? 'ST00060001'); + 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, + sewerBzmnCd: values.bsnmCd, + fcltNm: values.fctName, + statsYr: values.year, + }); + + const tabRowDataExists = (tabKey: string): boolean => { + if (tabKey === 'ST00060002') { + const left = rowDataMap.get('MainFacility'); + const right = rowDataMap.get('MainFacilityTarget'); + return (left?.length ?? 0) > 0 || (right?.length ?? 0) > 0; + } + return (rowDataMap.get(tabKey)?.length ?? 0) > 0; + }; + + const loadPublicSewageFacilityActiveTabData = async ( + tabKey: string, + param: PublicSewageFacilityRequestParams, + searchContext: SearchFormValues | null, + ): Promise => { + try { + if (tabKey === 'ST00060001') { + const response = await statisticsInputPublicSewageFacilityApiService.getPublicSewageFacility(param); + if (response.status !== 'success') { + 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) => ({ + ctpvNm: commonInfo.ctpvNm, + ...item, + rmrk: item.rmrk ?? null, + })); + + if (list.length === 0) { + setShowIsNotApplicable(true); + } else { + setShowIsNotApplicable(false); + } + + const gridRows = toGridRows(convertedData); + 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('ST00060001', gridRows); + return next; + }); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + return true; + } + + if (tabKey === 'ST00060002') { + const response = await statisticsInputPublicSewageFacilityApiService.getPublicSewageMainFacility(param); + if (response.status !== 'success') { + 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, + fcltPstnCn: item.fcltPstnCn, + prxyBzentyNmCn: item.prxyBzentyNmCn, + cnsgnCstVl: item.cnsgnCstVl, + prxyCtrtCn: item.prxyCtrtCn, + wholNope: item.wholNope, + isMainYn: item.isMainYn, + admdstCd: item.admdstCd, + fcltSn: item.fcltSn, + swtrmtFcltCd: item.swtrmtFcltCd, + 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', leftGridRows); + next.set('MainFacilityTarget', []); + return next; + }); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, leftGridRows.length > 0, searchContext); + return true; + } + + return true; + } catch { + alert('데이터 조회 중 오류가 발생했습니다.'); + return false; + } + }; + + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === 'ALL') { + alert('시도를 선택해주세요.'); + return; + } + try { + 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 = + 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); + } + + 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, values); + } catch { + alert('데이터 조회 중 오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } + }; + + const handleActiveTabChange = async (tabKey: string) => { + activeTabRef.current = tabKey; + setActiveTab(tabKey); + + if (!hasSearched || tabRowDataExists(tabKey) || !searchParamRef.current) return; + try { + setIsLoading(true); + 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, @@ -47,25 +622,109 @@ 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 &&
+
+ +
-
- - - {PublicSewageFactInpColumnList.map((tab) => ( - - {tab.tabLabel || tab.tabKey} - - ))} - + } + void handleActiveTabChange(v)}> + + {PublicSewageFactInpColumnList.map((tab) => ( + + {tab.tabLabel || tab.tabKey} + + ))} + {PublicSewageFactInpColumnList.map((tab) => { if (tab.isSplitGrid && tab.left && tab.right) { @@ -78,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); @@ -118,6 +789,7 @@ const PublicSewageFacilityInp = () => { columnInfo={gridColumnInfo} rowStartNum={tab.rowStartNum} fixedColumns={tab.fixedColumns} + statusInfo={activeTabStatus} rowData={rowDataMap.get(tab.tabKey) ?? []} setRowData={(updater) => { setRowDataMap((prev) => { @@ -137,7 +809,16 @@ const PublicSewageFacilityInp = () => { ); })} - + +
+ ); }; diff --git a/apps/st/src/components/statistics/input/SewagePipeInp.tsx b/apps/st/src/components/statistics/input/SewagePipeInp.tsx index 754038519..e73f398c2 100644 --- a/apps/st/src/components/statistics/input/SewagePipeInp.tsx +++ b/apps/st/src/components/statistics/input/SewagePipeInp.tsx @@ -28,13 +28,20 @@ import { doList } from "@/components/util/column/input/DoList"; import { statisticsInputSewPipeApiService } from "@/entities/statistics/api/input/statistics-sewage-pipe-input-service"; import type { SeweragePipeInputFactInstallStatusSaveRequest, + SeweragePipeInputFactInstallStatusSubmitRequest, SeweragePipeInputManholeStatusItem, + SeweragePipeInputManholeStatusSaveYearlyStatItem, SeweragePipeInputManholeStatusSaveRequest, + SeweragePipeInputManholeStatusSubmitRequest, SeweragePipeInputPipeDiameterStatusSaveRequest, + SeweragePipeInputPipeDiameterStatusSubmitRequest, SeweragePipeInputPipeTypeStatusItem, SeweragePipeInputPipeTypeStatusSaveRequest, + SeweragePipeInputPipeTypeStatusSubmitRequest, SeweragePipeInputRegionalFactStatusSaveRequest, SeweragePipeInputRegionalFactStatusSaveItem, + SeweragePipeInputRegionalFactStatusSubmitRequest, + SeweragePipeInputSubmitResponse, SeweragePipeInputVerificationResult, SeweragePipeRequestParams, } from "@/entities/statistics/model/input/statistics-sewage-pipe-input-interface"; @@ -903,6 +910,22 @@ const SewagePipeInp = () => { const userInfo = await getUserInfo(); const statsCdForTab = tabStatsCdMap.get(tabKey) ?? statsCd; const statusForTab = tabStatusInfoMap.get(tabKey) ?? null; + const submitBase = { + statsYr: values.year ?? "", + ctpvNm: values.sido ?? "", + sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), + sewerBzmnCd: values.bsnmCd ?? "", + userId: userInfo.userUnqId, + laePicNm: submitInfo.laePicNm, + laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, + laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, + laePicTelNo: submitInfo.laePicTelNo, + laeIdfrNm: submitInfo.laeIdfrNm, + laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, + laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, + laeIdfrTelNo: submitInfo.laeIdfrTelNo, + }; + let response: SeweragePipeInputSubmitResponse | null = null; switch (tabKey) { case REGIONAL_FACT_TAB_KEY: { @@ -914,97 +937,294 @@ const SewagePipeInp = () => { const submitList = buildRegionalFactSubmitListFromGrid(currentData); - const requestData = { - statsYr: values.year ?? "", - ctpvNm: values.sido ?? "", - sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), - sewerBzmnCd: values.bsnmCd ?? "", - userId: userInfo.userUnqId, + const requestData: SeweragePipeInputRegionalFactStatusSubmitRequest = { + ...submitBase, submitList, - laePicNm: submitInfo.laePicNm, - laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, - laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, - laePicTelNo: submitInfo.laePicTelNo, - laeIdfrNm: submitInfo.laeIdfrNm, - laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, - laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, - laeIdfrTelNo: submitInfo.laeIdfrTelNo, }; setGlobalProgressLabel("일반 검증 진행 중..."); - const response = + response = await statisticsInputSewPipeApiService.submitRegionalFactStatus(requestData); - if (response.status !== "success") { - alert(response.message || "데이터 제출 중 오류가 발생했습니다."); + break; + } + case "ST00020002": { + const currentData = getFactInstallGridDataRef.current?.(); + if (!currentData || currentData.length === 0) { + alert("제출할 데이터가 없습니다."); return; } - - const finalStatus = response.data?.finalStatus; - const nvpassed = response.data?.nvpassed; - - if (finalStatus === "5") { - setGlobalProgress(50); - setGlobalProgressLabel("IMR 검증 진행 중..."); - const imrParam = { - user_id: userInfo.userUnqId, - sewer_bzmn_cd: values.bsnmCd ?? "", - stats_cd: statsCdForTab, - stats_yr: values.year ?? "", - lae_sbmsn_stts_cd: statusForTab?.laeSbmsnSttsCd === "4" ? "4" : "2", - gen_verify_stts_cd: nvpassed ? "1" : "2", - }; - - try { - const imrResponse = await statisticsApi.callImrService(imrParam); - if (imrResponse.job_id) { - await waitForImrJobCompleted( - imrResponse.job_id, - (id) => statisticsApi.getImrStatus(id), - { - onProgress: (jobProgress) => { - setGlobalProgress(50 + Math.round(jobProgress / 2)); - }, - }, - ); - } - setGlobalProgress(100); - await loadSewagePipeActiveTabData( - tabKey, - buildSewagePipeSearchParam(values), - values, - true, - false, - ); - setVerificationResultDialogOpen(true); - } catch (e) { - await loadSewagePipeActiveTabData( - tabKey, - buildSewagePipeSearchParam(values), - values, - true, - false, - ); - const msg = - e instanceof Error && e.message - ? e.message - : "IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요."; - alert(msg); - } - } else { - setGlobalProgress(100); - await loadSewagePipeActiveTabData( - tabKey, - buildSewagePipeSearchParam(values), - values, - true, - false, - ); - alert("제출되었습니다."); + const columnFormatMap = new Map(); + FactInstallStatusInpColumn.columns.forEach((col) => { + if (col.fieldName && col.dataFormat) columnFormatMap.set(col.fieldName, col.dataFormat); + }); + const formatNumber = (value: string | number | null | undefined, fieldName: string): number => { + if (value === null || value === undefined || value === "") return 0; + const num = typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value; + if (isNaN(num)) return 0; + const format = columnFormatMap.get(fieldName); + if (!format) return num; + const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0; + return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); + }; + const submitList = currentData.map((d) => ({ + admdstCd: String(d.admdstCd ?? ""), + sggNm: String(d.sggNm ?? ""), + admdstNm: String(d.admdstNm ?? ""), + fcltSn: String(d.fcltSn ?? ""), + swtrmtFcltCd: String(d.swtrmtFcltCd ?? ""), + swtrmtFcltNm: String(d.swtrmtFcltNm ?? ""), + prcsFcltCpct: formatNumber(d.prcsFcltCpct, "prcsFcltCpct"), + prcsZoneArea: formatNumber(d.prcsZoneArea, "prcsZoneArea"), + cbsysPrcsZoneArea: formatNumber(d.cbsysPrcsZoneArea, "cbsysPrcsZoneArea"), + sprsysPrcsZoneArea: formatNumber(d.sprsysPrcsZoneArea, "sprsysPrcsZoneArea"), + sprsysVartnRt: formatNumber(d.sprsysVartnRt, "sprsysVartnRt"), + mltvEqpmCnt: formatNumber(d.mltvEqpmCnt, "mltvEqpmCnt"), + cbsysMltvEqpmCnt: formatNumber(d.cbsysMltvEqpmCnt, "cbsysMltvEqpmCnt"), + sprsysMltvEqpmCnt: formatNumber(d.sprsysMltvEqpmCnt, "sprsysMltvEqpmCnt"), + sprsysMltvLnkgCnt: formatNumber(d.sprsysMltvLnkgCnt, "sprsysMltvLnkgCnt"), + sprsysMltvYetLnkgCnt: formatNumber(d.sprsysMltvYetLnkgCnt, "sprsysMltvYetLnkgCnt"), + wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"), + wholIcptgAtlfcsPrlg: formatNumber(d.wholIcptgAtlfcsPrlg, "wholIcptgAtlfcsPrlg"), + icptgPrlg: formatNumber(d.icptgPrlg, "icptgPrlg"), + atlfcsPrlg: formatNumber(d.atlfcsPrlg, "atlfcsPrlg"), + cbsysStofcbrCnt: formatNumber(d.cbsysStofcbrCnt, "cbsysStofcbrCnt"), + sprsysStofcbrCnt: formatNumber(d.sprsysStofcbrCnt, "sprsysStofcbrCnt"), + sprsysExlnOtflCnt: formatNumber(d.sprsysExlnOtflCnt, "sprsysExlnOtflCnt"), + wholBrchlnPrlg: formatNumber(d.wholBrchlnPrlg, "wholBrchlnPrlg"), + cbsysBrchlnPrlg: formatNumber(d.cbsysBrchlnPrlg, "cbsysBrchlnPrlg"), + exlnBrchlnPrlg: formatNumber(d.exlnBrchlnPrlg, "exlnBrchlnPrlg"), + drtwBrchlnPrlg: formatNumber(d.drtwBrchlnPrlg, "drtwBrchlnPrlg"), + rmrk: String(d.rmrk ?? ""), + })); + const requestData: SeweragePipeInputFactInstallStatusSubmitRequest = { + ...submitBase, + submitList, + }; + setGlobalProgressLabel("일반 검증 진행 중..."); + response = await statisticsInputSewPipeApiService.submitFactInstallStatus(requestData); + break; + } + case "ST00020003": { + const currentData = getPipeTypeGridDataRef.current?.(); + if (!currentData || currentData.length === 0) { + alert("제출할 데이터가 없습니다."); + return; } + const columnFormatMap = new Map(); + PipeTypeStatusInpColumn.columns.forEach((col) => { + if (col.fieldName && col.dataFormat) columnFormatMap.set(col.fieldName, col.dataFormat); + }); + const formatNumber = (value: string | number | null | undefined, fieldName: string): number => { + if (value === null || value === undefined || value === "") return 0; + const num = typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value; + if (isNaN(num)) return 0; + const format = columnFormatMap.get(fieldName); + if (!format) return num; + const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0; + return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); + }; + const lastDataYear = getPipeTypeLastDataYear(); + const submitList = currentData.map((d) => { + const yearlyStatList = []; + for (let year = 1995; year <= lastDataYear; year++) { + const wholKey = `y${year}PrlgWhol`; + const lyngKey = `y${year}LyngPrlg`; + const dmltnKey = `y${year}DmltnPrlg`; + yearlyStatList.push({ + lyngYr: String(year), + lyngPrlg: formatNumber(d[lyngKey] as string | number | undefined, lyngKey), + dmltnPrlg: formatNumber(d[dmltnKey] as string | number | undefined, dmltnKey), + prlgWhol: formatNumber(d[wholKey] as string | number | undefined, wholKey), + }); + } + return { + admdstCd: String(d.admdstCd ?? ""), + admdstNm: String(d.admdstNm ?? ""), + pipeSeCd: String(d.pipeSeCd ?? ""), + yearlyStatList, + rmrk: String(d.rmrk ?? ""), + lyngPrlgWhol: formatNumber(d.lyngPrlg, "lyngPrlg"), + dmltnPrlgWhol: formatNumber(d.dmltnPrlg, "dmltnPrlg"), + pipeByPrstWhol: formatNumber(d.pipeByPrstWhol, "pipeByPrstWhol"), + }; + }); + const requestData: SeweragePipeInputPipeTypeStatusSubmitRequest = { + ...submitBase, + submitList, + }; + setGlobalProgressLabel("일반 검증 진행 중..."); + response = await statisticsInputSewPipeApiService.submitPipeTypeStatus(requestData); + break; + } + case "ST00020004": { + const currentData = getPipeDiameterGridDataRef.current?.(); + if (!currentData || currentData.length === 0) { + alert("제출할 데이터가 없습니다."); + return; + } + const columnFormatMap = new Map(); + PipeDiameterStatusInpColumn.columns.forEach((col) => { + if (col.fieldName && col.dataFormat) columnFormatMap.set(col.fieldName, col.dataFormat); + }); + const formatNumber = (value: string | number | null | undefined, fieldName: string): number => { + if (value === null || value === undefined || value === "") return 0; + const num = typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value; + if (isNaN(num)) return 0; + const format = columnFormatMap.get(fieldName); + if (!format) return num; + const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0; + return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); + }; + const submitList = currentData.map((d) => ({ + pipeSeCd: String(d.pipeSeCd ?? ""), + step1PdmtPrlg: formatNumber(d.step1PdmtPrlg, "step1PdmtPrlg"), + step2PdmtPrlg: formatNumber(d.step2PdmtPrlg, "step2PdmtPrlg"), + step3PdmtPrlg: formatNumber(d.step3PdmtPrlg, "step3PdmtPrlg"), + step4PdmtPrlg: formatNumber(d.step4PdmtPrlg, "step4PdmtPrlg"), + step5PdmtPrlg: formatNumber(d.step5PdmtPrlg, "step5PdmtPrlg"), + sqrePdmtPrlg: formatNumber(d.sqrePdmtPrlg, "sqrePdmtPrlg"), + etcPdmtPrlg: formatNumber(d.etcPdmtPrlg, "etcPdmtPrlg"), + rmrk: String(d.rmrk ?? ""), + pdmtByTsmWhol: formatNumber(d.pdmtByTsmWhol, "pdmtByTsmWhol"), + admdstCd: String(d.admdstCd ?? ""), + admdstNm: String(d.admdstNm ?? ""), + })); + const requestData: SeweragePipeInputPipeDiameterStatusSubmitRequest = { + ...submitBase, + submitList, + }; + setGlobalProgressLabel("일반 검증 진행 중..."); + response = await statisticsInputSewPipeApiService.submitPipeDiameterStatus(requestData); + break; + } + case "ST00020005": { + const currentData = getManholeGridDataRef.current?.(); + if (!currentData || currentData.length === 0) { + alert("제출할 데이터가 없습니다."); + return; + } + const columnFormatMap = new Map(); + ManholeStatusInpColumn.columns.forEach((col) => { + if (col.fieldName && col.dataFormat) columnFormatMap.set(col.fieldName, col.dataFormat); + }); + const formatNumber = (value: string | number | null | undefined, fieldName: string): number => { + if (value === null || value === undefined || value === "") return 0; + const num = typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value; + if (isNaN(num)) return 0; + const format = columnFormatMap.get(fieldName); + if (!format) return num; + const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0; + return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); + }; + const minYy = 14; + const maxYy = getManholeLastDataYear() % 100; + const submitList = currentData.map((d) => { + const yearlyStats: SeweragePipeInputManholeStatusSaveYearlyStatItem[] = []; + for (let yy = minYy; yy <= maxYy; yy++) { + const year = 2000 + yy; + const wholKey = `yy${yy}WholMnhlCnt`; + const step1Key = `yy${yy}Step1MnhlCnt`; + const step2Key = `yy${yy}Step2MnhlCnt`; + const step3Key = `yy${yy}Step3MnhlCnt`; + const step4Key = `yy${yy}Step4MnhlCnt`; + const step5Key = `yy${yy}Step5MnhlCnt`; + yearlyStats.push({ + manhYr: String(year), + wholMnhlCnt: formatNumber(d[wholKey] as string | number | undefined, wholKey), + step1MnhlCnt: formatNumber(d[step1Key] as string | number | undefined, step1Key), + step2MnhlCnt: formatNumber(d[step2Key] as string | number | undefined, step2Key), + step3MnhlCnt: formatNumber(d[step3Key] as string | number | undefined, step3Key), + step4MnhlCnt: formatNumber(d[step4Key] as string | number | undefined, step4Key), + step5MnhlCnt: formatNumber(d[step5Key] as string | number | undefined, step5Key), + }); + } + return { + emphsMngSeCd: String(d.emphsMngSeCd ?? ""), + admdstCd: String(d.admdstCd ?? ""), + admdstNm: String(d.admdstNm ?? ""), + yearlyStats, + rmrk: String(d.rmrk ?? ""), + }; + }); + const requestData: SeweragePipeInputManholeStatusSubmitRequest = { + ...submitBase, + submitList, + }; + setGlobalProgressLabel("일반 검증 진행 중..."); + response = await statisticsInputSewPipeApiService.submitManholeStatus(requestData); break; } default: alert("해당 탭 제출 API는 준비 중입니다."); + return; + } + + if (!response || response.status !== "success") { + alert(response?.message || "데이터 제출 중 오류가 발생했습니다."); + return; + } + + const finalStatus = response.data?.finalStatus; + const nvpassed = response.data?.nvpassed; + + if (finalStatus === "5") { + setGlobalProgress(50); + setGlobalProgressLabel("IMR 검증 진행 중..."); + const imrParam = { + user_id: userInfo.userUnqId, + sewer_bzmn_cd: values.bsnmCd ?? "", + stats_cd: statsCdForTab, + stats_yr: values.year ?? "", + lae_sbmsn_stts_cd: statusForTab?.laeSbmsnSttsCd === "4" ? "4" : "2", + gen_verify_stts_cd: nvpassed ? "1" : "2", + }; + + try { + const imrResponse = await statisticsApi.callImrService(imrParam); + if (imrResponse.job_id) { + await waitForImrJobCompleted( + imrResponse.job_id, + (id) => statisticsApi.getImrStatus(id), + { + onProgress: (jobProgress) => { + setGlobalProgress(50 + Math.round(jobProgress / 2)); + }, + }, + ); + } + setGlobalProgress(100); + await loadSewagePipeActiveTabData( + tabKey, + buildSewagePipeSearchParam(values), + values, + true, + false, + ); + setVerificationResultDialogOpen(true); + } catch (e) { + await loadSewagePipeActiveTabData( + tabKey, + buildSewagePipeSearchParam(values), + values, + true, + false, + ); + const msg = + e instanceof Error && e.message + ? e.message + : "IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요."; + alert(msg); + } + } else { + setGlobalProgress(100); + await loadSewagePipeActiveTabData( + tabKey, + buildSewagePipeSearchParam(values), + values, + true, + false, + ); + alert("제출되었습니다."); } } catch { alert("데이터 제출 중 오류가 발생했습니다."); @@ -1537,7 +1757,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/management/NoticeTalkSending.tsx b/apps/st/src/components/statistics/management/NoticeTalkSending.tsx index 6ead463fb..ff0cae557 100644 --- a/apps/st/src/components/statistics/management/NoticeTalkSending.tsx +++ b/apps/st/src/components/statistics/management/NoticeTalkSending.tsx @@ -3,9 +3,7 @@ import { useMemo, useState } from "react"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; -import { useQuery } from "@tanstack/react-query"; import { - type ColumnDef, getCoreRowModel, type RowSelectionState, useReactTable, @@ -14,17 +12,21 @@ import { import { client } from "@/shared/api/client"; import { ENDPOINT } from "@/shared/config"; import { useAlertStore } from "@/shared/lib"; -import type { PageableList } from "@/shared/model"; import { type AdstZoneResponseDto, + type BusinessSendRequest, + type BusinessSendResponse, + type SelectedRecipient, type SewerStUserItem, + useNtftalkTemplateList, + useNtftalkTemplateVariables, + userListColumns, useSewerStUserList, useStAnlsSidoCombo, useStAnlsSigunguCombo, } from "@/entities/statistics"; import { Button, - Checkbox, DataTable, Dialog, DialogClose, @@ -49,35 +51,6 @@ import { TableRow, } from "@common/components"; -interface NtftalkTemplate { - templateCode: string; - templateName: string; - templateContent: string; -} - -interface TemplateVariablesResponse { - templateCode: string; - variables: string[]; - count: number; -} - -interface BusinessSendRequest { - templateCode: string; - callback?: string; - fixedRecipientNum?: string; - recipients: Record[]; -} - -interface BusinessSendResponse { - success: boolean; - sentCount: number; - message: string; -} - -interface SelectedRecipient extends SewerStUserItem { - variableOverrides: Record; -} - type CommonVariableKey = | "userNm" | "sido" @@ -94,14 +67,6 @@ const GUBUN_OPTIONS = [ { value: "IDFR", label: "승인자" }, { value: "MCTPV", label: "검토자" }, ] as const; -const DEFAULT_TEMPLATE_PARAMS = { - sort: "createdAt,desc", - senderKey: "", - inspectionStatus: "APR", - templateMessageType: "BA", - page: 0, - size: 10, -}; function getDefaultStatsYear(): string { return String(new Date().getFullYear() - 1); @@ -161,38 +126,11 @@ function NtitlkSndng() { const [userListRowSelection, setUserListRowSelection] = useState({}); - const { data: templatePageData } = useQuery({ - queryKey: [ - ENDPOINT.NTFTALK_SERVICE.TEMP, - "ntitlk-sndng", - DEFAULT_TEMPLATE_PARAMS, - ], - queryFn: async () => { - const response = await client.get>( - ENDPOINT.NTFTALK_SERVICE.TEMP, - { params: DEFAULT_TEMPLATE_PARAMS }, - ); - return response.data; - }, - }); - + const { data: templatePageData } = useNtftalkTemplateList(); const templates = templatePageData?.content ?? []; - const { data: templateVariablesData } = useQuery({ - queryKey: [ - ENDPOINT.NTFTALK_SERVICE.ATA_SEND, - "template-variables", - selectedTemplateCode, - ], - queryFn: async () => { - const response = await client.get( - `${ENDPOINT.NTFTALK_SERVICE.ATA_SEND}/template-variables/${selectedTemplateCode}`, - ); - return response.data; - }, - enabled: Boolean(selectedTemplateCode), - }); - + const { data: templateVariablesData } = + useNtftalkTemplateVariables(selectedTemplateCode); const variableNames = templateVariablesData?.variables ?? []; const { data: sidoData } = useStAnlsSidoCombo(); @@ -203,80 +141,6 @@ function NtitlkSndng() { const { data: userListResponse, isLoading: isUserListLoading } = useSewerStUserList(appliedParams); const userList = (userListResponse as unknown as SewerStUserItem[]) ?? []; - const userListColumns = useMemo[]>( - () => [ - { - id: "select", - header: ({ table }) => ( -
e.stopPropagation()} - onKeyDown={(e) => e.stopPropagation()} - role="presentation" - > - - table.toggleAllPageRowsSelected(!!value) - } - aria-label="전체 선택" - /> -
- ), - cell: ({ row }) => ( -
e.stopPropagation()} - onKeyDown={(e) => e.stopPropagation()} - role="presentation" - > - - row.toggleSelected(!!value) - } - aria-label="행 선택" - /> -
- ), - enableSorting: false, - enableHiding: false, - }, - { - accessorKey: "userNm", - header: "이름", - }, - { - accessorKey: "sido", - header: "시도", - }, - { - accessorKey: "gugun", - header: "시군구", - }, - { - accessorKey: "gubunNm", - header: "구분", - }, - { - accessorKey: "deptNm", - header: "소속", - }, - { - accessorKey: "jbgdNm", - header: "직급", - }, - { - accessorKey: "telNo", - header: "전화번호", - }, - ], - [], - ); const userListTable = useReactTable({ data: userList, columns: userListColumns, 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/HbStatisticsVerification.tsx b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx index 722fd3eca..2206b5d3b 100644 --- a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx +++ b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx @@ -20,18 +20,18 @@ import { ENDPOINT } from "@/shared/config"; import { useAlertStore } from "@/shared/lib"; import type { ApiErrorResponse } from "@/shared/model"; import { CodeSelect } from "@/entities/code"; -import type { - AnalysisRequestPayload, - AnalysisResponseDto, - StAnlsMstResponse, - StAnlsMstSearchParams, - StAnlsOutlierListResponse, +import { + analysisApi, + type AnalysisRequestPayload, + type AnalysisResponseDto, + type StAnlsMstResponse, + type StAnlsMstSearchParams, + type StAnlsOutlierListResponse, + statisticsAnalysisMasterService, + verificationHistoryColumns, + type VerificationTargetCodeResponse, + verificationTargetManagementApi, } from "@/entities/statistics"; -import { verificationHistoryColumns } from "@/entities/statistics"; -import { analysisApi } from "@/entities/statistics/api/analysis-api"; -import { stAnlsMstApi } from "@/entities/statistics/api/statistics-analysis-master-api"; -import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service"; -import type { VerificationTargetCodeResponse } from "@/entities/statistics/model/verification-target-management-interface"; import { Button, Checkbox, @@ -102,7 +102,7 @@ function toNumber(value: string): number { return Number.isNaN(parsed) ? 0 : parsed; } -const HbStatisticsVerification = () => { +export default function HbStatisticsVerification() { const { setMessage: alert } = useAlertStore((state) => state); const queryClient = useQueryClient(); @@ -207,7 +207,9 @@ const HbStatisticsVerification = () => { params.verifyDtEnd = dayjs(to).format("YYYY-MM-DD"); } - return stAnlsMstApi.getAnalysisHistoryList(params); + return statisticsAnalysisMasterService.getAnalysisHistoryList( + params, + ); }, refetchInterval: (query) => { const data = query.state.data; @@ -248,9 +250,10 @@ const HbStatisticsVerification = () => { ], queryFn: async () => { if (!outlierDialog.anlsSn) return []; - const response = await stAnlsMstApi.getOutlierList( - outlierDialog.anlsSn, - ); + const response = + await statisticsAnalysisMasterService.getOutlierList( + outlierDialog.anlsSn, + ); return response; }, enabled: outlierDialog.open && !!outlierDialog.anlsSn, @@ -771,6 +774,4 @@ const HbStatisticsVerification = () => { ); -}; - -export default HbStatisticsVerification; +} diff --git a/apps/st/src/components/statistics/verification/ImrVerificationHistory.tsx b/apps/st/src/components/statistics/verification/ImrVerificationHistory.tsx index ad9990e95..209856e6a 100644 --- a/apps/st/src/components/statistics/verification/ImrVerificationHistory.tsx +++ b/apps/st/src/components/statistics/verification/ImrVerificationHistory.tsx @@ -24,25 +24,21 @@ import dayjs from "dayjs"; import { ENDPOINT } from "@/shared/config"; import { CodeSelect } from "@/entities/code"; -import type { - AdstZoneResponseDto, - StAnlsMstResponse, - StAnlsMstSearchParams, - StAnlsRsltResponse, -} from "@/entities/statistics"; import { + type AdstZoneResponseDto, IMR_DETAIL_VISIBLE_COLUMNS_BY_STATS_CD, IMR_DETAIL_VISIBLE_COLUMNS_DEFAULT, + imrApi, imrDetailGridColumnInfo, imrVerificationHistoryColumns, -} from "@/entities/statistics"; -import { imrApi } from "@/entities/statistics/api/imr-api"; -import { + type StAnlsMstResponse, + type StAnlsMstSearchParams, + stAnlsRsltApi, + type StAnlsRsltResponse, + statisticsAnalysisMasterService, useStAnlsSidoCombo, useStAnlsSigunguCombo, -} from "@/entities/statistics/api/queries"; -import { stAnlsMstApi } from "@/entities/statistics/api/statistics-analysis-master-api"; -import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api"; +} from "@/entities/statistics"; import { Button, DataTable, @@ -144,7 +140,7 @@ const ImrVerificationHistory = () => { historyPagination, ], queryFn: () => - stAnlsMstApi.getImrAnalysisMasterList({ + statisticsAnalysisMasterService.getImrAnalysisMasterList({ ...appliedSearchParams, page: historyPagination.pageIndex, size: historyPagination.pageSize, diff --git a/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx b/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx index 89a988fda..e9c17bff9 100644 --- a/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx +++ b/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx @@ -1,6 +1,18 @@ import React, { useState } from "react"; -import type { StAnlsDtlResponse } from "@/entities/statistics"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { useQueryClient } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; + +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; +import type { ApiErrorResponse } from "@/shared/model"; +import type { + StAnlsDtlConfirmRequest, + StAnlsDtlConfirmResponse, + StAnlsDtlResponse, +} from "@/entities/statistics"; +import { statisticsAnalysisDetailService } from "@/entities/statistics"; import { Button } from "@common/components"; import Indicators from "./Indicators"; @@ -38,6 +50,47 @@ const HbVerificationDetail: React.FC = ({ vrfcTrgtNm: string; } | null>(null); + const queryClient = useQueryClient(); + const { setMessage: alert } = useAlertStore((state) => state); + const { confirm } = useConfirm(); + + const confirmMutation = useMutationWrapper< + StAnlsDtlConfirmResponse, + AxiosError, + StAnlsDtlConfirmRequest + >({ + mutationFn: (payload) => + statisticsAnalysisDetailService.confirmDetail(payload), + onSuccess: async () => { + setCheckedSns([]); + + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: [ + ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_DTL, + "list", + anlsSn, + ], + }), + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST], + }), + ]); + }, + }); + + const handleConfirmClick = async () => { + if (checkedSns.length === 0) return; + + if (!(await confirm("검증확정 처리하시겠습니까?"))) return; + + const vrfcTrgtSnList = checkedSns + .map((value) => Number(value)) + .filter((value) => !Number.isNaN(value)); + + confirmMutation.mutate({ anlsSn, vrfcTrgtSnList }); + }; + const handleTargetSelect = (row: StAnlsDtlResponse) => { setSelectedVrfcTrgtSn(String(row.vrfcTrgtSn)); setSelectedTargetInfo({ @@ -70,7 +123,14 @@ const HbVerificationDetail: React.FC = ({ 검증 상세
- +
diff --git a/apps/st/src/components/statistics/verification/detail/VerificationList.tsx b/apps/st/src/components/statistics/verification/detail/VerificationList.tsx index 3966b3131..306c533a3 100644 --- a/apps/st/src/components/statistics/verification/detail/VerificationList.tsx +++ b/apps/st/src/components/statistics/verification/detail/VerificationList.tsx @@ -6,7 +6,7 @@ import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; import { ENDPOINT } from "@/shared/config"; import type { StAnlsDtlResponse } from "@/entities/statistics"; import { getVerificationListColumns } from "@/entities/statistics"; -import { stAnlsDtlApi } from "@/entities/statistics/api/statistics-analysis-detail-api"; +import { statisticsAnalysisDetailService } from "@/entities/statistics/api/statistics-analysis-detail-service"; import { DataTable } from "@common/components"; interface VerificationListProps { @@ -27,7 +27,11 @@ const VerificationList: React.FC = ({ const { data: detailData, isLoading } = useQuery({ queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_DTL, "list", anlsSn], queryFn: () => - stAnlsDtlApi.getDetailList({ anlsSn, page: 0, size: 100 }), + statisticsAnalysisDetailService.getDetailList({ + anlsSn, + page: 0, + size: 100, + }), enabled: !!anlsSn, }); 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/MainFacilityInpColumn.ts b/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/MainFacilityInpColumn.ts index eed27df03..cd85731e6 100644 --- a/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/MainFacilityInpColumn.ts +++ b/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/MainFacilityInpColumn.ts @@ -7,6 +7,7 @@ export const MainFacilityColumn = { { name: 'prxyBzentyNmCn', fieldName: 'prxyBzentyNmCn', header: { text: '위탁업체명' }, editable: false }, { name: 'cnsgnCstVl', fieldName: 'cnsgnCstVl', header: { text: '위탁비용(백만원/년)' }, editable: false }, { name: 'prxyCtrtCn', fieldName: 'prxyCtrtCn', header: { text: '위탁계약기간' }, editable: false }, + { name: 'wholNope', fieldName: 'wholNope', header: { text: '직원수(명)' }, editable: false }, { name: 'isMainYn', fieldName: 'isMainYn', header: { text: '중심처리시설여부' }, editable: false }, { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, editable: false, styleName: 'rg-cell-readonly' }, @@ -21,6 +22,7 @@ export const MainFacilityColumn = { 'prxyBzentyNmCn', 'cnsgnCstVl', 'prxyCtrtCn', + 'wholNope', 'isMainYn', 'admdstCd', 'fcltSn', 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 091286300..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. 공공하수처리시설 */ @@ -110,8 +111,9 @@ export const PublicSewageFactColumn = { { name: 'intgPrcsFcltCn', fieldName: 'intgPrcsFcltCn', header: { text: '중심처리시설' }, editable: false, styleName: 'rg-cell-readonly' }, { name: 'wholNope', fieldName: 'wholNope', header: { text: '직원총수(명)' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' }, { name: 'sftyAcdntEvntcsCnt', fieldName: 'sftyAcdntEvntcsCnt', header: { text: '안전사고건수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'rmk', fieldName: '비고', header: { text: '비고' } }, + { 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' }, ], @@ -208,7 +210,7 @@ export const PublicSewageFactColumn = { 'intgPrcsFcltCn', 'wholNope', 'sftyAcdntEvntcsCnt', - 'rmk', + 'rmrk', 'admdstCd', 'fcltSn', 'swtrmtFcltCd', 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 new file mode 100644 index 000000000..d77b875c3 --- /dev/null +++ b/apps/st/src/entities/statistics/api/input/statistics-public-sewage-facility-input-service.ts @@ -0,0 +1,65 @@ +import { stClient } from "@/shared/api/client"; +import { ENDPOINT } from "@/shared/config"; + +import type { + PublicSewageFacilityRequestParams, + PublicSewageFacilityResponse, + PublicSewageFacilitySaveRequest, + PublicSewageFacilitySaveResponse, + PublicSewageFacilitySubmitRequest, + PublicSewageFacilitySubmitResponse, + PublicSewageMainFacilityResponse, + PublicSewageMappingTargetsRequestParams, + PublicSewageMappingTargetsResponse, + StatisticsInputPublicSewageFacilityService, +} from "../../model/input/statistics-public-sewage-facility-input-interface"; + +export class StatisticsInputPublicSewageFacilityApiService implements StatisticsInputPublicSewageFacilityService { + public async getPublicSewageFacility(param: PublicSewageFacilityRequestParams): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.INPUT}/public-sewage-facility/facility`, + { params: param }, + ); + return response.data; + } + + public async getPublicSewageMainFacility(param: PublicSewageFacilityRequestParams): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.INPUT}/public-sewage-facility/main-facility`, + { params: param }, + ); + 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/api/input/statistics-sewage-pipe-input-service.ts b/apps/st/src/entities/statistics/api/input/statistics-sewage-pipe-input-service.ts index daefd245a..956abbd6b 100644 --- a/apps/st/src/entities/statistics/api/input/statistics-sewage-pipe-input-service.ts +++ b/apps/st/src/entities/statistics/api/input/statistics-sewage-pipe-input-service.ts @@ -16,6 +16,8 @@ import type { SeweragePipeInputRegionalFactStatusSaveResponse, SeweragePipeInputRegionalFactStatusSubmitRequest, SeweragePipeInputRegionalFactStatusSubmitResponse, + SeweragePipeInputFactInstallStatusSubmitRequest, + SeweragePipeInputFactInstallStatusSubmitResponse, SeweragePipeInputRegionalResponse, SeweragePipeRequestParams, StatisticsInputSewPipeService, @@ -23,10 +25,14 @@ import type { SeweragePipeInputFactInstallStatusSaveResponse, SeweragePipeInputPipeTypeStatusSaveRequest, SeweragePipeInputPipeTypeStatusSaveResponse, + SeweragePipeInputPipeTypeStatusSubmitRequest, SeweragePipeInputPipeDiameterStatusSaveRequest, SeweragePipeInputPipeDiameterStatusSaveResponse, + SeweragePipeInputPipeDiameterStatusSubmitRequest, SeweragePipeInputManholeStatusSaveRequest, SeweragePipeInputManholeStatusSaveResponse, + SeweragePipeInputManholeStatusSubmitRequest, + SeweragePipeInputSubmitResponse, } from "../../model/input/statistics-sewage-pipe-input-interface"; export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeService { @@ -126,6 +132,14 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS return response.data; } + public async submitFactInstallStatus(param: SeweragePipeInputFactInstallStatusSubmitRequest): Promise { + const response = await stClient.post( + `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/facility-install-status/submit`, + param, + ); + return response.data; + } + public async savePipeTypeStatus(param: SeweragePipeInputPipeTypeStatusSaveRequest): Promise { const response = await stClient.post( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/pipe-type-status/save`, @@ -134,6 +148,14 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS return response.data; } + public async submitPipeTypeStatus(param: SeweragePipeInputPipeTypeStatusSubmitRequest): Promise { + const response = await stClient.post( + `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/pipe-type-status/submit`, + param, + ); + return response.data; + } + public async savePipeDiameterStatus(param: SeweragePipeInputPipeDiameterStatusSaveRequest): Promise { const response = await stClient.post( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/pipe-diameter-status/save`, @@ -142,6 +164,14 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS return response.data; } + public async submitPipeDiameterStatus(param: SeweragePipeInputPipeDiameterStatusSubmitRequest): Promise { + const response = await stClient.post( + `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/pipe-diameter-status/submit`, + param, + ); + return response.data; + } + public async saveManholeStatus(param: SeweragePipeInputManholeStatusSaveRequest): Promise { const response = await stClient.post( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/manhole-status/save`, @@ -149,6 +179,14 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS ); return response.data; } + + public async submitManholeStatus(param: SeweragePipeInputManholeStatusSubmitRequest): Promise { + const response = await stClient.post( + `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/manhole-status/submit`, + param, + ); + return response.data; + } }; export const statisticsInputSewPipeApiService = new StatisticsInputSewPipeApiService(); \ No newline at end of file diff --git a/apps/st/src/entities/statistics/api/notice-talk/notice-talk-sending-service.ts b/apps/st/src/entities/statistics/api/notice-talk/notice-talk-sending-service.ts new file mode 100644 index 000000000..476c8fecd --- /dev/null +++ b/apps/st/src/entities/statistics/api/notice-talk/notice-talk-sending-service.ts @@ -0,0 +1,43 @@ +import { client } from "@/shared/api/client"; +import { ENDPOINT } from "@/shared/config"; +import type { PageableList } from "@/shared/model"; + +import type { + NtftalkTemplate, + TemplateVariablesResponse, +} from "../../model/notice-talk/notice-talk-sending-interface"; + +export const NTFTALK_TEMPLATE_LIST_PARAMS = { + sort: "createdAt,desc", + senderKey: "", + inspectionStatus: "APR", + templateMessageType: "BA", + page: 0, + size: 10, +}; + +/** + * 알림톡 발송 화면용 Ntftalk API + */ +export class NoticeTalkSendingApiService { + async getTemplatePage( + params: typeof NTFTALK_TEMPLATE_LIST_PARAMS = NTFTALK_TEMPLATE_LIST_PARAMS, + ): Promise> { + const response = await client.get>( + ENDPOINT.NTFTALK_SERVICE.TEMP, + { params }, + ); + return response.data; + } + + async getTemplateVariables( + templateCode: string, + ): Promise { + const response = await client.get( + `${ENDPOINT.NTFTALK_SERVICE.ATA_SEND}/template-variables/${templateCode}`, + ); + return response.data; + } +} + +export const noticeTalkSendingApi = new NoticeTalkSendingApiService(); diff --git a/apps/st/src/entities/statistics/api/notice-talk/queries.ts b/apps/st/src/entities/statistics/api/notice-talk/queries.ts new file mode 100644 index 000000000..20d28314d --- /dev/null +++ b/apps/st/src/entities/statistics/api/notice-talk/queries.ts @@ -0,0 +1,33 @@ +import { useQuery } from "@tanstack/react-query"; + +import { ENDPOINT } from "@/shared/config"; + +import { + noticeTalkSendingApi, + NTFTALK_TEMPLATE_LIST_PARAMS, +} from "./notice-talk-sending-service"; + +export function useNtftalkTemplateList() { + return useQuery({ + queryKey: [ + ENDPOINT.NTFTALK_SERVICE.TEMP, + "ntitlk-sndng", + NTFTALK_TEMPLATE_LIST_PARAMS, + ], + queryFn: () => + noticeTalkSendingApi.getTemplatePage(NTFTALK_TEMPLATE_LIST_PARAMS), + }); +} + +export function useNtftalkTemplateVariables(templateCode: string) { + return useQuery({ + queryKey: [ + ENDPOINT.NTFTALK_SERVICE.ATA_SEND, + "template-variables", + templateCode, + ], + queryFn: () => + noticeTalkSendingApi.getTemplateVariables(templateCode), + enabled: Boolean(templateCode), + }); +} diff --git a/apps/st/src/entities/statistics/api/queries.ts b/apps/st/src/entities/statistics/api/queries.ts index 30a6437ca..ff392a235 100644 --- a/apps/st/src/entities/statistics/api/queries.ts +++ b/apps/st/src/entities/statistics/api/queries.ts @@ -1,18 +1,18 @@ import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import { ENDPOINT } from "@/shared/config"; -import { generalverificationApiService } from "@/entities/statistics/api/general-verification-service"; -import { StatisticsInputStatusApi } from "@/entities/statistics/api/input/statistics-input-status-service"; -import type { VerifyResultPageData } from "@/entities/statistics/model/general-verification-interface"; + +import type { VerifyResultPageData } from "../model/general-verification-interface"; +import type { SewerStUserSearchParams } from "../model/sewer-st-user-interface"; import type { StatisticsVerificationFilterState, StatisticsVerificationSubFilterState, -} from "@/entities/statistics/model/statistics-verification-report/statistics-verification-report-types"; - -import type { SewerStUserSearchParams } from "../model/sewer-st-user-interface"; +} from "../model/statistics-verification-report/statistics-verification-report-types"; +import { StatisticsInputStatusApi } from "./input/statistics-input-status-service"; +import { generalverificationApiService } from "./general-verification-service"; import { sewerStUserApi } from "./sewer-st-user-service"; -import { stAnlsMstApi } from "./statistics-analysis-master-api"; +import { statisticsAnalysisMasterService } from "./statistics-analysis-master-service"; import { stAnlsRsltApi } from "./statistics-analysis-result-api"; const DETAIL_PAGE_SIZE = 100; @@ -20,14 +20,14 @@ const DETAIL_PAGE_SIZE = 100; export const useStAnlsSidoCombo = () => { return useQuery({ queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST, "sido"], - queryFn: () => stAnlsMstApi.getSidoCombo(), + queryFn: () => statisticsAnalysisMasterService.getSidoCombo(), }); }; export const useStAnlsSigunguCombo = (sidoCd: string) => { return useQuery({ queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST, "sigungu", sidoCd], - queryFn: () => stAnlsMstApi.getSigunguCombo(sidoCd), + queryFn: () => statisticsAnalysisMasterService.getSigunguCombo(sidoCd), enabled: Boolean(sidoCd), }); }; diff --git a/apps/st/src/entities/statistics/api/statistics-analysis-detail-api.ts b/apps/st/src/entities/statistics/api/statistics-analysis-detail-api.ts deleted file mode 100644 index 957680ec2..000000000 --- a/apps/st/src/entities/statistics/api/statistics-analysis-detail-api.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { client } from "@/shared/api"; -import { ENDPOINT } from "@/shared/config"; - -import type { - StAnlsDtlListApiResponse, - StAnlsDtlResponse, - StAnlsDtlSearchParams, -} from "../model/statistics-analysis/statistics-analysis-detail-interface"; - -export class StAnlsDtlApiService { - public async getDetailList( - params: StAnlsDtlSearchParams, - ): Promise { - const response = await client.get( - `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_DTL}`, - { - params, - }, - ); - - return response.data; - } - - public async getDetail( - anlsSn: number, - vrfcTrgtSn: number, - ): Promise { - const response = await client.get( - `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_DTL}/${anlsSn}/${vrfcTrgtSn}`, - ); - - return response.data; - } -} - -export const stAnlsDtlApi = new StAnlsDtlApiService(); diff --git a/apps/st/src/entities/statistics/api/statistics-analysis-detail-service.ts b/apps/st/src/entities/statistics/api/statistics-analysis-detail-service.ts new file mode 100644 index 000000000..5d47c6925 --- /dev/null +++ b/apps/st/src/entities/statistics/api/statistics-analysis-detail-service.ts @@ -0,0 +1,51 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; + +import type { + StAnlsDtlConfirmRequest, + StAnlsDtlConfirmResponse, + StAnlsDtlListApiResponse, + StAnlsDtlResponse, + StAnlsDtlSearchParams, +} from "../model/statistics-analysis/statistics-analysis-detail-interface"; + +async function getDetailList( + params: StAnlsDtlSearchParams, +): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_DTL}`, + { + params, + }, + ); + + return response.data; +} + +async function getDetail( + anlsSn: number, + vrfcTrgtSn: number, +): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_DTL}/${anlsSn}/${vrfcTrgtSn}`, + ); + + return response.data; +} + +async function confirmDetail( + request: StAnlsDtlConfirmRequest, +): Promise { + const response = await client.post( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_DTL}/confirm`, + request, + ); + + return response.data; +} + +export const statisticsAnalysisDetailService = { + getDetailList, + getDetail, + confirmDetail, +}; diff --git a/apps/st/src/entities/statistics/api/statistics-analysis-master-api.ts b/apps/st/src/entities/statistics/api/statistics-analysis-master-api.ts deleted file mode 100644 index dfe951e53..000000000 --- a/apps/st/src/entities/statistics/api/statistics-analysis-master-api.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { client } from "@/shared/api"; -import { ENDPOINT } from "@/shared/config"; - -import type { - AdstZoneResponseDto, - StAnlsMstImrListApiResponse, - StAnlsMstListApiResponse, - StAnlsMstSearchParams, - StAnlsOutlierListResponse, -} from "../model/statistics-analysis/statistics-analysis-master-interface"; - -export class StAnlsMstApiService { - public async getAnalysisHistoryList( - params: StAnlsMstSearchParams, - ): Promise { - const response = await client.get( - `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}`, - { - params, - }, - ); - - return response.data; - } - - public async getImrAnalysisMasterList( - params: StAnlsMstSearchParams, - ): Promise { - const response = await client.get( - `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/imr`, - { - params, - }, - ); - - return response.data; - } - - public async getOutlierList( - anlsSn: number, - ): Promise { - const response = await client.get( - `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/outlierList/${anlsSn}`, - ); - - return response.data; - } - - public async getSidoCombo(): Promise { - const response = await client.get( - `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/sido`, - ); - - return response.data; - } - - public async getSigunguCombo( - sidoCd: string, - ): Promise { - const response = await client.get( - `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/sigungu`, - { params: { sidoCd } }, - ); - - return response.data; - } -} - -export const stAnlsMstApi = new StAnlsMstApiService(); diff --git a/apps/st/src/entities/statistics/api/statistics-analysis-master-service.ts b/apps/st/src/entities/statistics/api/statistics-analysis-master-service.ts new file mode 100644 index 000000000..a9e9d7918 --- /dev/null +++ b/apps/st/src/entities/statistics/api/statistics-analysis-master-service.ts @@ -0,0 +1,71 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; + +import type { + AdstZoneResponseDto, + StAnlsMstImrListApiResponse, + StAnlsMstListApiResponse, + StAnlsMstSearchParams, + StAnlsOutlierListResponse, +} from "../model/statistics-analysis/statistics-analysis-master-interface"; + +async function getAnalysisHistoryList( + params: StAnlsMstSearchParams, +): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}`, + { + params, + }, + ); + + return response.data; +} + +async function getImrAnalysisMasterList( + params: StAnlsMstSearchParams, +): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/imr`, + { + params, + }, + ); + + return response.data; +} + +async function getOutlierList( + anlsSn: number, +): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/outlierList/${anlsSn}`, + ); + + return response.data; +} + +async function getSidoCombo(): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/sido`, + ); + + return response.data; +} + +async function getSigunguCombo(sidoCd: string): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/sigungu`, + { params: { sidoCd } }, + ); + + return response.data; +} + +export const statisticsAnalysisMasterService = { + getAnalysisHistoryList, + getImrAnalysisMasterList, + getOutlierList, + getSidoCombo, + getSigunguCombo, +}; diff --git a/apps/st/src/entities/statistics/index.ts b/apps/st/src/entities/statistics/index.ts index 28a485e76..159b0eb76 100644 --- a/apps/st/src/entities/statistics/index.ts +++ b/apps/st/src/entities/statistics/index.ts @@ -1,3 +1,4 @@ +export { analysisApi } from "./api/analysis-api"; export { useDashboardEcptReqCount, useDashboardStatsSubmissionRate, @@ -8,6 +9,12 @@ export { export { dashboardApi } from "./api/dashboard/dashboard-service"; export { useScheduleInfo } from "./api/dashboard/schedule-info-queries"; export { scheduleInfoApi } from "./api/dashboard/schedule-info-service"; +export { imrApi } from "./api/imr-api"; +export { noticeTalkSendingApi } from "./api/notice-talk/notice-talk-sending-service"; +export { + useNtftalkTemplateList, + useNtftalkTemplateVariables, +} from "./api/notice-talk/queries"; export { useSewerStUserList, useStAnlsSidoCombo, @@ -16,7 +23,11 @@ export { useStatsVrfcRsltData, } from "./api/queries"; export { sewerStUserApi } from "./api/sewer-st-user-service"; +export { statisticsAnalysisDetailService } from "./api/statistics-analysis-detail-service"; +export { statisticsAnalysisMasterService } from "./api/statistics-analysis-master-service"; +export { stAnlsRsltApi } from "./api/statistics-analysis-result-api"; export { statisticsApprovalApi } from "./api/statistics-approval-service"; +export { verificationTargetManagementApi } from "./api/verification-target-management-service"; export { type AnalysisHistoryListApiResponse, type AnalysisRequestPayload, @@ -64,12 +75,22 @@ export { } from "./model/imr/imr-interface"; export { imrVerificationHistoryColumns } from "./model/imr/imr-verification-history-columns"; export { type StatsInputStatusList } from "./model/input/statistics-input-status-interface"; +export { userListColumns } from "./model/notice-talk/notice-talk-sending-columns"; +export { + type BusinessSendRequest, + type BusinessSendResponse, + type NtftalkTemplate, + type SelectedRecipient, + type TemplateVariablesResponse, +} from "./model/notice-talk/notice-talk-sending-interface"; export { type SewerStUserItem, type SewerStUserListResponse, type SewerStUserSearchParams, } from "./model/sewer-st-user-interface"; export { + type StAnlsDtlConfirmRequest, + type StAnlsDtlConfirmResponse, type StAnlsDtlListApiResponse, type StAnlsDtlResponse, type StAnlsDtlSearchParams, @@ -121,3 +142,4 @@ export { type IMRArticleItem, type ValidationArticleItem, } from "./model/verification-article/verification-article-interface"; +export { type VerificationTargetCodeResponse } from "./model/verification-target-management-interface"; 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 new file mode 100644 index 000000000..19c86e20c --- /dev/null +++ b/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts @@ -0,0 +1,254 @@ +import type { SewerageStatusInfo } from "../statistics-interface"; + +export interface PublicSewageFacilityRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + fcltNm?: string; + statsYr?: string; + mode?: string; +} + +export interface PublicSewageFacilityVerifyResultItem { + vrfcCd: string; + rsnExpln: string; + nowVlCn?: string; + compVlCn?: string; +} + +/** 필드명 -> 검증 결과 배열 */ +export type PublicSewageFacilityVerificationResult = Record< + string, + PublicSewageFacilityVerifyResultItem[] +>; + +export interface PublicSewageFacilityCommonInfo { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: PublicSewageFacilityVerificationResult | null; +} + +export interface PublicSewageFacilityItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + fcltSn: number; + swtrmtFcltCd: string; + swtrmtFcltNm: string; + impdFcltPstnCn: string | null; + prcsFcltCpct: number; + prcsFcltPhysCpct: number; + prcsFcltBoilCpct: number; + prcsFcltAltdCpct: number; + inflwSwageQnt: number; + inflwSwagePhysQnt: number; + inflwSwageBoilQnt: number; + inflwSwageAltdQnt: number; + prcsQnt: number; + prcsPhysCpct: number; + prcsBoilCpct: number; + prcsAltdCpct: number; + inflwBodVl: number; + dschgBodVl: number; + inflwTocQnt: number; + dschgTocQnt: number; + inflwSsQnt: number; + dschgSsQnt: number; + inflwTnVl: number; + dschgTnVl: number; + inflwTpVl: number; + dschgTpVl: number; + inflwGrssClfgVl: number; + dschgGrssClfgVl: number; + prcsRt: number; + prcsLodQnt: number; + prcsMthdExpln: string | null; + wholHmnQnt: number; + wholLvstQnt: number; + wholEfntQnt: number; + wholEtcQnt: number; + telNo: string | null; + cmptnStrtYmd: string | null; + oprtngStrtYmd: string | null; + wholBizVl: number; + eflntDsnfMthdExpln: string | null; + wtsysNm: string | null; + trbtrNm: string | null; + dtlUnitZoneCn: string | null; + rgnSeNm: string | null; + msirbCn: string | null; + goalWqNo: string | null; + engySrlneRt: number; + engyItslfPrdctnQnt: number; + anlEngyWholUseQnt: number; + anlElpwrUseQnt: number; + itslfEngyUseQnt: number; + swtrmtEmsnQnt: number; + operMnbdCn: string | null; + prxyBzentyNmCn: string | null; + cnsgnCstVl: number; + prxyCtrtCn: string | null; + intgPrcsFcltCn: string | null; + wholNope: number; + sftyAcdntEvntcsCnt: number; + rmrk: string | null; + verificationResult?: PublicSewageFacilityVerificationResult | null; +} + +export interface PublicSewageFacilityResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: PublicSewageFacilityCommonInfo; + publicSewageFacilityList: PublicSewageFacilityItem[]; +} + +export interface PublicSewageFacilityResponse { + status: string; + message?: string; + data: PublicSewageFacilityResponseData; +} + +/** ST00060002 지자체별 전체시설 목록 항목 */ +export interface PublicSewageMainFacilityItem { + admdstCd: string; + sggNm: string | null; + admdstNm: string; + isMainYn: string; + fcltSn: string; + swtrmtFcltCd: string; + swtrmtFcltNm: string; + fcltPstnCn: string | null; + prxyBzentyNmCn: string | null; + cnsgnCstVl: number; + prxyCtrtCn: string | null; + wholNope: number; +} + +/** ST00060002 조회 응답 data */ +export interface PublicSewageMainFacilityResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: PublicSewageFacilityCommonInfo; + mainFacilityList: PublicSewageMainFacilityItem[]; +} + +/** ST00060002 조회 API 응답 */ +export interface PublicSewageMainFacilityResponse { + status: string; + message?: string; + 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; +} diff --git a/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts index 9a3db3ea1..d6357d78f 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts @@ -122,8 +122,8 @@ export interface SeweragePipeInputRegionalFactStatusSaveResponse { status: string; } -/** ST00020001 지역별 시설현황 제출 API 요청 (`submitList` 행은 저장과 동일) */ -export interface SeweragePipeInputRegionalFactStatusSubmitRequest { +/** 하수관로 입력 제출 공통 본문 (`submitList` 제외) */ +export interface SeweragePipeInputSubmitRequestBase { statsYr: string; ctpvNm: string; sggNm: string; @@ -137,6 +137,10 @@ export interface SeweragePipeInputRegionalFactStatusSubmitRequest { laeIdfrTelNo: string; laeIdfrDeptNm: string; laeIdfrJbgdNm: string; +} + +/** ST00020001 지역별 시설현황 제출 API 요청 (`submitList` 행은 저장과 동일) */ +export interface SeweragePipeInputRegionalFactStatusSubmitRequest extends SeweragePipeInputSubmitRequestBase { submitList: SeweragePipeInputRegionalFactStatusSaveItem[]; } @@ -165,6 +169,30 @@ export interface SeweragePipeInputFactInstallStatusSaveResponse { status: string; } +/** ST00020002 시설별 설치현황 제출 API 요청 (`submitList` 행은 Tab2 저장과 동일) */ +export interface SeweragePipeInputFactInstallStatusSubmitRequest extends SeweragePipeInputSubmitRequestBase { + submitList: SeweragePipeInputFactInstallStatusSaveItem[]; +} + +export interface SeweragePipeInputFactInstallStatusSubmitResponse { + data: { + finalStatus: string; + nvpassed: boolean; + } + message?: string; + status: string; +} + +/** 하수관로 입력 제출 API 공통 응답 */ +export interface SeweragePipeInputSubmitResponse { + data: { + finalStatus: string; + nvpassed: boolean; + } + message?: string; + status: string; +} + /** 시설별 설치 현황 — 목록 항목 */ export interface SeweragePipeInputFactInstallStatusItem { admdstCd: string; @@ -283,6 +311,11 @@ export interface SeweragePipeInputPipeTypeStatusSaveResponse { status: string; } +/** ST00020003 관종별 현황 제출 API 요청 */ +export interface SeweragePipeInputPipeTypeStatusSubmitRequest extends SeweragePipeInputSubmitRequestBase { + submitList: SeweragePipeInputPipeTypeStatusSaveItem[]; +} + /** 관경별 현황 — 목록 항목 */ export interface SeweragePipeInputPipeDiameterStatusItem { admdstCd: string; @@ -350,6 +383,11 @@ export interface SeweragePipeInputPipeDiameterStatusSaveResponse { status: string; } +/** ST00020004 관경별 현황 제출 API 요청 */ +export interface SeweragePipeInputPipeDiameterStatusSubmitRequest extends SeweragePipeInputSubmitRequestBase { + submitList: SeweragePipeInputPipeDiameterStatusSaveItem[]; +} + /** 맨홀 현황 — 연도별 통계 항목 */ export interface SeweragePipeInputManholeYearlyStatItem { manhYr: string; @@ -443,6 +481,11 @@ export interface SeweragePipeInputManholeStatusSaveResponse { status: string; } +/** ST00020005 맨홀 현황 제출 API 요청 */ +export interface SeweragePipeInputManholeStatusSubmitRequest extends SeweragePipeInputSubmitRequestBase { + submitList: SeweragePipeInputManholeStatusSaveItem[]; +} + /** 시설별 현황 관리 - 하수처리장별 연결 현황 목록 항목 */ export interface SeweragePipeInputFacilityConnectionItem { fcltNm: string; @@ -571,7 +614,11 @@ export interface StatisticsInputSewPipeService { saveRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSaveRequest): Promise; submitRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSubmitRequest): Promise; saveFactInstallStatus(param: SeweragePipeInputFactInstallStatusSaveRequest): Promise; + submitFactInstallStatus(param: SeweragePipeInputFactInstallStatusSubmitRequest): Promise; savePipeTypeStatus(param: SeweragePipeInputPipeTypeStatusSaveRequest): Promise; + submitPipeTypeStatus(param: SeweragePipeInputPipeTypeStatusSubmitRequest): Promise; savePipeDiameterStatus(param: SeweragePipeInputPipeDiameterStatusSaveRequest): Promise; + submitPipeDiameterStatus(param: SeweragePipeInputPipeDiameterStatusSubmitRequest): Promise; saveManholeStatus(param: SeweragePipeInputManholeStatusSaveRequest): Promise; + submitManholeStatus(param: SeweragePipeInputManholeStatusSubmitRequest): Promise; } \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/notice-talk/notice-talk-sending-columns.tsx b/apps/st/src/entities/statistics/model/notice-talk/notice-talk-sending-columns.tsx new file mode 100644 index 000000000..03a6663f8 --- /dev/null +++ b/apps/st/src/entities/statistics/model/notice-talk/notice-talk-sending-columns.tsx @@ -0,0 +1,74 @@ +import type { ColumnDef } from "@tanstack/react-table"; + +import { Checkbox } from "@common/components"; + +import type { SewerStUserItem } from "../sewer-st-user-interface"; + +export const userListColumns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( +
e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + role="presentation" + > + + table.toggleAllPageRowsSelected(!!value) + } + aria-label="전체 선택" + /> +
+ ), + cell: ({ row }) => ( +
e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + role="presentation" + > + row.toggleSelected(!!value)} + aria-label="행 선택" + /> +
+ ), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: "userNm", + header: "이름", + }, + { + accessorKey: "sido", + header: "시도", + }, + { + accessorKey: "gugun", + header: "시군구", + }, + { + accessorKey: "gubunNm", + header: "구분", + }, + { + accessorKey: "deptNm", + header: "소속", + }, + { + accessorKey: "jbgdNm", + header: "직급", + }, + { + accessorKey: "telNo", + header: "전화번호", + }, +]; diff --git a/apps/st/src/entities/statistics/model/notice-talk/notice-talk-sending-interface.ts b/apps/st/src/entities/statistics/model/notice-talk/notice-talk-sending-interface.ts new file mode 100644 index 000000000..e7cf89b58 --- /dev/null +++ b/apps/st/src/entities/statistics/model/notice-talk/notice-talk-sending-interface.ts @@ -0,0 +1,30 @@ +import type { SewerStUserItem } from "../sewer-st-user-interface"; + +export interface NtftalkTemplate { + templateCode: string; + templateName: string; + templateContent: string; +} + +export interface TemplateVariablesResponse { + templateCode: string; + variables: string[]; + count: number; +} + +export interface BusinessSendRequest { + templateCode: string; + callback?: string; + fixedRecipientNum?: string; + recipients: Record[]; +} + +export interface BusinessSendResponse { + success: boolean; + sentCount: number; + message: string; +} + +export interface SelectedRecipient extends SewerStUserItem { + variableOverrides: Record; +} diff --git a/apps/st/src/entities/statistics/model/statistics-analysis/statistics-analysis-detail-interface.ts b/apps/st/src/entities/statistics/model/statistics-analysis/statistics-analysis-detail-interface.ts index af0919b87..81d9c5840 100644 --- a/apps/st/src/entities/statistics/model/statistics-analysis/statistics-analysis-detail-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-analysis/statistics-analysis-detail-interface.ts @@ -18,6 +18,8 @@ export interface StAnlsDtlResponse { nmlDataNocs: number; // 정상 데이터 건수 exclDataNocs: number; // 제외 데이터 건수 otlrDataNocs: number; // 이상치 데이터 건수 + cfmtnYn: string; // 확정여부 (Y/N) + cfmtnDt: string | null; // 확정일자 (yyyy-MM-dd HH:mm:ss) statsCd: string; // 통계 코드 statsNm: string; // 통계 항목 명 vrfcTrgtNm: string; // 검증대상명 @@ -30,3 +32,13 @@ export interface StAnlsDtlSearchParams extends PageableParams { } export type StAnlsDtlListApiResponse = PageableList; + +export interface StAnlsDtlConfirmRequest { + anlsSn: number; // 분석 일련번호 + vrfcTrgtSnList: number[]; // 검증 대상 일련번호 목록 +} + +export interface StAnlsDtlConfirmResponse { + anlsSn: number; // 분석 일련번호 + processedCount: number; // 처리된 검증 대상 건수 +} diff --git a/apps/st/src/entities/statistics/model/statistics-analysis/statistics-analysis-master-interface.ts b/apps/st/src/entities/statistics/model/statistics-analysis/statistics-analysis-master-interface.ts index 1129c14a8..1e3e6836f 100644 --- a/apps/st/src/entities/statistics/model/statistics-analysis/statistics-analysis-master-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-analysis/statistics-analysis-master-interface.ts @@ -45,13 +45,15 @@ export interface StAnlsMstResponseDto { /** 검증대상명 */ vrfcTrgtNm: string; /** 분석진행비율 */ - anlsPrgrsRt: number; + anlsPrgrsRt: number | null; /** 하수도사업자코드 */ sewerBzmnCd: string; /** 통계코드 */ statsCd: string; /** 검증방법코드 */ vrfcMthdCd: string; + /** 확정건수/총건수 (예: 3/10) */ + cfmtnYnCntTotalCnt: string; } /** @@ -132,7 +134,6 @@ export interface StAnlsOutlierListResponseDto { outlierDataList: StAnlsOutlierDataResponseDto[]; } -/** 기존 인터페이스 호환을 위한 별칭 (필요 시 유지) */ export type StAnlsMstResponse = StAnlsMstResponseDto; export type StAnlsMstImrResponse = StAnlsMstImrResponseDto; export type StAnlsMstSearchParams = StAnlsMstSearchDto; diff --git a/apps/st/src/entities/statistics/model/statistics-analysis/verification-history-columns.tsx b/apps/st/src/entities/statistics/model/statistics-analysis/verification-history-columns.tsx index edd85652e..d83e415db 100644 --- a/apps/st/src/entities/statistics/model/statistics-analysis/verification-history-columns.tsx +++ b/apps/st/src/entities/statistics/model/statistics-analysis/verification-history-columns.tsx @@ -77,6 +77,12 @@ export const verificationHistoryColumns = [
{info.getValue() ?? "-"}
), }), + columnHelper.accessor("cfmtnYnCntTotalCnt", { + header: "확정건수/총", + cell: (info) => ( +
{info.getValue() ?? "-"}
+ ), + }), columnHelper.accessor("nmlDataNocs", { header: "정상건수", cell: (info) => ( diff --git a/apps/st/src/entities/statistics/model/statistics-analysis/verification-list-columns.tsx b/apps/st/src/entities/statistics/model/statistics-analysis/verification-list-columns.tsx index 05d0ddae5..bdea4cca9 100644 --- a/apps/st/src/entities/statistics/model/statistics-analysis/verification-list-columns.tsx +++ b/apps/st/src/entities/statistics/model/statistics-analysis/verification-list-columns.tsx @@ -1,6 +1,7 @@ import React from "react"; import type { ColumnDef } from "@tanstack/react-table"; +import dayjs from "dayjs"; import { Checkbox } from "@common/components"; @@ -16,94 +17,123 @@ export const getVerificationListColumns = ({ rowData, checkedSns, onCheck, -}: GetVerificationListColumnsParams): ColumnDef[] => [ - { - id: "checkbox", - header: () => { - const isAllChecked = - rowData.length > 0 && checkedSns.length === rowData.length; - return ( -
- { - if (value) { - onCheck( - rowData.map((r) => String(r.vrfcTrgtSn)), - rowData, +}: GetVerificationListColumnsParams): ColumnDef[] => { + const selectableRows = rowData.filter((r) => r.cfmtnYn !== "Y"); + + return [ + { + id: "checkbox", + header: () => { + const isAllChecked = + selectableRows.length > 0 && + checkedSns.length === selectableRows.length; + return ( +
+ { + if (value) { + onCheck( + selectableRows.map((r) => + String(r.vrfcTrgtSn), + ), + selectableRows, + ); + } else { + onCheck([], []); + } + }} + aria-label="Select all rows" + /> +
+ ); + }, + cell: ({ row }) => { + if (row.original.cfmtnYn === "Y") { + return
; + } + const rowId = String(row.original.vrfcTrgtSn); + const isChecked = checkedSns.includes(rowId); + return ( +
+ { + const newCheckedSns = value + ? [...checkedSns, rowId] + : checkedSns.filter((id) => id !== rowId); + const newCheckedRows = selectableRows.filter( + (r) => + newCheckedSns.includes( + String(r.vrfcTrgtSn), + ), ); - } else { - onCheck([], []); + onCheck(newCheckedSns, newCheckedRows); + }} + onClick={(e: React.MouseEvent) => + e.stopPropagation() } - }} - aria-label="Select all rows" - /> -
- ); + aria-label={`Select row ${rowId}`} + /> +
+ ); + }, + enableSorting: false, + enableHiding: false, }, - cell: ({ row }) => { - const rowId = String(row.original.vrfcTrgtSn); - const isChecked = checkedSns.includes(rowId); - return ( -
- { - const newCheckedSns = value - ? [...checkedSns, rowId] - : checkedSns.filter((id) => id !== rowId); - const newCheckedRows = rowData.filter((r) => - newCheckedSns.includes(String(r.vrfcTrgtSn)), - ); - onCheck(newCheckedSns, newCheckedRows); - }} - onClick={(e: React.MouseEvent) => e.stopPropagation()} - aria-label={`Select row ${rowId}`} - /> -
- ); + { + header: "통계 년도", + accessorKey: "statsYr", }, - enableSorting: false, - enableHiding: false, - }, - { - header: "통계 년도", - accessorKey: "statsYr", - }, - { - header: "통계 항목", - accessorKey: "statsNm", - }, - { - header: "검증 항목", - accessorKey: "vrfcTrgtNm", - }, - { - header: "μ", - accessorKey: "ipcrCntVl", - }, - { - header: "Percentile", - accessorKey: "pctlCrtrVl", - }, - { - header: "A", - accessorKey: "scopAjmtCntVl", - }, - { - header: "C", - accessorKey: "thrsCntVl", - }, - { - header: "정상건수", - accessorKey: "nmlDataNocs", - }, - { - header: "제외건수", - accessorKey: "exclDataNocs", - }, - { - header: "이상건수", - accessorKey: "otlrDataNocs", - }, -]; + { + header: "통계 항목", + accessorKey: "statsNm", + }, + { + header: "검증 항목", + accessorKey: "vrfcTrgtNm", + }, + { + header: "확정일자", + accessorKey: "cfmtnDt", + cell: ({ row }) => { + return ( +
+ {row.original.cfmtnDt + ? dayjs(row.original.cfmtnDt).format("YYYY-MM-DD") + : "-"} +
+ ); + }, + }, + { + header: "μ", + accessorKey: "ipcrCntVl", + }, + { + header: "Percentile", + accessorKey: "pctlCrtrVl", + }, + { + header: "A", + accessorKey: "scopAjmtCntVl", + }, + { + header: "C", + accessorKey: "thrsCntVl", + }, + { + header: "정상건수", + accessorKey: "nmlDataNocs", + }, + { + header: "제외건수", + accessorKey: "exclDataNocs", + }, + { + header: "이상건수", + accessorKey: "otlrDataNocs", + }, + ]; +}; diff --git a/apps/st/src/shared/lib/use-mutation-wrapper.tsx b/apps/st/src/shared/lib/use-mutation-wrapper.tsx index cea1a8f83..7b378dc86 100644 --- a/apps/st/src/shared/lib/use-mutation-wrapper.tsx +++ b/apps/st/src/shared/lib/use-mutation-wrapper.tsx @@ -1,29 +1,30 @@ -import { AxiosError } from "axios"; import { useMutation, type UseMutationOptions } from "@tanstack/react-query"; +import { AxiosError } from "axios"; + import { useAlertStore } from "./use-alert-store"; export const useMutationWrapper = < - TData = unknown, - TError = unknown, - TVariables = void, - TContext = unknown, + TData = unknown, + TError = unknown, + TVariables = void, + TContext = unknown, >( - mutationOptions: UseMutationOptions, + mutationOptions: UseMutationOptions, ) => { - const alert = useAlertStore((state) => state.setMessage); - return useMutation({ - ...mutationOptions, - onError: (error) => { - let errorMessage = ""; + const alert = useAlertStore((state) => state.setMessage); + return useMutation({ + ...mutationOptions, + onError: (error) => { + let errorMessage = ""; - if (error instanceof AxiosError) { - if (error.response?.data?.message) - errorMessage += `${error.response.data.message}\n`; - if (error.response?.data?.errors[0]?.message) - errorMessage += `${error.response.data.errors[0].message}\n`; - } + if (error instanceof AxiosError) { + if (error.response?.data?.message) + errorMessage += `${error.response.data.message}\n`; + if (error.response?.data?.errors[0]?.message) + errorMessage += `${error.response.data.errors[0].message}\n`; + } - alert(`${errorMessage}`); - }, - }); + alert(`${errorMessage}`); + }, + }); };