Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev

This commit is contained in:
2026-05-08 13:29:02 +09:00
11 changed files with 780 additions and 73 deletions
+3
View File
@@ -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];
}
@@ -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 = ({
<CompactDataGrid
columnInfo={leftColumnInfo}
fixedColumns={leftFixedColumns}
showHeaderCheckbox={showHeaderCheckbox}
showHeaderCheckbox={false}
enableSort={enableSort}
rowData={leftRowData}
setRowData={setLeftRowData}
onRowClick={leftOnRowClick}
cellStyleCallback={leftCellStyleCallback}
/>
<CompactDataGrid
columnInfo={rightColumnInfo}
@@ -1,41 +1,338 @@
import { useRef, useState } from 'react';
import SearchForm from '@/components/module/SearchForm';
import DataGrid from '@/components/module/DataGrid';
import SplitDataGrid from '@/components/module/SplitDataGrid';
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from '@common/components';
import SubmitButton from '@/components/button/SumitButton';
import SaveButton from '@/components/button/SaveButton';
import OperateDataButton from '@/components/button/OperateDataButton';
import UpdateOperateDataButton from '@/components/button/UpdateOperateDataButton';
import { useConfirm } from '@frontend-opensource/use-react-hooks';
import type { ColumnInfo, ColumnInfoList } from '@/components/type/DataGrid.type';
import type { SearchFormValues } from '@/components/type/SearchForm.type';
import { PublicSewageFactInpColumnList } from '@/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactInpColumn';
import { statisticsInputPublicSewageFacilityApiService } from '@/entities/statistics/api/input/statistics-public-sewage-facility-input-service';
import type { PublicSewageFacilityRequestParams } from '@/entities/statistics/model/input/statistics-public-sewage-facility-input-interface';
import { toGridRows } from '@/components/util/grid/gridFunction';
import { useAlertStore } from '@/shared/lib';
import { statisticsInputSewSupplyRateApi } from '@/entities/statistics/api/input/statistics-input-sew-supply-rate-service';
import { statisticsInputPublicSewageFacilityApiService } from '@/entities/statistics/api/input/statistics-public-sewage-facility-input-service';
import { statisticsApi } from '@/entities/statistics/api/statistics-service';
import type {
PublicSewageFacilityRequestParams,
PublicSewageFacilitySaveItem,
PublicSewageFacilitySaveRequest,
PublicSewageMappingTargetsRequestParams,
} from '@/entities/statistics/model/input/statistics-public-sewage-facility-input-interface';
import {
type CheckInputPeriodItem,
getVerificationPeriodActionBlockedMessage,
isInputPeriodCurrent,
} from '@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface';
import type { StatisticsInputSubmitInfoFormData } from '@/entities/statistics/model/statistics-input-submit-info-schema';
import type { cancelSubmitRequest, ImrStatusResponse, SewerageStatusInfo } from '@/entities/statistics/model/statistics-interface';
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from '@common/components';
import { getUserInfo } from '../../../../../../packages/common/components/src/api';
import SubmitDailog from './SubmitDailog';
import CancelSubmitButton from '@/components/button/CancelSubmitButton';
import NotApplicableCheckButton from '@/components/button/NotApplicableCheckButton';
import SaveButton from '@/components/button/SaveButton';
import SubmitButton from '@/components/button/SumitButton';
import UpdateOperateDataButton from '@/components/button/UpdateOperateDataButton';
import DataGrid from '@/components/module/DataGrid';
import SearchForm from '@/components/module/SearchForm';
import SplitDataGrid from '@/components/module/SplitDataGrid';
import type { CellStyleCallbackParams, ColumnInfo, ColumnInfoList } from '@/components/type/DataGrid.type';
import type { SearchFormValues } from '@/components/type/SearchForm.type';
import { doList } from '@/components/util/column/input/DoList';
import { PublicSewageFactInpColumnList } from '@/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactInpColumn';
import { toGridRows } from '@/components/util/grid/gridFunction';
import { useAdminStore } from '@/store/admin-store';
type TabToolbarSnapshot = {
showSaveInToolbar: boolean;
showEnterableToolbarButtons: boolean;
showActionToolbarCard: boolean;
};
function buildGugunOkForToolbar(values: SearchFormValues | null): boolean {
return doList.includes(values?.bsnmCd || '')
? Boolean(values?.gugun && values.gugun !== 'ALL')
: true;
}
const computeTabEnterable = (
statusInfo: SewerageStatusInfo | null | undefined,
periodEnterable: boolean,
): boolean => {
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<void>((resolve) => setTimeout(resolve, ms));
async function waitForImrJobCompleted(
jobId: string,
getStatus: (id: string) => Promise<ImrStatusResponse>,
options?: {
pollIntervalMs?: number;
maxAttempts?: number;
},
): Promise<void> {
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<boolean>(false);
const [hasSearched, setHasSearched] = useState(false);
const [isNotApplicable, setIsNotApplicable] = useState(false);
const [showIsNotApplicable, setShowIsNotApplicable] = useState(false);
const [searchValues, setSearchValues] = useState<SearchFormValues | null>(null);
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
const [tabStatusInfoMap, setTabStatusInfoMap] = useState<Map<string, SewerageStatusInfo | null>>(new Map());
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<Map<string, TabToolbarSnapshot>>(() => new Map());
const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true);
const [periodCheckList, setPeriodCheckList] = useState<CheckInputPeriodItem[]>([]);
const [showDailog, setShowDailog] = useState<boolean>(false);
const [selectedMainFacilitySn, setSelectedMainFacilitySn] = useState<string | null>(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<string, string | number>[]): PublicSewageFacilitySaveItem[] => {
const toNumber = (v: unknown): number => {
if (v === null || v === undefined) return 0;
if (v === '') return 0;
const num = typeof v === 'string' ? parseFloat(v.replace(/,/g, '')) : (v as number);
return Number.isFinite(num) ? num : 0;
};
const toNullableString = (v: unknown): string | null => {
if (v === null || v === undefined) return null;
const s = typeof v === 'string' ? v : String(v);
return s.trim() === '' ? null : s;
};
return currentData.map((d) => ({
admdstCd: String(d.admdstCd ?? ''),
admdstNm: String(d.admdstNm ?? ''),
swtrmtFcltCd: String(d.swtrmtFcltCd ?? ''),
swtrmtFcltNm: String(d.swtrmtFcltNm ?? ''),
fcltSn: String(d.fcltSn ?? ''),
fcltPstnCn: d.fcltPstnCn === '' ? null : toNullableString(d.fcltPstnCn),
inflwSwagePhysQnt: toNumber(d.inflwSwagePhysQnt),
inflwSwageBoilQnt: toNumber(d.inflwSwageBoilQnt),
inflwSwageAltdQnt: toNumber(d.inflwSwageAltdQnt),
prcsPhysCpct: toNumber(d.prcsPhysCpct),
prcsBoilCpct: toNumber(d.prcsBoilCpct),
prcsAltdCpct: toNumber(d.prcsAltdCpct),
prxyCtrtCn: d.prxyCtrtCn === '' ? null : toNullableString(d.prxyCtrtCn),
sftyAcdntEvntcsCnt: toNumber(d.sftyAcdntEvntcsCnt),
rmrk: toNullableString(d.rmrk),
}));
};
const getNoApplicableForRequest = (): boolean => {
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
const searchContextForToolbar = lastSearchedValues ?? searchValues;
const gugunOk = buildGugunOkForToolbar(searchContextForToolbar);
const enterable = computeTabEnterable(activeTabStatus, inputPeriodEnterable);
const showEnterableToolbarButtons = enterable && gugunOk;
const noApplicableVisible = showEnterableToolbarButtons && showIsNotApplicable;
return noApplicableVisible ? Boolean(isNotApplicable) : false;
};
const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
if (verificationBlocked) {
alert(verificationBlocked);
return;
}
if (activeTab !== 'ST00060001') {
alert('ST00060002 제출 API 연동 전입니다.');
return;
}
if (!lastSearchedValues || !searchParamRef.current) {
alert('먼저 조회를 진행해주세요.');
return;
}
const currentData = rowDataMap.get('ST00060001') ?? [];
if (!currentData || currentData.length === 0) {
alert('제출할 데이터가 없습니다.');
return;
}
const userInfo = await getUserInfo();
const submitList = buildFacilitySaveList(currentData);
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
const statsCdForTab = tabStatsCdMap.get(activeTab) ?? activeTab;
const requestData = {
statsYr: lastSearchedValues.year ?? '',
ctpvNm: lastSearchedValues.sido ?? '',
sggNm: lastSearchedValues.gugun === 'ALL' ? '' : lastSearchedValues.gugun ?? '',
sewerBzmnCd: lastSearchedValues.bsnmCd ?? '',
userId: userInfo.userUnqId,
noApplicable: getNoApplicableForRequest(),
laePicNm: submitInfo.laePicNm,
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
laePicTelNo: submitInfo.laePicTelNo,
laeIdfrNm: submitInfo.laeIdfrNm,
laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm,
laeIdfrDeptNm: submitInfo.laeIdfrDeptNm,
laeIdfrTelNo: submitInfo.laeIdfrTelNo,
submitList,
};
try {
setIsLoading(true);
const response = await statisticsInputPublicSewageFacilityApiService.submitPublicSewageFacility(requestData);
if (response.status === 'success') {
if (response.data?.finalStatus === '5') {
const imrParam = {
user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? '',
stats_cd: statsCdForTab,
stats_yr: lastSearchedValues.year ?? '',
lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === '4' ? '4' : '2',
gen_verify_stts_cd: response.data?.nvpassed ? '1' : '2',
};
try {
const imrResponse = await statisticsApi.callImrService(imrParam);
if (imrResponse.job_id) {
await waitForImrJobCompleted(
imrResponse.job_id,
(id) => statisticsApi.getImrStatus(id),
);
}
alert('제출되었습니다.');
} catch {
alert('제출은 완료되었으나 IMR 검증 처리 중 오류가 발생했습니다.');
}
} else {
alert(response.message || '제출 처리 결과를 확인해주세요.');
}
await loadPublicSewageFacilityActiveTabData(
'ST00060001',
searchParamRef.current,
lastSearchedValues,
);
} else {
alert(response.message || '데이터 제출 중 오류가 발생했습니다.');
}
} finally {
setIsLoading(false);
}
};
const saveFunc = () => {
console.log('save');
// 저장 로직
};
void (async () => {
if (activeTab !== 'ST00060001') {
alert('ST00060002 저장 API 연동 전입니다.');
return;
}
const getOperateDataFunc = () => {
console.log('get operate data');
// 운영 데이터 가져오기
if (!lastSearchedValues || !searchParamRef.current) {
alert('먼저 조회를 진행해주세요.');
return;
}
const currentData = rowDataMap.get('ST00060001') ?? [];
if (!currentData || currentData.length === 0) {
alert('저장할 데이터가 없습니다.');
return;
}
if (!(await confirm("저장 하시겠습니까?"))) return;
const userInfo = await getUserInfo();
const saveList = buildFacilitySaveList(currentData);
const param: PublicSewageFacilitySaveRequest = {
statsYr: lastSearchedValues.year ?? '',
ctpvNm: lastSearchedValues.sido ?? '',
sggNm: lastSearchedValues.gugun === 'ALL' ? '' : lastSearchedValues.gugun ?? '',
sewerBzmnCd: lastSearchedValues.bsnmCd ?? '',
userId: userInfo.userUnqId,
superAdmin: isAdminInfo ? 'superAdmin' : '',
noApplicable: getNoApplicableForRequest(),
saveList,
};
try {
setIsLoading(true);
const response = await statisticsInputPublicSewageFacilityApiService.savePublicSewageFacility(param);
if (response.status === 'success') {
alert('저장되었습니다.');
await loadPublicSewageFacilityActiveTabData(
'ST00060001',
searchParamRef.current,
lastSearchedValues,
);
} else {
alert(response.message || '데이터 저장 중 오류가 발생했습니다.');
}
} finally {
setIsLoading(false);
}
})();
};
const updateOperateDataButtonFunc = () => {
@@ -51,6 +348,29 @@ const PublicSewageFacilityInp = () => {
const firstTabKey = PublicSewageFactInpColumnList[0]?.tabKey ?? 'ST00060001';
const searchParamRef = useRef<PublicSewageFacilityRequestParams | null>(null);
const commitToolbarSnapshotForLoadedTab = (
tabKey: string,
statusInfo: SewerageStatusInfo | null,
hasRowData: boolean,
searchContext: SearchFormValues | null,
) => {
setToolbarSnapshotByTab((prev) => {
const next = new Map(prev);
next.set(
tabKey,
computeTabToolbarSnapshot(
statusInfo,
hasRowData,
searchDoneRef.current,
searchContext,
periodEnterableRef.current,
isAdminInfo,
),
);
return next;
});
};
const buildPublicSewageFacilityParams = (values: SearchFormValues): PublicSewageFacilityRequestParams => ({
ctpvNm: values.sido,
sggNm: values.gugun === 'ALL' ? '' : values.gugun,
@@ -71,6 +391,7 @@ const PublicSewageFacilityInp = () => {
const loadPublicSewageFacilityActiveTabData = async (
tabKey: string,
param: PublicSewageFacilityRequestParams,
searchContext: SearchFormValues | null,
): Promise<boolean> => {
try {
if (tabKey === 'ST00060001') {
@@ -79,6 +400,9 @@ const PublicSewageFacilityInp = () => {
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
return false;
}
setSelectedMainFacilitySn(null);
const statusInfo = response.data.statusInfo ?? null;
const statsCd = response.data.commonInfo.statsCd || tabKey;
const commonInfo = response.data.commonInfo;
const list = response.data.publicSewageFacilityList ?? [];
const convertedData = list.map((item) => ({
@@ -86,11 +410,30 @@ const PublicSewageFacilityInp = () => {
...item,
rmrk: item.rmrk ?? null,
}));
setRowDataMap((prev) => {
if (list.length === 0) {
setShowIsNotApplicable(true);
} else {
setShowIsNotApplicable(false);
}
const gridRows = toGridRows(convertedData);
setTabStatusInfoMap((prev) => {
const next = new Map(prev);
next.set('ST00060001', toGridRows(convertedData));
next.set(tabKey, statusInfo);
return next;
});
setTabStatsCdMap((prev) => {
const next = new Map(prev);
next.set(tabKey, statsCd);
return next;
});
setRowDataMap((prev) => {
const next = new Map(prev);
next.set('ST00060001', gridRows);
return next;
});
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
return true;
}
@@ -100,6 +443,9 @@ const PublicSewageFacilityInp = () => {
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
return false;
}
setSelectedMainFacilitySn(null);
const statusInfo = response.data.statusInfo ?? null;
const statsCd = response.data.commonInfo.statsCd || tabKey;
const list = response.data.mainFacilityList ?? [];
const leftRows = list.map((item) => ({
swtrmtFcltNm: item.swtrmtFcltNm,
@@ -115,12 +461,24 @@ const PublicSewageFacilityInp = () => {
sggNm: item.sggNm ?? '',
admdstNm: item.admdstNm,
}));
const leftGridRows = toGridRows(leftRows);
setTabStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(tabKey, statusInfo);
return next;
});
setTabStatsCdMap((prev) => {
const next = new Map(prev);
next.set(tabKey, statsCd);
return next;
});
setRowDataMap((prev) => {
const next = new Map(prev);
next.set('MainFacility', toGridRows(leftRows));
next.set('MainFacility', leftGridRows);
next.set('MainFacilityTarget', []);
return next;
});
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, leftGridRows.length > 0, searchContext);
return true;
}
@@ -137,22 +495,44 @@ const PublicSewageFacilityInp = () => {
return;
}
try {
setIsLoading(true);
const param = buildPublicSewageFacilityParams(values);
const periodResponse = await statisticsInputSewSupplyRateApi.checkInputPeriod(values.year || '');
if (periodResponse.status !== 'success') {
alert(periodResponse.message);
return;
}
const periods = Array.isArray(periodResponse.data) ? periodResponse.data : [];
setPeriodCheckList(periods);
const periodOk = isInputPeriodCurrent(periods);
periodEnterableRef.current = periodOk;
setInputPeriodEnterable(periodOk);
const yearChangedFromLastSearch =
hasSearched &&
(searchParamRef.current?.statsYr ?? '') !== (param.statsYr ?? '');
lastSearchedValues != null &&
(lastSearchedValues.year ?? '') !== (values.year ?? '');
// 조회 시 이전 선택 로우 강조 초기화
setSelectedMainFacilitySn(null);
if (yearChangedFromLastSearch) {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setToolbarSnapshotByTab(new Map());
activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey);
}
searchParamRef.current = param;
setLastSearchedValues(values);
searchDoneRef.current = true;
setHasSearched(true);
setIsLoading(true);
const param = buildPublicSewageFacilityParams(values);
searchParamRef.current = param;
const tabKeyToLoad = yearChangedFromLastSearch ? firstTabKey : activeTabRef.current;
await loadPublicSewageFacilityActiveTabData(tabKeyToLoad, param);
await loadPublicSewageFacilityActiveTabData(tabKeyToLoad, param, values);
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
}
@@ -165,12 +545,76 @@ const PublicSewageFacilityInp = () => {
if (!hasSearched || tabRowDataExists(tabKey) || !searchParamRef.current) return;
try {
setIsLoading(true);
await loadPublicSewageFacilityActiveTabData(tabKey, searchParamRef.current);
await loadPublicSewageFacilityActiveTabData(tabKey, searchParamRef.current, lastSearchedValues);
} finally {
setIsLoading(false);
}
};
const handleReset = () => {
setIsLoading(false);
setHasSearched(false);
setIsNotApplicable(false);
setShowIsNotApplicable(false);
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setToolbarSnapshotByTab(new Map());
setInputPeriodEnterable(true);
periodEnterableRef.current = true;
setPeriodCheckList([]);
searchParamRef.current = null;
searchDoneRef.current = false;
activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey);
setLastSearchedValues(null);
setSelectedMainFacilitySn(null);
};
const cnacelSubmitFunc = async () => {
try {
if (await confirm('제출된 내용을 취소하시겠습니까?')) {
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
if (verificationBlocked) {
alert(verificationBlocked);
return;
}
const userInfo = await getUserInfo();
const statsCdForCancel = tabStatsCdMap.get(activeTab) ?? activeTab;
const requestData: cancelSubmitRequest = {
sewerBzmnCd: lastSearchedValues?.bsnmCd ?? '',
ctpvNm: lastSearchedValues?.sido ?? '',
sggNm: lastSearchedValues?.gugun ?? '',
statsYr: lastSearchedValues?.year ?? '',
mdfrId: userInfo.userUnqId,
statsCd: statsCdForCancel,
};
const response = await statisticsApi.cancelSubmit(requestData);
if (response.status === 'success') {
if (searchParamRef.current) {
await loadPublicSewageFacilityActiveTabData(
activeTab,
searchParamRef.current,
lastSearchedValues,
);
}
alert('제출 취소되었습니다.');
} else {
alert(response.message ?? '제출 취소 중 오류가 발생했습니다.');
}
}
} catch {
alert('제출 취소 중 오류가 발생했습니다.');
}
};
const getGridColumnInfo = (
tab: ColumnInfoList,
): ColumnInfo => ({
@@ -178,18 +622,95 @@ const PublicSewageFacilityInp = () => {
columnLayout: tab.columnLayout ?? [],
});
const handleMainFacilityRowClick = async (rowData: Record<string, string | number>) => {
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 (
<>
<div className="relative">
<SearchForm fctName={true} onSubmit={handleSearch} />
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<UpdateOperateDataButton updateOperateDataButtonFunc={updateOperateDataButtonFunc} />
<OperateDataButton getOperateDataFunc={getOperateDataFunc} />
<SubmitButton submitFunc={submitFunc} />
<SaveButton saveFunc={saveFunc} />
<SearchForm
fctName={true}
onSubmit={handleSearch}
onReset={handleReset}
onValuesChange={setSearchValues}
/>
{showActionToolbarCard && (
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
{showEnterableToolbarButtons && !isMainFacilityTab && (
<>
{showIsNotApplicable && <NotApplicableCheckButton isCheck={isNotApplicable} setIsCheck={setIsNotApplicable} />}
<UpdateOperateDataButton updateOperateDataButtonFunc={updateOperateDataButtonFunc} />
<SubmitButton submitFunc={submitFunc} />
</>
)}
{!isMainFacilityTab && activeTabStatus?.laeSbmsnSttsCd === '2' && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
</div>
</div>
</div>
)}
{isLoading && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
@@ -216,10 +737,22 @@ const PublicSewageFacilityInp = () => {
rightColumnInfo={getGridColumnInfo(tab.right)}
leftFixedColumns={tab.left.fixedColumns}
rightFixedColumns={tab.right.fixedColumns}
showHeaderCheckbox={false}
showHeaderCheckbox={true}
enableSort={false}
leftRowData={rowDataMap.get(leftKey) ?? []}
rightRowData={rowDataMap.get(rightKey) ?? []}
leftOnRowClick={
tab.tabKey === 'ST00060002'
? (row) => {
void handleMainFacilityRowClick(row);
}
: undefined
}
leftCellStyleCallback={
tab.tabKey === 'ST00060002'
? leftMainFacilityCellStyleCallback
: undefined
}
setLeftRowData={(updater) => {
setRowDataMap((prev) => {
const next = new Map(prev);
@@ -256,6 +789,7 @@ const PublicSewageFacilityInp = () => {
columnInfo={gridColumnInfo}
rowStartNum={tab.rowStartNum}
fixedColumns={tab.fixedColumns}
statusInfo={activeTabStatus}
rowData={rowDataMap.get(tab.tabKey) ?? []}
setRowData={(updater) => {
setRowDataMap((prev) => {
@@ -277,6 +811,14 @@ const PublicSewageFacilityInp = () => {
})}
</Tabs>
</div>
<SubmitDailog
open={showDailog}
onOpenChange={setShowDailog}
statusInfo={activeTabStatus}
onConfirm={executeSubmit}
submitMode="normal"
isLoading={isLoading}
/>
</>
);
};
@@ -1756,7 +1756,7 @@ const SewagePipeInp = () => {
/>
{showEnterableToolbarButtons && (
<>
<LastYearDataButton getLastYearDataFunc={() => void getLastYearDataFunc()} />
{activeTab !== 'ST00020002' && <LastYearDataButton getLastYearDataFunc={() => void getLastYearDataFunc()} />}
<SubmitButton submitFunc={() => void submitFunc(false)} />
</>
)}
@@ -295,9 +295,9 @@ const SeweragePenetrationRateInp = () => {
statsCd: statsCd,
};
// if (isAdminInfo) {
// params.superAdmin = 'superAdmin';
// }
if (isAdminInfo) {
params.superAdmin = 'superAdmin';
}
try {
setIsLoading(true);
@@ -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 (
<div className="relative">
<SearchForm isDetail={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" />
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" />
{isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
<div className="relative size-[5rem]">
@@ -172,6 +177,7 @@ const SewagePipeTot = () => {
</div>
</div>
)}
<CellColorInfo />
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<Tabs
value={activeTab}
@@ -221,10 +227,11 @@ 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';
}}
/>
</TabsContent>
@@ -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 처리
@@ -365,6 +365,10 @@ export type SplitDataGridProps = {
setRightRowData: React.Dispatch<
React.SetStateAction<Record<string, string | number>[]>
>;
/** 좌측 분할 그리드 행 클릭 이벤트 */
leftOnRowClick?: (rowData: Record<string, string | number>) => void;
/** 좌측 분할 그리드 셀 스타일 콜백 */
leftCellStyleCallback?: CellStyleCallback;
/** 전체 정렬 기능 on/off (기본값: false) */
enableSort?: boolean;
};
@@ -1,6 +1,7 @@
import { footerInt, footerDecimal1, footerDecimal3 } from "@/components/util/grid/footerSummary";
import { ValueType } from "realgrid";
import { footerDecimal1, footerDecimal3,footerInt } from "@/components/util/grid/footerSummary";
/**
* 하수도 통계 관리 > 06. 공공하수처리시설
*/
@@ -112,6 +113,7 @@ export const PublicSewageFactColumn = {
{ name: 'sftyAcdntEvntcsCnt', fieldName: 'sftyAcdntEvntcsCnt', header: { text: '안전사고건수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
{ name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, editable: false, styleName: 'rg-cell-readonly' },
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false, editable: false, styleName: 'rg-cell-readonly' },
{ name: 'fcltSn', fieldName: 'fcltSn', header: { text: '처리시설일련번호' }, visible: false, editable: false, valueType: ValueType.TEXT, styleName: 'rg-cell-readonly' },
{ name: 'swtrmtFcltCd', fieldName: 'swtrmtFcltCd', header: { text: '하수처리시설코드' }, visible: false, editable: false, styleName: 'rg-cell-readonly' },
],
@@ -1,9 +1,16 @@
import { ENDPOINT } from "@/shared/config";
import { stClient } from "@/shared/api/client";
import { ENDPOINT } from "@/shared/config";
import type {
PublicSewageMainFacilityResponse,
PublicSewageFacilityRequestParams,
PublicSewageFacilityResponse,
PublicSewageFacilitySaveRequest,
PublicSewageFacilitySaveResponse,
PublicSewageFacilitySubmitRequest,
PublicSewageFacilitySubmitResponse,
PublicSewageMainFacilityResponse,
PublicSewageMappingTargetsRequestParams,
PublicSewageMappingTargetsResponse,
StatisticsInputPublicSewageFacilityService,
} from "../../model/input/statistics-public-sewage-facility-input-interface";
@@ -23,6 +30,36 @@ export class StatisticsInputPublicSewageFacilityApiService implements Statistics
);
return response.data;
}
public async getMappingTargets(
param: PublicSewageMappingTargetsRequestParams,
): Promise<PublicSewageMappingTargetsResponse> {
const response = await stClient.get<PublicSewageMappingTargetsResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/public-sewage-facility/main-facility/mapping-targets`,
{ params: param },
);
return response.data;
}
public async savePublicSewageFacility(
param: PublicSewageFacilitySaveRequest,
): Promise<PublicSewageFacilitySaveResponse> {
const response = await stClient.post<PublicSewageFacilitySaveResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/public-sewage-facility/facility/save`,
param,
);
return response.data;
}
public async submitPublicSewageFacility(
param: PublicSewageFacilitySubmitRequest,
): Promise<PublicSewageFacilitySubmitResponse> {
const response = await stClient.post<PublicSewageFacilitySubmitResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/public-sewage-facility/facility/submit`,
param,
);
return response.data;
}
}
export const statisticsInputPublicSewageFacilityApiService = new StatisticsInputPublicSewageFacilityApiService();
@@ -139,7 +139,116 @@ export interface PublicSewageMainFacilityResponse {
data: PublicSewageMainFacilityResponseData;
}
/** ST00060002 메인시설 클릭 시 우측 매핑대상 조회 요청 */
export interface PublicSewageMappingTargetsRequestParams {
sewerBzmnCd: string;
ctpvNm: string;
sggNm: string;
statsYr: string;
fcltSn: string;
}
/** ST00060002 메인시설 클릭 시 우측 매핑대상 조회 항목 */
export interface PublicSewageMappingTargetItem {
fcltSn: string;
admdstNm: string;
swtrmtFcltCd: string;
swtrmtFcltNm: string;
fcltPstnCn: string;
mappedYn: string;
mainFcltSn: string;
isMainYn: string;
}
/** ST00060002 메인시설 클릭 시 우측 매핑대상 조회 응답 */
export interface PublicSewageMappingTargetsResponse {
status: string;
message?: string;
data: PublicSewageMappingTargetItem[];
}
/** ST00060001 공공하수처리시설 저장 — saveList 항목 */
export interface PublicSewageFacilitySaveItem {
admdstCd: string;
admdstNm: string;
swtrmtFcltCd: string;
swtrmtFcltNm: string;
fcltSn: string;
fcltPstnCn: string | null;
inflwSwagePhysQnt: number;
inflwSwageBoilQnt: number;
inflwSwageAltdQnt: number;
prcsPhysCpct: number;
prcsBoilCpct: number;
prcsAltdCpct: number;
prxyCtrtCn: string | null;
sftyAcdntEvntcsCnt: number;
rmrk: string | null;
}
/** ST00060001 공공하수처리시설 저장 API 요청 */
export interface PublicSewageFacilitySaveRequest {
statsYr: string;
ctpvNm: string;
sggNm: string;
sewerBzmnCd: string;
userId: string;
superAdmin: string;
/**
* 저장 요청에서 "해당없음" 처리 여부가 함께 전달되는 경우를 고려합니다.
* (백엔드 스펙에 따라 필수 여부는 달라질 수 있어 optional로 둡니다.)
*/
noApplicable?: boolean;
saveList: PublicSewageFacilitySaveItem[];
}
export interface PublicSewageFacilitySaveResponse {
data?: string;
message?: string;
status: string;
}
/** 공공하수처리시설 입력 제출 공통 본문 (`submitList` 제외) */
export interface PublicSewageFacilitySubmitRequestBase {
statsYr: string;
ctpvNm: string;
sggNm: string;
sewerBzmnCd: string;
userId: string;
noApplicable?: boolean;
laePicNm: string;
laePicTelNo: string;
laeKbrdrDeptNm: string;
laeKbrdrJbgdNm: string;
laeIdfrNm: string;
laeIdfrTelNo: string;
laeIdfrDeptNm: string;
laeIdfrJbgdNm: string;
}
/** ST00060001 공공하수처리시설 제출 API 요청 (`submitList` 행은 저장과 동일) */
export interface PublicSewageFacilitySubmitRequest extends PublicSewageFacilitySubmitRequestBase {
submitList: PublicSewageFacilitySaveItem[];
}
export interface PublicSewageFacilitySubmitResponse {
data: {
finalStatus: string;
nvpassed: boolean;
}
message?: string;
status: string;
}
export interface StatisticsInputPublicSewageFacilityService {
getPublicSewageFacility(param: PublicSewageFacilityRequestParams): Promise<PublicSewageFacilityResponse>;
getPublicSewageMainFacility(param: PublicSewageFacilityRequestParams): Promise<PublicSewageMainFacilityResponse>;
getMappingTargets(param: PublicSewageMappingTargetsRequestParams): Promise<PublicSewageMappingTargetsResponse>;
savePublicSewageFacility(param: PublicSewageFacilitySaveRequest): Promise<PublicSewageFacilitySaveResponse>;
submitPublicSewageFacility(param: PublicSewageFacilitySubmitRequest): Promise<PublicSewageFacilitySubmitResponse>;
}