This commit is contained in:
2026-03-27 17:14:30 +09:00
24 changed files with 373 additions and 194 deletions
+9
View File
@@ -32,9 +32,18 @@
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error {
@apply !bg-[#ffe6e6] !text-[#c00];
}
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error-imr {
@apply !bg-[#fff7bf] !text-[#b45309];
}
.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-rowtype-biz-sum {
@apply !bg-[#ffe9cf];
}
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-rowtype-other {
@apply !bg-[#fff7bf];
}
/* 진행상황: 소프트 인라인 요약 (툴바와 자연스럽게 blend) */
.data-grid-status-bubble {
@@ -67,6 +67,7 @@ const CompactDataGrid = ({
fixedColumns,
showHeaderCheckbox = false,
rowData,
enableSort = false,
// setRowData,
getGridDataRef,
cellStyleCallback,
@@ -375,7 +376,7 @@ const CompactDataGrid = ({
numberFormat: column.footer.numberFormat,
} : undefined}
mergeRule={column.mergeCells ? { criteria: 'value' } : undefined}
sortable={!column.sortdisable}
sortable={enableSort && !column.sortdisable}
/>
))}
</RealGridReact>
+54 -47
View File
@@ -324,6 +324,7 @@ const DataGrid = ({
statusInfo,
highlightedRowId,
idFieldName = "id",
enableSort = false,
}: DataGridProps) => {
const { columns, columnLayout } = columnInfo;
const gridRef = useRef<RealGridReact>(null);
@@ -557,10 +558,10 @@ const DataGrid = ({
) {
const cell =
worksheet[
XLSX.utils.encode_cell({
r: rowIndex,
c: col,
})
XLSX.utils.encode_cell({
r: rowIndex,
c: col,
})
];
if (
cell &&
@@ -636,7 +637,7 @@ const DataGrid = ({
const col = range.s.c + colIndex;
const cell =
worksheet[
XLSX.utils.encode_cell({ r: row, c: col })
XLSX.utils.encode_cell({ r: row, c: col })
];
const headerName =
headers[colIndex] || `Column${colIndex + 1}`;
@@ -759,12 +760,14 @@ const DataGrid = ({
gridView as {
onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean;
}
).onSorting = createOnSortingHandler(
columns,
setRowData,
sortDirRef,
originalRowsRef,
);
).onSorting = enableSort
? createOnSortingHandler(
columns,
setRowData,
sortDirRef,
originalRowsRef,
)
: () => false;
(gridView.displayOptions as DisplayOptions).fitStyle =
GridFitStyle.EVEN;
@@ -934,7 +937,7 @@ const DataGrid = ({
) {
(
column.footer[
index
index
] as FooterItemWithCallback
).valueCallback =
footerItem.valueCallback;
@@ -987,11 +990,11 @@ const DataGrid = ({
getGridDataRef.current = () => {
const gv = gridRef.current?.gridView as
| {
getDataSource?: () => {
getRowCount(): number;
getValue(r: number, f: string): unknown;
};
}
getDataSource?: () => {
getRowCount(): number;
getValue(r: number, f: string): unknown;
};
}
| undefined;
if (!gv?.getDataSource) return [];
const ds = gv.getDataSource();
@@ -1102,21 +1105,23 @@ const DataGrid = ({
<span>
진행상황:{" "}
{statusInfo.laeSbmsnSttsCd === null ||
statusInfo.laeSbmsnSttsCd === "0"
statusInfo.laeSbmsnSttsCd === "0"
? "미작성"
: statusInfo.laeSbmsnSttsCd === "1"
? "작성중"
: statusInfo.laeSbmsnSttsCd === "2"
? "제출"
: statusInfo.laeSbmsnSttsCd === "3"
? "승인"
: "반려"}
? "작성중"
: statusInfo.laeSbmsnSttsCd === "2"
? "제출"
: statusInfo.laeSbmsnSttsCd === "3"
? "승인"
: statusInfo.laeSbmsnSttsCd === "4"
? "반려"
: "검증중"}
</span>
<span>
제출일:{" "}
{statusInfo.laeSbmsnYmd
? statusInfo.laeSbmsnYmd.length === 8 &&
/^\d{8}$/.test(statusInfo.laeSbmsnYmd)
/^\d{8}$/.test(statusInfo.laeSbmsnYmd)
? `${statusInfo.laeSbmsnYmd.slice(0, 4)}-${statusInfo.laeSbmsnYmd.slice(4, 6)}-${statusInfo.laeSbmsnYmd.slice(6, 8)}`
: statusInfo.laeSbmsnYmd
: "미제출"}
@@ -1170,23 +1175,25 @@ const DataGrid = ({
d: unknown[],
) => boolean;
}
).onSorting = createOnSortingHandler(
columns,
setRowData,
sortDirRef,
originalRowsRef,
);
).onSorting = enableSort
? createOnSortingHandler(
columns,
setRowData,
sortDirRef,
originalRowsRef,
)
: () => false;
const dataProvider = gridRef.current
?.dataProvider as unknown as
| {
setFields?(f: unknown[]): void;
setRows?(
r: Record<string, string | number>[],
): void;
getJsonRow?(
r: number,
): Record<string, unknown>;
}
setFields?(f: unknown[]): void;
setRows?(
r: Record<string, string | number>[],
): void;
getJsonRow?(
r: number,
): Record<string, unknown>;
}
| undefined;
if (dataProvider?.setFields) {
dataProvider.setFields(
@@ -1339,14 +1346,14 @@ const DataGrid = ({
})}
footer={
column.footer &&
!Array.isArray(column.footer)
!Array.isArray(column.footer)
? {
text: column.footer.text,
expression:
column.footer.expression,
numberFormat:
column.footer.numberFormat,
}
text: column.footer.text,
expression:
column.footer.expression,
numberFormat:
column.footer.numberFormat,
}
: undefined
}
mergeRule={
@@ -1354,7 +1361,7 @@ const DataGrid = ({
? { criteria: "value" }
: undefined
}
sortable={!column.sortdisable}
sortable={enableSort && !column.sortdisable}
/>
);
})}
+17 -1
View File
@@ -11,7 +11,8 @@ import {
SelectTrigger,
SelectContent,
SelectItem,
SelectValue
SelectValue,
Checkbox
}
from "@common/components";
@@ -64,6 +65,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
order: props.defaultValues?.order || '',
fctName: props.defaultValues?.fctName || '',
bsnmCd: props.defaultValues?.bsnmCd || '',
isDetail: false,
});
const sggKey: [string, string] | null =
@@ -106,6 +108,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
order: '',
fctName: '',
bsnmCd: '',
isDetail: false,
};
setValues(resetValues);
props.onReset?.();
@@ -201,6 +204,19 @@ const SearchForm = (props: SearchFormProps = {}) => {
</Select>
</div>
}
{mergedProps.isDetail &&
<div className="flex items-center gap-2">
<Label className="text-label">
세부 데이터 포함
</Label>
<Checkbox
checked={values.isDetail}
onCheckedChange={(value) =>
setValues({ ...values, isDetail: value === true })
}
/>
</div>
}
{mergedProps.order &&
<div className="flex items-center gap-2">
<Label className="text-label">
@@ -11,6 +11,7 @@ const SplitDataGrid = ({
rightRowData,
setLeftRowData,
setRightRowData,
enableSort = false,
}: SplitDataGridProps) => {
return (
<div className="flex gap-[0.6rem] mx-[0.6rem]">
@@ -18,6 +19,7 @@ const SplitDataGrid = ({
columnInfo={leftColumnInfo}
fixedColumns={leftFixedColumns}
showHeaderCheckbox={showHeaderCheckbox}
enableSort={enableSort}
rowData={leftRowData}
setRowData={setLeftRowData}
/>
@@ -25,6 +27,7 @@ const SplitDataGrid = ({
columnInfo={rightColumnInfo}
fixedColumns={rightFixedColumns}
showHeaderCheckbox={showHeaderCheckbox}
enableSort={enableSort}
rowData={rightRowData}
setRowData={setRightRowData}
/>
@@ -306,7 +306,7 @@ function createNumberComparer(dp: RealGridDataProviderComparer): (field: string
};
}
const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, rows = [], showHeaderCheckbox = false, rowDataMap, setRowDataMap, activeTab: activeTabProp, onActiveTabChange, getGridDataRef }: TabDataGridProps) => {
const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, rows = [], showHeaderCheckbox = false, rowDataMap, setRowDataMap, activeTab: activeTabProp, onActiveTabChange, getGridDataRef, enableSort = false }: TabDataGridProps) => {
// 각 탭별로 그리드 ref와 데이터를 관리
const gridRefs = useRef<Map<string, RealGridReact>>(new Map());
const activeTabRef = useRef<string>("");
@@ -849,6 +849,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
leftFixedColumns={info.left.fixedColumns}
rightFixedColumns={info.right.fixedColumns}
showHeaderCheckbox={showHeaderCheckbox}
enableSort={enableSort}
leftRowData={leftRowData}
rightRowData={rightRowData}
setLeftRowData={(updater) => {
@@ -1158,7 +1159,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
};
const onSortingHandler = createOnSortingHandler(columns, setRowData, sortDirRef, originalRowsRef, findGridElement);
(gridView as { onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean }).onSorting = onSortingHandler;
(gridView as { onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean }).onSorting = enableSort ? onSortingHandler : () => false;
}, 150);
}
} else {
@@ -1190,7 +1191,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
numberFormat: (column.footer as { numberFormat?: string }).numberFormat,
} : undefined}
mergeRule={column.mergeCells ? { criteria: column.mergeGroupField ? `values['${column.mergeGroupField}'] + value` : 'value' } : undefined}
sortable={!column.sortdisable}
sortable={enableSort && !column.sortdisable}
/>
))}
</RealGridReact>
@@ -40,6 +40,11 @@ type GridRowWithVerification = Record<string, string | number> & {
verificationResult?: SewerageSupplyRateVerificationResult;
};
const hasVerificationErrors = (verification?: SewerageSupplyRateVerificationResult | null): boolean => {
if (!verification) return false;
return Object.values(verification).some((errors) => Array.isArray(errors) && errors.length > 0);
};
function getSewerageSupplyRateParams(
values: SearchFormValues,
overrides?: { yr?: string; demoYr?: string; mode?: string }
@@ -91,7 +96,8 @@ const SeweragePenetrationRateInp = () => {
if (col.fieldName === 'pbadmsWholPpltnCnt') return { ...col, footer: footerWithApiDefault('sewerBzmnWholPpltnCnt', '#,##0') };
if (col.fieldName === 'domplWholPpltnCnt') return { ...col, footer: footerWithApiDefault('sewerBzmnDomplWholPpltnCnt', '#,##0') };
if (col.fieldName === 'frgnrWholPpltnCnt') return { ...col, footer: footerWithApiDefault('sewerBzmnFrgnrWholPpltnCnt', '#,##0') };
if (col.fieldName === 'sewerBzmnArea') return { ...col, footer: footerWithApiDefault('sewerBzmnArea', '#,##0.000') };
// 그리드 필드명은 refArea(행정참고자료면적). API commonInfo.sewerBzmnArea와 매핑
if (col.fieldName === 'refArea') return { ...col, footer: footerWithApiDefault('sewerBzmnArea', '#,##0.000') };
return col;
}),
};
@@ -204,7 +210,10 @@ const SeweragePenetrationRateInp = () => {
};
const submitFunc = async () => {
const isError = rowData.filter((d) => d.verificationResult);
const isError = rowData.filter((d) =>
hasVerificationErrors((d as GridRowWithVerification).verificationResult)
);
console.log(isError);
if (isError.length > 0) {
alert(`데이터에 오류가 있습니다.
수정 후 제출해주세요.`);
@@ -291,7 +300,7 @@ const SeweragePenetrationRateInp = () => {
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false);
alert('제출되었습니다.');
} else {
alert('데이터 제출 중 오류가 발생했습니다.');
alert(response.message || '데이터 제출 중 오류가 발생했습니다.');
}
} catch {
alert('데이터 제출 중 오류가 발생했습니다.');
@@ -381,17 +390,25 @@ const SeweragePenetrationRateInp = () => {
sewerBzmnFrgnrWholPpltnCnt: footerFromApi?.sewerBzmnFrgnrWholPpltnCnt // 행정참고자료 외국인 합계
};
if (isAdminInfo) {
requestData.superAdmin = 'superAdmin';
}
// if (isAdminInfo) {
// requestData.superAdmin = 'superAdmin';
// }
const response = await statisticsInputSewSupplyRateApi.saveSewerageSupplyRate(requestData);
if (response.status === 'success') {
fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
const imrParam = {
user_id: userInfo.userUnqId,
sewer_bzmn_cd: searchValues?.bsnmCd ?? '',
stats_cd: statsCd,
stats_yr: searchValues?.year ?? '',
};
const response = await statisticsAPi.callImrService(imrParam)
console.log(response);
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
alert('저장되었습니다.');
} else {
alert('데이터 저장 중 오류가 발생했습니다.');
alert(response.message || '데이터 저장 중 오류가 발생했습니다.');
}
} else {
// 저장 취소
@@ -432,14 +449,26 @@ const SeweragePenetrationRateInp = () => {
}
};
const gugunOkOrNotDoList = doList.includes(lastSearchedValues?.bsnmCd || '')
? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL')
: true;
const showSaveInToolbar = (isEnterable || isAdminInfo) && gugunOkOrNotDoList;
const showEnterableToolbarButtons = isEnterable && gugunOkOrNotDoList;
/** 카드는 저장/제출줄/제출취소 중 하나라도 보일 때만 (관리자만으로 빈 카드 방지) */
const showActionToolbarCard =
rowData.length > 0 &&
(showSaveInToolbar ||
(statusInfo?.laeSbmsnSttsCd === '2' && (isAdminInfo || (isEnterable && gugunOkOrNotDoList))));
return (
<>
<div className="relative">
<SearchForm onSubmit={handleSearch} onReset={handleReset} onValuesChange={setSearchValues} />
{rowData.length > 0 && (isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) || isAdminInfo) && (
{showActionToolbarCard && (
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
{isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) && (
{showEnterableToolbarButtons && (
<>
<SubmitExceptionButton SubmitExceptionFunc={SubmitExceptionFunc} />
<LastYearDataButton getLastYearDataFunc={() => getLastYearDataFunc(searchValues ?? {})} />
@@ -448,7 +477,7 @@ const SeweragePenetrationRateInp = () => {
</>
)}
{statusInfo?.laeSbmsnSttsCd === '2' && <CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />}
{(isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) || isAdminInfo) && <SaveButton saveFunc={saveFunc} />}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
</div>
</div>
)}
@@ -472,7 +501,9 @@ const SeweragePenetrationRateInp = () => {
if (!verification) return;
const errors = verification[fieldName];
if (Array.isArray(errors) && errors.length > 0) {
return 'rg-cell-verify-error';
// IMR 검증은 노란색으로 구분
const hasImr = errors.some((e) => e.vrfcCd === 'IMR');
return hasImr ? 'rg-cell-verify-error-imr' : 'rg-cell-verify-error';
}
}}
cellTooltipCallback={({ row, fieldName }) => {
@@ -480,7 +511,13 @@ const SeweragePenetrationRateInp = () => {
if (!verification) return;
const errors = verification[fieldName];
if (Array.isArray(errors) && errors.length > 0) {
return errors.map((e) => e.rsnExpln).filter(Boolean).join('\n');
return errors
.map((e) => {
const prefix = e.vrfcCd === 'IMR' ? 'IMR검증 - ' : '일반검증 - ';
return `${prefix}${e.rsnExpln}`;
})
.filter(Boolean)
.join('\n');
}
}}
/>
@@ -1,8 +1,6 @@
import { useState, useMemo } from "react";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import SaveButton from "@/components/button/SaveButton";
import ApproveButton from "@/components/button/ApproveButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import ReturnButton from "@/components/button/ReturnButton";
import { LoadingWithAnimation } from "@common/components";
@@ -88,9 +86,7 @@ const SeweragePenetrationRateRvw = () => {
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
<ApproveButton approveFunc={approveFunc} />
<ReturnButton returnFunc={returnFunc} />
<SaveButton saveFunc={saveFunc} />
</div>
</div>
@@ -1,113 +1,69 @@
import { useState } from "react";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SeweragePentrationRateTotColumn } from "@/components/util/column/total/SeweragePentrationRateTotColumn";
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { useAlertStore } from "@/shared/lib";
import SearchForm from "@/components/module/SearchForm";
import DataGrid from "@/components/module/DataGrid";
import { LoadingWithAnimation } from "@common/components";
const SeweragePenetrationRateTot = () => {
const columns = [
{ name: 'sido', fieldName: '시도', header: { text: '시도' } },
{ name: 'sgg', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'rvbsn', fieldName: '수계', header: { text: '수계' } },
{ name: 'bstrm', fieldName: '지류', header: { text: '지류' } },
{ name: 'mdbsn', fieldName: '세부단위유역', header: { text: '세부단위유역' } },
{ name: 'lgbsn', fieldName: '중권역', header: { text: '중권역' } },
{ name: 'mdmbsn', fieldName: '소권역', header: { text: '소권역' } },
{ name: 'specl_cntrpln_ar', fieldName: '특별대책지역', header: { text: '특별대책지역' } },
{ name: 'total_popl', fieldName: '총인구', header: { text: '총인구' } },
{ name: 'popl_sm', fieldName: '내국인', header: { text: '내국인' } },
{ name: 'frgnr_popl_sm', fieldName: '외국인', header: { text: '외국인' } },
{ name: 'area', fieldName: '행정참고자료면적(㎢)', header: { text: '행정참고자료면적(㎢)' } },
{ name: 'popltn_sm', fieldName: '총인구(명)', header: { text: '총인구(명)' } },
{ name: 'ar_sm', fieldName: '총면적(㎢)', header: { text: '총면적(㎢)' } },
{ name: 'swge_proc_zone_inner_sm', fieldName: '합계', header: { text: '합계' } },
{ name: 'pblswrg_popltn_sm', fieldName: '소계', header: { text: '소계' } },
{ name: 'pblswrg_prm_trt', fieldName: '물리적처리', header: { text: '물리적처리' } },
{ name: 'pblswrg_scd_trt', fieldName: '생물학적처리', header: { text: '생물학적처리' } },
{ name: 'pblswrg_tert_trt', fieldName: '고도처리', header: { text: '고도처리' } },
{ name: 'wstr_popltn_sm', fieldName: '소계', header: { text: '소계' } },
{ name: 'wstr_prm_trt', fieldName: '물리적처리', header: { text: '물리적처리' } },
{ name: 'wstr_scd_trt', fieldName: '생물학적처리', header: { text: '생물학적처리' } },
{ name: 'wstr_tert_trt', fieldName: '고도처리', header: { text: '고도처리' } },
{ name: 'not_conect_popltn', fieldName: '미접속인구', header: { text: '미접속인구' } },
{ name: 'swge_proc_zone_inner_ar', fieldName: '면적(㎢)', header: { text: '면적(㎢)' } },
{ name: 'swge_proc_zone_outer_sm', fieldName: '합계', header: { text: '합계' } },
{ name: 'town_unproc_popltn', fieldName: '미처리인구', header: { text: '미처리인구' } },
{ name: 'town_swtt_popltn', fieldName: '오수처리인구', header: { text: '오수처리인구' } },
{ name: 'town_wrrtn_popltn', fieldName: '정화조인구', header: { text: '정화조인구' } },
{ name: 'swge_proc_zone_outer_ar', fieldName: '면적(㎢)', header: { text: '면적(㎢)' } },
{ name: 'pblswrg_popltn_suprt', fieldName: '공공하수처리구역인구보급율(%)', header: { text: '공공하수처리구역인구보급율(%)' } },
{ name: 'adtrt_popltn_suprt', fieldName: '고도처리인구보급율(%)', header: { text: `고도처리인구보급율(%)` } },
{ name: 'swrg_instlrt', fieldName: '하수도설치율(%)', header: { text: '하수도설치율(%)' } },
{ name: 'rmk', fieldName: '비고', header: { text: '비고' } },
];
const columnLayout = [
{
name: 'groupA',
hideChildHeaders: false,
header: { text: '행정구역' },
items: ['sido', 'sgg'],
},
'rvbsn',
'bstrm',
{
name: 'groupB',
hideChildHeaders: false,
header: { text: '수계영향권별분류류' },
items: ['mdbsn', 'lgbsn', 'mdmbsn'],
},
'specl_cntrpln_ar',
{
name: 'groupC',
hideChildHeaders: false,
header: { text: '행정참고자료인구' },
items: ['total_popl', 'popl_sm', 'frgnr_popl_sm'],
},
'area',
'popltn_sm',
'ar_sm',
{
name: 'groupD',
hideChildHeaders: false,
header: { text: '하수처리구역내' },
items: [
'swge_proc_zone_inner_sm',
{
name: 'pblswrgGroup',
hideChildHeaders: false,
header: { text: '공공하수처리인구(명)' },
items: ['pblswrg_popltn_sm', 'pblswrg_prm_trt', 'pblswrg_scd_trt', 'pblswrg_tert_trt'],
},
{
name: 'wstrGroup',
hideChildHeaders: false,
header: { text: '폐수처리인구(명)' },
items: ['wstr_popltn_sm', 'wstr_prm_trt', 'wstr_scd_trt', 'wstr_tert_trt'],
},
'not_conect_popltn',
'swge_proc_zone_inner_ar'
],
},
{
name: 'groupE',
hideChildHeaders: false,
header: { text: '하수처리구역외' },
items: ['swge_proc_zone_outer_sm', 'town_swtt_popltn', 'town_unproc_popltn', 'town_wrrtn_popltn', 'swge_proc_zone_outer_ar'],
},
'pblswrg_popltn_suprt',
'adtrt_popltn_suprt',
'swrg_instlrt',
'rmk',
];
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const { setMessage: alert } = useAlertStore((state) => state);
const handleSearch = async (values: SearchFormValues) => {
try {
setIsLoading(true);
const param = {
ctpvNm: values.sido === 'ALL' ? '' : values.sido,
sggNm: values.gugun === 'ALL' ? '' : values.gugun,
sewerBzmnCd: values.bsnmCd,
statsYr: values.year,
rowDataYn: values.isDetail
};
const response = await statisticsTotalStatusApi.getSewerageSupplyRateList(param);
if (response.status === 'success') {
setRowData(toGridRows(response.data));
} else {
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
}
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
}
};
return (
<>
<SearchForm />
<DataGrid columnInfo={SeweragePentrationRateTotColumn} rowStartNum={2} isExcelImport={false} fixedColumns={1} rowData={rowData} setRowData={setRowData} />
<SearchForm isDetail={true} onSubmit={handleSearch} />
{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" />
</div>
</div>}
<DataGrid
columnInfo={SeweragePentrationRateTotColumn}
rowStartNum={2}
isExcelImport={false}
fixedColumns={1}
rowData={rowData}
setRowData={setRowData}
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';
}}
/>
</>
);
};
@@ -2,7 +2,12 @@
import { useEffect, useMemo, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
keepPreviousData,
useMutation,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import {
getCoreRowModel,
getPaginationRowModel,
@@ -212,6 +217,7 @@ const HbStatisticsVerification = () => {
);
return hasProcessing ? 3000 : false;
},
placeholderData: keepPreviousData,
});
const historyRowData: StAnlsMstResponse[] = analysisHistory?.content ?? [];
@@ -3,7 +3,7 @@
import { useEffect, useMemo, useState } from "react";
import { Line } from "react-chartjs-2";
import { useQuery } from "@tanstack/react-query";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import {
getCoreRowModel,
getPaginationRowModel,
@@ -31,15 +31,17 @@ import type {
StAnlsRsltResponse,
} from "@/entities/statistics";
import {
IMR_DETAIL_VISIBLE_COLUMNS_BY_STATS_CD,
IMR_DETAIL_VISIBLE_COLUMNS_DEFAULT,
imrDetailGridColumnInfo,
imrVerificationHistoryColumns,
} from "@/entities/statistics";
import { stAnlsMstApi } from "@/entities/statistics/api/statistics-analysis-master-api";
import { imrApi } from "@/entities/statistics/api/imr-api";
import {
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";
import {
Button,
@@ -55,6 +57,7 @@ import {
} from "@common/components";
import DataGrid from "@/components/module/DataGrid";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
ChartJS.register(
CategoryScale,
@@ -74,6 +77,19 @@ type DateRangeValue = {
const EMPTY_DATA: never[] = [];
const EMPTY_ADST_LIST: AdstZoneResponseDto[] = [];
function getImrDetailGridColumnInfo(statsCd: string): ColumnInfo {
const visible =
IMR_DETAIL_VISIBLE_COLUMNS_BY_STATS_CD[statsCd] ??
IMR_DETAIL_VISIBLE_COLUMNS_DEFAULT;
const visibleSet = new Set(visible);
return {
columns: imrDetailGridColumnInfo.columns.map((col) =>
visibleSet.has(col.name) ? col : { ...col, visible: false },
),
columnLayout: imrDetailGridColumnInfo.columnLayout,
};
}
const ImrVerificationHistory = () => {
// Area A Search States
const [searchSido, setSearchSido] = useState<string>("");
@@ -114,6 +130,11 @@ const ImrVerificationHistory = () => {
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
const sigunguOptions = sigunguData ?? EMPTY_ADST_LIST;
const imrDetailColumnInfo = useMemo(
() => getImrDetailGridColumnInfo(selectedHistory?.statsCd ?? ""),
[selectedHistory?.statsCd],
);
// Area A Data Fetching
const { data: historyData } = useQuery({
queryKey: [
@@ -128,6 +149,7 @@ const ImrVerificationHistory = () => {
page: historyPagination.pageIndex,
size: historyPagination.pageSize,
}),
placeholderData: keepPreviousData,
});
const historyTable = useReactTable({
@@ -482,7 +504,7 @@ const ImrVerificationHistory = () => {
</div>
) : (
<DataGrid
columnInfo={imrDetailGridColumnInfo}
columnInfo={imrDetailColumnInfo}
rowData={gridRowData}
setRowData={setGridRowData}
idFieldName="rsltSn"
@@ -47,8 +47,7 @@ const CopyDialog = ({
const handleChangeCode = (value: string) => {
if (row?.trgtStatsCd === value) {
alert('원본과 동일한 통계항목으로 복사할 수 없습니다.');
return;
alert('원본과 동일한 통계항목으로 복사할 경우\n복사 완료 후 검증 분류를 꼭 수정해주세요.');
}
setSelectedCode(value);
@@ -244,6 +244,7 @@ const RegistDialog = memo(
vrfcNm: data.vrfcNm,
vrfcClsfCd: data.vrfcClsfCd,
dcptDigtCnt: data.dcptDigtCnt,
useYn: data.useYn,
...(data.dcptDigtCnt === 0
? {}
: { dcptPrcsMthCd: data.dcptPrcsMthCd }),
@@ -248,6 +248,8 @@ export type DataGridProps = {
highlightedRowId?: string | number | null;
/** ID로 사용할 필드명 (기본값: 'id') */
idFieldName?: string;
/** 전체 정렬 기능 on/off (기본값: false) */
enableSort?: boolean;
};
export type TabDataGridProps = {
@@ -270,6 +272,8 @@ export type TabDataGridProps = {
getGridDataRef?: {
current: (() => Record<string, string | number>[]) | null;
};
/** 전체 정렬 기능 on/off (기본값: false) */
enableSort?: boolean;
};
export type SplitDataGridProps = {
@@ -286,4 +290,6 @@ export type SplitDataGridProps = {
setRightRowData: React.Dispatch<
React.SetStateAction<Record<string, string | number>[]>
>;
/** 전체 정렬 기능 on/off (기본값: false) */
enableSort?: boolean;
};
@@ -5,6 +5,7 @@ export type SearchFormValues = {
order?: string;
fctName?: string;
bsnmCd?: string;
isDetail?: boolean;
};
export type SearchFormProps = {
@@ -14,6 +15,7 @@ export type SearchFormProps = {
order?: boolean;
fctName?: boolean;
userMenual?: boolean;
isDetail?: boolean;
onSubmit?: (values: SearchFormValues) => void;
onReset?: () => void;
onValuesChange?: (values: SearchFormValues) => void;
@@ -27,4 +29,5 @@ export const defaultSearchFormProps: Required<Omit<SearchFormProps, 'onSubmit' |
order: false,
fctName: false,
userMenual: false,
isDetail: false,
};
@@ -12,7 +12,7 @@ export const SeweragePentrationRateTotColumn = {
// 수계 및 권역
{ name: 'rvbsn', fieldName: 'rvbsn', header: { text: '수계' }, editable: false, },
{ name: 'trbtrNm', fieldName: 'trbtrNm', header: { text: '지류' } },
{ name: 'trbtrNm', fieldName: 'trbtrNm', header: { text: '지류' }, editable: false, },
// 수계영향권별분류
{ name: 'mdbsn', fieldName: 'mdbsn', header: { text: '세부단위유역' }, editable: false, },
@@ -20,15 +20,7 @@ export const SeweragePentrationRateTotColumn = {
{ name: 'mdmbsn', fieldName: 'mdmbsn', header: { text: '소권역' }, editable: false, },
// 특별대책지역
{ name: 'spcCtpnRgnCn', fieldName: 'spcCtpnRgnCn', header: { text: '특별대책지역' } },
// 행정참고자료인구
{ name: 'pbadmsWholPpltnCnt', fieldName: 'pbadmsWholPpltnCnt', header: { text: '총인구' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', },
{ name: 'domplWholPpltnCnt', fieldName: 'domplWholPpltnCnt', header: { text: '내국인' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', },
{ name: 'frgnrWholPpltnCnt', fieldName: 'frgnrWholPpltnCnt', header: { text: '외국인' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', },
// 행정참고자료면적
{ name: 'refArea', fieldName: 'refArea', header: { text: '행정참고자료면적(㎢)' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', },
{ name: 'spcCtpnRgnCn', fieldName: 'spcCtpnRgnCn', header: { text: '특별대책지역' }, editable: false, },
// 총인구
{
@@ -47,6 +39,7 @@ export const SeweragePentrationRateTotColumn = {
header: { text: '총면적(㎢)' },
valueType: ValueType.NUMBER,
dataFormat: '#,##0.000',
editable: false,
},
// 하수처리구역 내 > 합계
@@ -140,7 +133,7 @@ export const SeweragePentrationRateTotColumn = {
{ name: 'swagePplPsplyRt', fieldName: 'swagePplPsplyRt', header: { text: '하수관로보급률(%)' }, visible: false },
{ name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정코드' }, visible: false },
],
].map((col) => ({ ...col, editable: false })),
columnLayout: [
{
@@ -162,13 +155,6 @@ export const SeweragePentrationRateTotColumn = {
items: ['mdbsn', 'lgbsn', 'mdmbsn'],
},
'spcCtpnRgnCn',
{
name: 'groupC',
hideChildHeaders: false,
header: { text: '행정참고자료인구' },
items: ['pbadmsWholPpltnCnt', 'domplWholPpltnCnt', 'frgnrWholPpltnCnt'],
},
'refArea',
'wholPpltnCnt',
'wholArea',
{
@@ -28,7 +28,7 @@ export class StatisticsInputSewSupplyRateApiService implements StatisticsInputSe
public async saveSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise<SewerageSupplyRateSaveApiResponse> {
const response = await stClient.post<SewerageSupplyRateSaveApiResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/sewerage-supply-rate`,
`${ENDPOINT.ST_SERVICE.INPUT}/sewerage-supply-rate/save`,
params,
);
return response.data;
@@ -34,6 +34,14 @@ export class StatisticsAPiService implements StatisticsService {
);
return response.data;
}
public async callImrService(param: any): Promise<any> {
const response = await stClient.post<any>(
'http://dev.onasys.co.kr:8000/statverify-service/api/v1/imr/outlier/detect',
param
);
return response.data;
}
}
export const statisticsAPi = new StatisticsAPiService();
@@ -0,0 +1,16 @@
import { ENDPOINT } from "@/shared/config";
import { stClient } from "@/shared/api/client";
import type { SewerageSupplyRateRequest, SewerageSupplyRateResponse } from "../model/statistics-total/statistics-sewage-supply-rate-total-interface";
export class StatisticsTotalApiService {
public async getSewerageSupplyRateList(params: SewerageSupplyRateRequest): Promise<SewerageSupplyRateResponse> {
const response = await stClient.get<SewerageSupplyRateResponse> (
`${ENDPOINT.ST_SERVICE.TOTAL}/sewer-supply-rate/summary`,
{ params: params }
);
return response.data;
}
}
export const statisticsTotalStatusApi = new StatisticsTotalApiService();
+5 -1
View File
@@ -7,7 +7,11 @@ export {
type JobStatusDto,
type OutlierRecordDto,
} from "./model/analysis/analysis-interface";
export { imrDetailGridColumnInfo } from "./model/imr/imr-detail-grid-column-info";
export {
IMR_DETAIL_VISIBLE_COLUMNS_BY_STATS_CD,
IMR_DETAIL_VISIBLE_COLUMNS_DEFAULT,
imrDetailGridColumnInfo,
} from "./model/imr/imr-detail-grid-column-info";
export {
type ImrDetailRequest,
type ImrDetailResponse,
@@ -2,6 +2,52 @@ import { ValueType } from "realgrid";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
/** 공통: 시도·구군·행정구역 */
const IMR_VISIBLE_REGION_COLUMNS = ["sido", "gugun", "adstDong"] as const;
/** ST00020003, ST00020005: 시도·구군·행정구역·관종구분 */
const IMR_VISIBLE_REGION_AND_PIPE_SE_COLUMNS = [
...IMR_VISIBLE_REGION_COLUMNS,
"pipeSeNm",
] as const;
/** ST00060001, ST00100001: 시도·구군·행정구역·시설코드·시설명·주소 */
const IMR_VISIBLE_FACILITY_FULL_COLUMNS = [
...IMR_VISIBLE_REGION_COLUMNS,
"admdstCd",
"fcltNm",
"addr",
] as const;
/** ST00040001, ST00040002, ST00050001, ST00070001: 시도·구군·행정구역·시설명·주소 */
const IMR_VISIBLE_FACILITY_NAME_ADDR_COLUMNS = [
...IMR_VISIBLE_REGION_COLUMNS,
"fcltNm",
"addr",
] as const;
/**
* statsCd별 표시 컬럼 name 목록.
* 그 외 코드는 {@link IMR_DETAIL_VISIBLE_COLUMNS_DEFAULT} 사용.
*/
export const IMR_DETAIL_VISIBLE_COLUMNS_BY_STATS_CD: Record<
string,
readonly string[]
> = {
ST00020003: [...IMR_VISIBLE_REGION_AND_PIPE_SE_COLUMNS],
ST00020005: [...IMR_VISIBLE_REGION_AND_PIPE_SE_COLUMNS],
ST00060001: [...IMR_VISIBLE_FACILITY_FULL_COLUMNS],
ST00100001: [...IMR_VISIBLE_FACILITY_FULL_COLUMNS],
ST00040001: [...IMR_VISIBLE_FACILITY_NAME_ADDR_COLUMNS],
ST00040002: [...IMR_VISIBLE_FACILITY_NAME_ADDR_COLUMNS],
ST00050001: [...IMR_VISIBLE_FACILITY_NAME_ADDR_COLUMNS],
ST00070001: [...IMR_VISIBLE_FACILITY_NAME_ADDR_COLUMNS],
};
/** 그 외 statsCd: 시도·구군·행정구역만 표시 */
export const IMR_DETAIL_VISIBLE_COLUMNS_DEFAULT: readonly string[] =
IMR_VISIBLE_REGION_COLUMNS;
export const imrDetailGridColumnInfo: ColumnInfo = {
columns: [
{
@@ -44,7 +90,7 @@ export const imrDetailGridColumnInfo: ColumnInfo = {
{
name: "pipeSeNm",
fieldName: "pipeSeNm",
header: { text: "관로구분" },
header: { text: "관종구분" },
width: 80,
},
{
@@ -39,4 +39,5 @@ export interface StatisticsService {
getSidoList(): Promise<ApiResponse<SearchCondition>>;
getSggList(sidoName: string): Promise<ApiResponse<Sgg[]>>;
cancelSubmit(param: cancelSubmitRequest): Promise<cancelSubmitResponse>;
callImrService(param: any): Promise<any>;
}
@@ -0,0 +1,54 @@
export interface SewerageSupplyRateRequest {
ctpvNm?: string;
sggNm?: string;
statsYr?: string;
sewerBzmnCd?: string;
rowDataYn?: boolean;
}
export interface SewerageSupplyRateTotalItem {
ctpvNm: string;
sggNm: string;
statsYr: string;
admdstNm: string;
admdstCd: string;
rowType: string;
trbtrNm: string;
rvbsn: string;
mdbsn: string;
lgbsn: string;
mdmbsn: string;
spcCtpnRgnCn: string;
wholPpltnCnt: number;
wholArea: number;
wholSwtrmtPpltnCnt: number;
wholPblSwtrmtPpltnCnt: number;
ts1SwtrmtPpltnCnt: number;
ts2SwtrmtPpltnCnt: number;
ts3SwtrmtPpltnCnt: number;
wholWwtPrcsPpltnCnt: number;
ts1WwtPrcsPpltnCnt: number;
ts2WwtPrcsPpltnCnt: number;
ts3WwtPrcsPpltnCnt: number;
yetCntnPpltnCnt: number;
staaArea: number;
wholStaaExcpPpltnCnt: number;
unprcsPpltnCnt: number;
drtwPrcsPpltnCnt: number;
spctnkPpltnCnt: number;
staaExcpArea: number;
staaPsplyRt: number;
altdPrcsPsplyRt: number;
sewerInstlRt: number;
rmrk: string;
}
export interface SewerageSupplyRateResponse {
status: string;
message: string;
data: SewerageSupplyRateTotalItem[];
}
// export interface StatisticsTotalSewSupplyRateService {
// getSewerageSupplyRateList(params: SewerageSupplyRateRequest): Promise<SewerageSupplyRateResponse>;
// }
+1
View File
@@ -72,6 +72,7 @@ export const ENDPOINT = {
REVIEW: "/st-management-service/api/v1/review",
INPUTCOMMON: "/st-input-service/api/v1/common",
INPUTVERIFY: "/st-input-service/api/v1/verify",
TOTAL: "/st-management-service/api/v1/stats"
},
STATVERIFY_SERVICE: {
VRFCTGT_MNG: "/statverify-service/api/v1/vrfc-trgt-mng",