This commit is contained in:
2026-04-29 13:34:14 +09:00
74 changed files with 4095 additions and 709 deletions
+4 -1
View File
@@ -14,7 +14,9 @@
"@common/components": "workspace:*",
"@common/config": "workspace:*",
"@common/network": "workspace:*",
"@rsone/components": "workspace:*",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@faker-js/faker": "^9.9.0",
"@fontsource-variable/noto-sans-kr": "^5.2.5",
"@frontend-opensource/use-react-hooks": "^1.1.4",
@@ -29,6 +31,7 @@
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@rsone/components": "workspace:*",
"@tanstack/react-query": "^5.67.1",
"@tanstack/react-query-devtools": "^5.75.5",
"@tanstack/react-table": "^8.21.2",
-9
View File
@@ -1,9 +0,0 @@
const Dashboard = () => {
return (
<div>
Dashboard
</div>
);
};
export default Dashboard;
+14 -4
View File
@@ -35,6 +35,9 @@
.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-summary {
@apply !bg-[#feee85];
}
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-rowtype-other {
@apply !bg-[#fff7bf];
}
@@ -42,6 +45,9 @@
.rg-grid .rg-body .rg-data-cell.rg-cell-rowtype-biz-sum {
@apply !bg-[#ffe9cf];
}
.rg-grid .rg-body .rg-data-cell.rg-cell-rowtype-summary {
@apply !bg-[#feee85];
}
.rg-grid .rg-body .rg-data-cell.rg-cell-rowtype-other {
@apply !bg-[#fff7bf];
}
@@ -49,6 +55,10 @@
.rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-rowtype-biz-sum {
@apply !bg-[#ffe9cf];
}
.rg-grid .rg-fixed-body .rg-table .rg-data-row td.rg-cell-rowtype-summary,
.rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-rowtype-summary {
@apply !bg-[#feee85];
}
.rg-grid .rg-fixed-body .rg-table .rg-data-row td.rg-cell-rowtype-other,
.rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-rowtype-other {
@apply !bg-[#fff7bf];
@@ -56,13 +66,13 @@
/* 진행상황: 소프트 인라인 요약 (툴바와 자연스럽게 blend) */
.data-grid-status-bubble {
@apply rounded-lg border border-slate-200/80 bg-slate-50/90 px-4 py-2.5;
@apply rounded-lg border border-slate-200/80 bg-white px-8 py-2.5;
box-shadow: none;
}
.data-grid-status-bubble span {
@apply text-base text-slate-700;
.data-grid-status-bubble > span {
@apply text-xl text-slate-700 font-bold;
}
.data-grid-status-bubble span + span {
.data-grid-status-bubble > span + span {
@apply ml-4 border-l border-slate-200/70 pl-4;
}
@@ -25,6 +25,12 @@ interface RealGridDataProviderComparer {
getValue?(row: number, field: string | number): unknown;
}
type GridViewWithCheckApi = GridViewWithSetColumns & {
checkItem?: (itemIndex: number, checked: boolean, exclusive?: boolean) => void;
checkRow?: (dataRow: number, checked: boolean, exclusive?: boolean) => void;
checkAll?: (checked: boolean) => void;
};
/** columnInfo → RealGrid DataField 배열 (computed → valueCallback 포함, setFields용) */
function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueType; computed?: import("../type/DataGrid.type").ComputedFieldConfig }[]) {
return columns.map((col) => {
@@ -72,6 +78,8 @@ const CompactDataGrid = ({
getGridDataRef,
cellStyleCallback,
onRowClick,
onItemChecked,
onItemAllChecked,
compactFullWidth = false,
compactGridHeightClass = "h-[65vh]",
}: DataGridProps) => {
@@ -96,6 +104,32 @@ const CompactDataGrid = ({
(dataProvider as { setRows: (r: unknown[]) => void }).setRows(rowData);
}
// __checked 플래그(1/true/Y) 기반으로 체크바 상태 동기화
if (showHeaderCheckbox) {
const checkGrid = gridView as GridViewWithCheckApi;
const checkByRowFlag = (value: unknown) =>
value === true ||
value === 1 ||
value === "1" ||
value === "Y" ||
value === "y" ||
value === "true";
try {
checkGrid.checkAll?.(false);
rowData.forEach((row, itemIndex) => {
const checked = checkByRowFlag((row as Record<string, unknown>).__checked);
if (!checked) return;
if (checkGrid.checkItem) {
checkGrid.checkItem(itemIndex, true, false);
return;
}
checkGrid.checkRow?.(itemIndex, true, false);
});
} catch (e) {
console.warn("check sync:", e);
}
}
// setDataComparer로 숫자 정렬 (setFields 미사용 → 데이터 유실·필드 중복 오류 없음)
const dp = dataProvider as RealGridDataProviderComparer;
if (typeof dp.setDataComparer === "function" && typeof dp.getValue === "function") {
@@ -337,6 +371,32 @@ const CompactDataGrid = ({
};
}, [onRowClick]);
useEffect(() => {
const gridView = gridRef.current?.gridView;
if (!gridView) return;
if (onItemChecked) {
gridView.onItemChecked = (_grid, itemIndex, checked) => {
onItemChecked(itemIndex, checked);
};
} else {
gridView.onItemChecked = undefined;
}
if (onItemAllChecked) {
gridView.onItemAllChecked = (_grid, checked) => {
onItemAllChecked(checked);
};
} else {
gridView.onItemAllChecked = undefined;
}
return () => {
if (gridView.onItemChecked) gridView.onItemChecked = undefined;
if (gridView.onItemAllChecked) gridView.onItemAllChecked = undefined;
};
}, [onItemChecked, onItemAllChecked]);
return (
<div className={`flex flex-col gap-[0.6rem] ${compactFullWidth ? "w-full min-w-0" : "w-1/2"}`}>
<div className={compactGridHeightClass}>
+81 -71
View File
@@ -580,10 +580,10 @@ const DataGrid = ({
) {
const cell =
worksheet[
XLSX.utils.encode_cell({
r: rowIndex,
c: col,
})
XLSX.utils.encode_cell({
r: rowIndex,
c: col,
})
];
if (
cell &&
@@ -659,7 +659,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}`;
@@ -783,13 +783,13 @@ const DataGrid = ({
onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean;
}
).onSorting = enableSort
? createOnSortingHandler(
columns,
setRowData,
sortDirRef,
originalRowsRef,
)
: () => false;
? createOnSortingHandler(
columns,
setRowData,
sortDirRef,
originalRowsRef,
)
: () => false;
(gridView.displayOptions as DisplayOptions).fitStyle =
GridFitStyle.EVEN;
@@ -959,7 +959,7 @@ const DataGrid = ({
) {
(
column.footer[
index
index
] as FooterItemWithCallback
).valueCallback =
footerItem.valueCallback;
@@ -1012,11 +1012,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();
@@ -1189,29 +1189,37 @@ const DataGrid = ({
style={{ display: "none" }}
onChange={handleFileChange}
/>
<div className="flex items-center justify-end gap-[0.6rem] px-[1.2rem]">
<div className={`flex items-center ${statusInfo ? 'justify-between' : 'justify-end'} gap-[0.6rem] pr-[1.2rem]`}>
{statusInfo && (
<div className="data-grid-status-bubble flex flex-wrap items-center gap-x-4 gap-y-1">
<span>
진행상황:{" "}
{statusInfo.laeSbmsnSttsCd === null ||
statusInfo.laeSbmsnSttsCd === "0"
statusInfo.laeSbmsnSttsCd === "0"
? "미작성"
: statusInfo.laeSbmsnSttsCd === "1"
? "작성중"
: statusInfo.laeSbmsnSttsCd === "2"
? "제출"
: statusInfo.laeSbmsnSttsCd === "3"
? "승인"
: statusInfo.laeSbmsnSttsCd === "4"
? "반려"
: "검증중"}
? <span className="animate-pulse">작성중</span>
: statusInfo.laeSbmsnSttsCd === "2"
? <span className="!text-[#2094fa]">제출</span>
: statusInfo.laeSbmsnSttsCd === "3"
? <span className="!text-[#1fd078]">승인</span>
: statusInfo.laeSbmsnSttsCd === "4"
? (
<span className="relative inline-flex !text-[#ff6c64] pr-3">
반려
<span className="absolute -right-0.5 -top-0.5 inline-flex size-[0.6125rem]">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[#ff6c64] opacity-75" />
<span className="relative inline-flex size-[0.6125rem] rounded-full bg-[#ff6c64]" />
</span>
</span>
)
: <span className="animate-pulse">검증중</span>}
</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
: "미제출"}
@@ -1228,26 +1236,28 @@ const DataGrid = ({
</span>
</div>
)}
{isExcelImport && (
<Button
className="h-[3.2rem] w-[96px] bg-gradient-green px-6 hover:bg-gradient-green-hover"
type="button"
onClick={importExcel}
>
<i className="diveicon di-file-excel" />
<span>가져오기</span>
</Button>
)}
{isExcelExport && (
<Button
className="h-[3.2rem] w-[96px] bg-gradient-green px-6 hover:bg-gradient-green-hover"
type="button"
onClick={exportExcel}
>
<i className="diveicon di-file-excel" />
<span>내보내기</span>
</Button>
)}
<div className="flex items-center gap-[0.6rem]">
{isExcelImport && (
<Button
className="h-[3.2rem] w-[96px] bg-gradient-green px-6 hover:bg-gradient-green-hover"
type="button"
onClick={importExcel}
>
<i className="diveicon di-file-excel" />
<span>가져오기</span>
</Button>
)}
{isExcelExport && (
<Button
className="h-[3.2rem] w-[96px] bg-gradient-green px-6 hover:bg-gradient-green-hover"
type="button"
onClick={exportExcel}
>
<i className="diveicon di-file-excel" />
<span>내보내기</span>
</Button>
)}
</div>
</div>
<div style={{ height: `${gridHeight}vh` }}>
<RealGridReact
@@ -1266,24 +1276,24 @@ const DataGrid = ({
) => boolean;
}
).onSorting = enableSort
? createOnSortingHandler(
columns,
setRowData,
sortDirRef,
originalRowsRef,
)
: () => false;
? 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(
@@ -1436,14 +1446,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={
@@ -4,15 +4,20 @@ import { useState } from "react";
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle } from "@common/components";
import SidoSggFacilityNameSearchForm from "./SidoSggFacilityNameSearchForm";
import type { SearchFormValues } from "../type/SearchForm.type";
type GridTopActionButtonProps = {
text: string;
onClick?: (helpers: { openDialog: (title?: string) => void; closeDialog: () => void }) => void;
/** 상위 화면 검색 통계연도 — 시설 조회 다이얼로그에 전달 */
statsYr?: string;
/** 상위 화면 최근 검색조건 */
initialSearchValues?: Pick<SearchFormValues, "sido" | "gugun" | "bsnmCd"> | null;
/** 다이얼로그 저장 성공 후 부모 갱신 */
onSaveSuccess?: () => Promise<void> | void;
};
const GridTopActionButton = ({ text, onClick, statsYr }: GridTopActionButtonProps) => {
const GridTopActionButton = ({ text, onClick, statsYr, initialSearchValues, onSaveSuccess }: GridTopActionButtonProps) => {
const [isOpen, setIsOpen] = useState(false);
const [title, setTitle] = useState(text);
@@ -41,7 +46,11 @@ const GridTopActionButton = ({ text, onClick, statsYr }: GridTopActionButtonProp
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
</DialogHeader>
<SidoSggFacilityNameSearchForm statsYr={statsYr} />
<SidoSggFacilityNameSearchForm
statsYr={statsYr}
initialSearchValues={initialSearchValues}
onSaveSuccess={onSaveSuccess}
/>
</DialogContent>
</Dialog>
</>
+29 -2
View File
@@ -89,6 +89,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
fctName: props.defaultValues?.fctName || "",
bsnmCd: props.defaultValues?.bsnmCd || "",
isDetail: false,
isSummary: props.defaultValues?.isSummary || "",
});
const sggKey: [string, string, string, string] | null =
@@ -132,6 +133,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
fctName: "",
bsnmCd: "",
isDetail: false,
isSummary: "",
};
setValues(resetValues);
props.onReset?.();
@@ -153,7 +155,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
const selectedBsnm =
value === "ALL"
? sidoList?.data.sidoList.find((v) => v.sido === values.sido)
?.bsnmCd || ""
?.bsnmCd || ""
: listForBsnm.find((v) => v.gugun === value)?.bsnmCd || "";
setValues((prev) => ({
@@ -250,10 +252,11 @@ const SearchForm = (props: SearchFormProps = {}) => {
)}
{mergedProps.isDetail && (
<div className="flex items-center gap-2">
<Label className="text-label">
<Label className="text-label cursor-pointer" htmlFor="isDetail">
세부 데이터 포함
</Label>
<Checkbox
id="isDetail"
checked={values.isDetail}
onCheckedChange={(value) =>
setValues({
@@ -264,6 +267,30 @@ const SearchForm = (props: SearchFormProps = {}) => {
/>
</div>
)}
{mergedProps.isSummary && (
<div className="flex items-center gap-2">
<Label className="text-label cursor-pointer">
요약표 포함
</Label>
<Select
value={values.isSummary}
onValueChange={(value) =>
setValues({ ...values, isSummary: value })
}
>
<SelectTrigger className="w-[10rem]">
<SelectValue placeholder="선택 안함"></SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL">선택 안함</SelectItem>
{[1,2,3,4,5,6,7,8,9,10].map((i) => (
<SelectItem value={i.toString()} key={i.toString()}>{`${i}개년`}</SelectItem>
))
}
</SelectContent>
</Select>
</div>
)}
{mergedProps.order && (
<div className="flex items-center gap-2">
<Label className="text-label">차수</Label>
@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useEffect, useRef, useState } from "react";
import useSWR from "swr";
import { ValueType } from "realgrid";
@@ -29,8 +29,13 @@ import type { ColumnInfo } from "../type/DataGrid.type";
import type {
SeweragePipeFacilitiesListRequest,
SeweragePipeFacilityAdminAreaListRequest,
SeweragePipeFactConnectionSaveRequest,
SeweragePipeFactConnectionSaveItem,
} from "@/entities/statistics/model/input/statistics-sewage-pipe-input-interface";
import { statisticsInputSewPipeApiService } from "@/entities/statistics/api/input/statistics-sewage-pipe-input-service";
import type { SearchFormValues } from "../type/SearchForm.type";
import { doNameList } from "../util/column/input/DoList";
import { getUserInfo } from "../../../../../packages/common/components/src/api";
/** 다이얼로그 좌측: 처리시설 정보 (순번 없음) */
const treatmentFacilityColumnInfo: ColumnInfo = {
@@ -48,21 +53,35 @@ const treatmentFacilityColumnInfo: ColumnInfo = {
editable: false,
},
{
name: "fctNm",
fieldName: "fctNm",
name: "fcltNm",
fieldName: "fcltNm",
header: { text: "시설명" },
editable: false,
},
{
name: "fctCpc",
fieldName: "fctCpc",
name: "fcltCd",
fieldName: "fcltCd",
header: { text: "시설코드" },
editable: false,
visible: false,
},
{
name: "fcltSn",
fieldName: "fcltSn",
header: { text: "시설일련번호" },
editable: false,
visible: false,
},
{
name: "fcltCpct",
fieldName: "fcltCpct",
header: { text: "시설용량(m³/일)" },
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
editable: false,
},
],
columnLayout: ["ctpvNm", "sggNm", "fctNm", "fctCpc"],
columnLayout: ["ctpvNm", "sggNm", "fcltNm", "fcltCpct", "fcltCd"],
};
/** 다이얼로그 우측: 행정구역 정보 (순번 없음, 헤더 체크박스는 showHeaderCheckbox) */
@@ -103,12 +122,26 @@ const getSggAllListFetcher = async (
type SidoSggFacilityNameSearchFormProps = {
/** 상위(SewagePipeInp 등) 검색 조건의 통계연도 — facilitiesList 등 API 필수값 */
statsYr?: string;
/** 상위(SewagePipeInp) 마지막 검색 조건 */
initialSearchValues?: Pick<SearchFormValues, "sido" | "gugun" | "bsnmCd"> | null;
/** 저장 성공 후 부모 탭 데이터 재조회 훅 */
onSaveSuccess?: () => Promise<void> | void;
};
const SidoSggFacilityNameSearchForm = ({
statsYr,
initialSearchValues,
onSaveSuccess,
}: SidoSggFacilityNameSearchFormProps) => {
const { setMessage: alert } = useAlertStore((state) => state);
const initialSidoValue =
initialSearchValues?.sido && initialSearchValues.sido !== "ALL"
? initialSearchValues.sido
: "ALL";
const initialGugunValue =
initialSearchValues?.gugun && initialSearchValues.gugun !== "ALL"
? initialSearchValues.gugun
: "ALL";
const { data: sidoList } = useSWR<ApiResponse<SearchCondition>>(
"sido-list",
@@ -134,9 +167,9 @@ const SidoSggFacilityNameSearchForm = ({
},
);
const [sido, setSido] = useState<string>("ALL");
const [gugun, setGugun] = useState<string>("ALL");
const [fctNm, setFctNm] = useState<string>("");
const [sido, setSido] = useState<string>(initialSidoValue);
const [gugun, setGugun] = useState<string>(initialGugunValue);
const [fcltNm, setFcltNm] = useState<string>("");
const [isLoading, setIsLoading] = useState<boolean>(false);
const [treatmentFacilityRows, setTreatmentFacilityRows] = useState<
@@ -145,6 +178,10 @@ const SidoSggFacilityNameSearchForm = ({
const [adminRegionRows, setAdminRegionRows] = useState<
Record<string, string | number>[]
>([]);
const [selectedFacilityRow, setSelectedFacilityRow] = useState<
Record<string, string | number> | null
>(null);
const didAutoFetchRef = useRef(false);
const sggKey: [string, string] | null =
sido && sido !== "ALL" ? ["sgg-all", sido] : null;
@@ -198,7 +235,7 @@ const SidoSggFacilityNameSearchForm = ({
alert("시도를 선택해주세요.");
return;
}
const trimmedFclty = fctNm.trim();
const trimmedFclty = fcltNm.trim();
const sewerBzmnCd = resolveSewerBzmnCd();
@@ -225,13 +262,15 @@ const SidoSggFacilityNameSearchForm = ({
list.map((item) => ({
ctpvNm: item.ctpvNm,
sggNm: item.sggNm,
fctNm: item.bscFctNm,
fctCpc: item.fctCpc,
bscFctCd: item.bscFctCd,
fcltNm: item.fcltNm,
fcltCpct: item.fcltCpct,
fcltCd: item.fcltCd,
fcltSn: item.fcltSn,
admdstCd: item.admdstCd,
statsYr: item.statsYr,
})),
);
setSelectedFacilityRow(null);
setAdminRegionRows([]);
} catch {
alert("처리시설 목록 조회 중 오류가 발생했습니다.");
@@ -247,7 +286,7 @@ const SidoSggFacilityNameSearchForm = ({
alert("상단에서 통계연도로 조회한 뒤 이용해 주세요.");
return;
}
const swtrmtFcltCd = String(row.bscFctCd ?? "").trim();
const swtrmtFcltCd = String(row.fcltCd ?? "").trim();
if (!swtrmtFcltCd) {
alert("처리시설 코드가 없습니다.");
return;
@@ -261,13 +300,14 @@ const SidoSggFacilityNameSearchForm = ({
const params: SeweragePipeFacilityAdminAreaListRequest = {
statsYr: statsYr.trim(),
swtrmtFcltCd,
sewerBzmnCd: String(row.admdstCd ?? "").trim(),
sewerBzmnCd: initialSearchValues?.bsnmCd || '',
ctpvNm,
sggNm,
...(doNameList.includes(ctpvNm) ? { sggNm } : {}),
};
try {
setIsLoading(true);
setSelectedFacilityRow(row);
const response =
await statisticsInputSewPipeApiService.getFacilityAdminAreaList(
params,
@@ -283,6 +323,8 @@ const SidoSggFacilityNameSearchForm = ({
sggNm: item.sggNm,
emdNm: item.admdstNm,
admdstCd: item.admdstCd,
isConnection: item.isConnection,
__checked: item.isConnection === "1" ? "1" : "0",
})),
);
} catch {
@@ -293,9 +335,172 @@ const SidoSggFacilityNameSearchForm = ({
};
const handleSaveAdminRegions = () => {
alert("저장 API 연동 예정입니다.");
if (!statsYr?.trim()) {
alert("상단에서 통계연도로 조회한 뒤 이용해 주세요.");
return;
}
if (!selectedFacilityRow) {
alert("처리시설을 먼저 선택해주세요.");
return;
}
const hasChanged = adminRegionRows.some(
(row) =>
(row.__checked === "1" && row.isConnection !== "1") ||
(row.__checked !== "1" && row.isConnection === "1"),
);
const saveList: SeweragePipeFactConnectionSaveItem[] = adminRegionRows
.filter((row) => row.__checked === "1")
.map((row) => ({
admdstCd: String(row.admdstCd ?? ""),
admdstNm: String(row.emdNm ?? ""),
}))
.filter((row) => row.admdstCd && row.admdstNm);
const deleteList = adminRegionRows
.filter((row) => row.__checked !== "1" && row.isConnection === "1")
.map((row) => String(row.admdstCd ?? ""))
.filter(Boolean);
if (!hasChanged) {
alert("변경된 연결 정보가 없습니다.");
return;
}
const run = async () => {
try {
setIsLoading(true);
const userInfo = await getUserInfo();
const request: SeweragePipeFactConnectionSaveRequest = {
ctpvNm: String(selectedFacilityRow.ctpvNm ?? ""),
sggNm: String(selectedFacilityRow.sggNm ?? ""),
fcltSn: String(selectedFacilityRow.fcltSn ?? ""),
statsYr: statsYr.trim(),
swtrmtFcltCd: String(selectedFacilityRow.fcltCd ?? ""),
swtrmtFcltNm: String(selectedFacilityRow.fcltNm ?? ""),
prcsFcltCpct: Number(selectedFacilityRow.fcltCpct ?? 0),
sewerBzmnCd: initialSearchValues?.bsnmCd?.trim() ?? "",
userId: userInfo.userUnqId,
saveList,
deleteList,
};
const response =
await statisticsInputSewPipeApiService.saveFactConnection(
request,
);
if (response.status !== "success") {
alert(response.message ?? "연결관계 저장에 실패했습니다.");
return;
}
setAdminRegionRows((prev) =>
prev.map((row) => ({
...row,
isConnection: row.__checked === "1" ? "1" : "0",
})),
);
await onSaveSuccess?.();
alert("저장되었습니다.");
} catch {
alert("연결관계 저장 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
void run();
};
const handleAdminRegionItemChecked = (
itemIndex: number,
checked: boolean,
) => {
setAdminRegionRows((prev) =>
prev.map((row, idx) =>
idx === itemIndex
? { ...row, __checked: checked ? "1" : "0" }
: row,
),
);
};
const handleAdminRegionItemAllChecked = (checked: boolean) => {
setAdminRegionRows((prev) =>
prev.map((row) => ({ ...row, __checked: checked ? "1" : "0" })),
);
};
useEffect(() => {
if (!initialSearchValues?.sido || initialSearchValues.sido === "ALL") return;
setSido(initialSearchValues.sido);
setGugun(
initialSearchValues.gugun && initialSearchValues.gugun !== "ALL"
? initialSearchValues.gugun
: "ALL",
);
}, [initialSearchValues?.sido, initialSearchValues?.gugun]);
useEffect(() => {
if (didAutoFetchRef.current) return;
if (!statsYr?.trim()) return;
if (!initialSearchValues?.sido || initialSearchValues.sido === "ALL") return;
const nextSido = initialSearchValues.sido;
const nextGugun =
initialSearchValues.gugun && initialSearchValues.gugun !== "ALL"
? initialSearchValues.gugun
: "ALL";
didAutoFetchRef.current = true;
setSido(nextSido);
setGugun(nextGugun);
const params: SeweragePipeFacilitiesListRequest = {
statsYr: statsYr.trim(),
...(initialSearchValues.bsnmCd?.trim()
? { sewerBzmnCd: initialSearchValues.bsnmCd.trim() }
: {}),
ctpvNm: nextSido,
...(nextGugun !== "ALL" ? { sggNm: nextGugun } : {}),
};
const run = async () => {
try {
setIsLoading(true);
const response =
await statisticsInputSewPipeApiService.getFacilitiesList(params);
if (response.status !== "success") {
alert(response.message ?? "처리시설 목록 조회에 실패했습니다.");
return;
}
const list = response.data ?? [];
setTreatmentFacilityRows(
list.map((item) => ({
ctpvNm: item.ctpvNm,
sggNm: item.sggNm,
fcltNm: item.fcltNm,
fcltCpct: item.fcltCpct,
fcltCd: item.fcltCd,
fcltSn: item.fcltSn,
admdstCd: item.admdstCd,
statsYr: item.statsYr,
})),
);
setSelectedFacilityRow(null);
setAdminRegionRows([]);
} catch {
alert("처리시설 목록 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
void run();
}, [statsYr, initialSearchValues, alert]);
return (
<div className="flex flex-col gap-4 pt-2">
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
@@ -346,8 +551,8 @@ const SidoSggFacilityNameSearchForm = ({
<Label className="shrink-0 text-label">시설명</Label>
<Input
className="min-w-[12rem] flex-1"
value={fctNm}
onChange={(e) => setFctNm(e.target.value)}
value={fcltNm}
onChange={(e) => setFcltNm(e.target.value)}
placeholder="시설명 입력"
/>
</div>
@@ -396,6 +601,8 @@ const SidoSggFacilityNameSearchForm = ({
rowData={adminRegionRows}
setRowData={setAdminRegionRows}
showHeaderCheckbox
onItemChecked={handleAdminRegionItemChecked}
onItemAllChecked={handleAdminRegionItemAllChecked}
compactFullWidth
// compactGridHeightClass="h-[min(40vh,28rem)] min-h-[16rem]"
/>
@@ -0,0 +1,93 @@
"use client";
import { useEffect, useState } from "react";
import { closestCenter, DndContext } from "@dnd-kit/core";
import { rectSortingStrategy, SortableContext } from "@dnd-kit/sortable";
import dayjs from "dayjs";
import { useUserStore } from "@/shared/lib";
import { Skeleton } from "@common/components";
import { useDndSensors } from "@/pages/statistics/lib/hooks/use-dnd-sensors";
import { usePanelOrder } from "@/pages/statistics/lib/hooks/use-panel-order";
import { getInitialPanels } from "@/pages/statistics/model/panel-config";
import { SortablePanel } from "@/pages/statistics/ui/sortable-panel";
interface PanelItem {
id: string;
component: React.ReactNode;
className?: string;
}
function Dashboard() {
const userInfo = useUserStore((state) => state.userInfo);
const [initialPanels, setInitialPanels] = useState<PanelItem[]>([]);
const [isMounted, setIsMounted] = useState(false);
const sensors = useDndSensors();
const { panels, handleDragEnd } = usePanelOrder({
initialPanels,
storageKey: "statistics-dashboard-panels-order",
});
useEffect(() => {
setIsMounted(true);
}, []);
useEffect(() => {
if (!userInfo) return;
const initialPanels = getInitialPanels({
dashboardParams: {
statsYr: String(dayjs().year() - 1),
dtlInstCd: userInfo.dtlInstCd,
enoCd: userInfo.enoCd,
laeSeCd: userInfo.laeSeCd,
},
scheduleInfoParams: {
statsYr: String(dayjs().year() - 1),
},
});
setInitialPanels(initialPanels);
}, [userInfo]);
return (
<>
{isMounted && initialPanels.length > 0 ? (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={panels.map((p) => p.id)}
strategy={rectSortingStrategy}
>
<div className="grid grid-cols-1 gap-4 p-2 md:grid-cols-2 lg:grid-cols-3">
{panels.map((panel) => (
<SortablePanel
key={panel.id}
id={panel.id}
className={panel.className}
>
{panel.component}
</SortablePanel>
))}
</div>
</SortableContext>
</DndContext>
) : (
<div className="grid grid-cols-1 gap-4 p-2 md:grid-cols-2 lg:grid-cols-3">
<Skeleton className="h-[20rem] rounded-xl bg-muted" />
<Skeleton className="h-[20rem] rounded-xl bg-muted" />
<Skeleton className="h-[20rem] rounded-xl bg-muted" />
<Skeleton className="h-[20rem] rounded-xl bg-muted" />
<Skeleton className="h-[20rem] rounded-xl bg-muted" />
<Skeleton className="h-[20rem] rounded-xl bg-muted" />
</div>
)}
</>
);
}
export default Dashboard;
@@ -1,4 +1,4 @@
import { useState, useMemo, useRef } from "react";
import { useState, useMemo, useRef, useEffect } from "react";
import SearchForm from "@/components/module/SearchForm";
import DataGrid from "@/components/module/DataGrid";
@@ -7,41 +7,44 @@ import GridTopActionButton from "@/components/module/GridTopActionButton";
import SubmitButton from "@/components/button/SumitButton";
import SaveButton from "@/components/button/SaveButton";
import LastYearDataButton from "@/components/button/LastYearDataButton";
import SubmitExceptionButton from "@/components/button/SubmitExceptionButton";
import VerificationResultButton from "@/components/button/VerificationResultButton";
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
import { SewagePipeInpColumnInfoList } from "@/components/util/column/input/sewagePipe/SewagePipeInpColumn";
import type { ColumnInfo, ColumnInfoList, ColumnConfig } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { useAlertStore } from "@/shared/lib";
import { useAlertStore, useGlobalProgressStore } from "@/shared/lib";
import { useAdminStore } from "@/store/admin-store";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { doList } from "@/components/util/column/input/DoList";
import { statisticsInputSewPipeApiService } from "@/entities/statistics/api/input/statistics-sewage-pipe-input-service";
import type {
SeweragePipeInputFactInstallStatusSaveRequest,
SeweragePipeInputRegionalFactStatusSaveRequest,
SeweragePipeInputStatusInfo,
SeweragePipeInputVerificationResult,
SeweragePipeRequestParams,
} from "@/entities/statistics/model/input/statistics-sewage-pipe-input-interface";
import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
import { FactInstallStatusInpColumn } from "@/components/util/column/input/sewagePipe/FactInstallStatusInpColumn";
import { RegionalFactStatusInpColumn } from "@/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
const applyEnterableFromStatus = (
/** 탭별 status + 입력기간으로 편집 가능 여부 */
const computeTabEnterable = (
statusInfo: SeweragePipeInputStatusInfo | null | undefined,
periodEnterable: boolean,
setIsEnterable: (v: boolean) => void
) => {
): boolean => {
if (statusInfo?.laeSbmsnSttsCd) {
if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) {
setIsEnterable(false);
} else {
setIsEnterable(periodEnterable);
return false;
}
} else {
setIsEnterable(periodEnterable);
return periodEnterable;
}
return periodEnterable;
};
function buildSewagePipeSearchParam(
@@ -58,6 +61,10 @@ function buildSewagePipeSearchParam(
}
/** ST00020006(연결)은 행이 좌·우 그리드 키에만 있음 */
type GridRowWithVerification = Record<string, string | number> & {
verificationResult?: SeweragePipeInputVerificationResult;
};
function tabRowDataExists(
m: Map<string, Record<string, string | number>[]>,
tabKey: string,
@@ -74,6 +81,43 @@ function tabRowDataExists(
return Array.isArray(rows) && rows.length > 0;
}
/** 조회 직후 해당 탭에만 저장하는 상단 툴바 노출 조건 */
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;
}
/**
* @param searchContext 조회에 사용된 검색값(해당 요청 기준으로 구군·사업자 조건 반영)
* @param treatAsSearched (hasRow || 이 값) 일 때 카드 후보 — load 내부에서는 searchDoneRef 반영용
*/
function computeTabToolbarSnapshot(
statusInfo: SeweragePipeInputStatusInfo | 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 };
}
// 하수도 통계 관리 > 02. 하수관로
const SewagePipeInp = () => {
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
@@ -81,19 +125,31 @@ const SewagePipeInp = () => {
const [activeTab, setActiveTab] = useState<string>(SewagePipeInpColumnInfoList[0]?.tabKey ?? "ST00020001");
const [isLoading, setIsLoading] = useState<boolean>(false);
const [searchValues, setSearchValues] = useState<SearchFormValues | null>(null);
const [isEnterable, setIsEnterable] = useState<boolean>(true);
const [lastSearchedYear, setLastSearchedYear] = useState<string | null>(null);
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
const [statusInfo, setStatusInfo] = useState<SeweragePipeInputStatusInfo | null>(null);
const [tabStatusInfoMap, setTabStatusInfoMap] = useState<Map<string, SeweragePipeInputStatusInfo | null>>(new Map());
// const [showDailog, setShowDailog] = useState<boolean>(false);
// const [isExceptionSubmitFlow, setIsExceptionSubmitFlow] = useState(false);
const [_statsCd, setStatsCd] = useState<string>('');
const [statsCd, setStatsCd] = useState<string>('');
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
/** API 연결 전에도 검색 후 툴바 등 UI를 쓸 수 있게 함 */
const [hasSearched, setHasSearched] = useState(false);
// const { confirm } = useConfirm();
/** checkInputPeriod — columnInfo useMemo가 입력기간 변경을 반영하도록 state로도 유지 */
const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true);
/** 탭별 조회 시점의 상단 버튼 노출 조건(해당 탭을 다시 조회할 때만 그 탭 항목 갱신) */
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<Map<string, TabToolbarSnapshot>>(
() => new Map(),
);
const { confirm } = useConfirm();
const showGlobalProgress = useGlobalProgressStore((state) => state.show);
const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress);
const hideGlobalProgress = useGlobalProgressStore((state) => state.hide);
const globalProgressVisible = useGlobalProgressStore((state) => state.visible);
const { isAdminInfo } = useAdminStore();
const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
const getFactInstallGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
const searchParamRef = useRef<SeweragePipeRequestParams | null>(null);
/** 검색 시 checkInputPeriod 결과 — 탭 전환 지연 조회 시 편집 가능 여부와 동일하게 맞춤 */
const periodEnterableRef = useRef(true);
@@ -102,23 +158,41 @@ const SewagePipeInp = () => {
const lazyTabLoggedRef = useRef<Set<string>>(new Set());
const firstTabKey = SewagePipeInpColumnInfoList[0]?.tabKey ?? "ST00020001";
const activeTabRef = useRef<string>(firstTabKey);
useEffect(() => {
activeTabRef.current = activeTab;
}, [activeTab]);
const syncTopStatusIfStillOnTab = (loadedTabKey: string, nextStatsCd: string) => {
if (loadedTabKey === activeTabRef.current) {
setStatsCd(nextStatsCd);
}
};
const handleReset = () => {
setRowDataMap(new Map());
setStatusInfo(null);
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setLastSearchedYear(null);
setLastSearchedValues(null);
setHasSearched(false);
setInputPeriodEnterable(true);
setToolbarSnapshotByTab(new Map());
searchParamRef.current = null;
searchDoneRef.current = false;
lazyTabLoggedRef.current.clear();
periodEnterableRef.current = true;
activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey);
setStatsCd("");
};
const columnInfo = useMemo(() => {
const canEdit = isEnterable || isAdminInfo;
return SewagePipeInpColumnInfoList.map((tab: ColumnInfoList) => {
const canEdit =
computeTabEnterable(tabStatusInfoMap.get(tab.tabKey) ?? null, inputPeriodEnterable) ||
isAdminInfo;
const applyEditable = (cols: ColumnConfig[] | undefined) =>
(cols ?? []).map((col: ColumnConfig) =>
canEdit
@@ -138,14 +212,36 @@ const SewagePipeInp = () => {
columns: applyEditable(tab.columns),
};
});
}, [isEnterable, isAdminInfo]);
}, [tabStatusInfoMap, isAdminInfo, inputPeriodEnterable]);
const commitToolbarSnapshotForLoadedTab = (
tabKey: string,
statusInfo: SeweragePipeInputStatusInfo | null,
hasRowData: boolean,
searchContext: SearchFormValues,
) => {
setToolbarSnapshotByTab((prev) => {
const next = new Map(prev);
next.set(
tabKey,
computeTabToolbarSnapshot(
statusInfo,
hasRowData,
searchDoneRef.current,
searchContext,
periodEnterableRef.current,
isAdminInfo,
),
);
return next;
});
};
/** mergeIntoExistingMap: true면 기존 맵에 병합(다른 탭 rowData·검색 후 다른 탭 캐시 유지). false면 맵 전체 교체 */
const loadSewagePipeActiveTabData = async (
tabKey: string,
searchParam: SeweragePipeRequestParams,
values: SearchFormValues,
checkEnterable: boolean,
mergeIntoExistingMap: boolean,
updateLastSearchedValues: boolean,
): Promise<boolean> => {
@@ -172,10 +268,16 @@ const SewagePipeInp = () => {
...d,
}));
applyRowMap((m) => m.set('ST00020001', toGridRows(convertedData)));
setStatusInfo(response.data.statusInfo);
setStatsCd(commonInfo.statsCd);
applyEnterableFromStatus(response.data.statusInfo, checkEnterable, setIsEnterable);
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
commitToolbarSnapshotForLoadedTab(
tabKey,
response.data.statusInfo,
convertedData.length > 0,
values,
);
return true;
}
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
@@ -190,10 +292,16 @@ const SewagePipeInp = () => {
...d,
}));
applyRowMap((m) => m.set('ST00020002', toGridRows(convertedData)));
setStatusInfo(response.data.statusInfo);
setStatsCd(commonInfo.statsCd);
applyEnterableFromStatus(response.data.statusInfo, checkEnterable, setIsEnterable);
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
commitToolbarSnapshotForLoadedTab(
tabKey,
response.data.statusInfo,
convertedData.length > 0,
values,
);
return true;
}
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
@@ -208,10 +316,16 @@ const SewagePipeInp = () => {
...d,
}));
applyRowMap((m) => m.set('ST00020003', toGridRows(convertedData)));
setStatusInfo(response.data.statusInfo);
setStatsCd(commonInfo.statsCd);
applyEnterableFromStatus(response.data.statusInfo, checkEnterable, setIsEnterable);
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
commitToolbarSnapshotForLoadedTab(
tabKey,
response.data.statusInfo,
convertedData.length > 0,
values,
);
return true;
}
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
@@ -226,10 +340,16 @@ const SewagePipeInp = () => {
...d,
}));
applyRowMap((m) => m.set('ST00020004', toGridRows(convertedData)));
setStatusInfo(response.data.statusInfo);
setStatsCd(commonInfo.statsCd);
applyEnterableFromStatus(response.data.statusInfo, checkEnterable, setIsEnterable);
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
commitToolbarSnapshotForLoadedTab(
tabKey,
response.data.statusInfo,
convertedData.length > 0,
values,
);
return true;
}
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
@@ -244,10 +364,16 @@ const SewagePipeInp = () => {
...d,
}));
applyRowMap((m) => m.set('ST00020005', toGridRows(convertedData)));
setStatusInfo(response.data.statusInfo);
setStatsCd(commonInfo.statsCd);
applyEnterableFromStatus(response.data.statusInfo, checkEnterable, setIsEnterable);
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
commitToolbarSnapshotForLoadedTab(
tabKey,
response.data.statusInfo,
convertedData.length > 0,
values,
);
return true;
}
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
@@ -268,10 +394,16 @@ const SewagePipeInp = () => {
m.set('treatmentPlantConnectionInp', toGridRows(facilityConvertedData));
m.set('adminAreaConnectionInp', toGridRows(adminConvertedData));
});
setStatusInfo(response.data.statusInfo);
setStatsCd(commonInfo.statsCd);
applyEnterableFromStatus(response.data.statusInfo, checkEnterable, setIsEnterable);
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
commitToolbarSnapshotForLoadedTab(
tabKey,
response.data.statusInfo,
facilityConvertedData.length > 0 || adminConvertedData.length > 0,
values,
);
return true;
}
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
@@ -281,7 +413,6 @@ const SewagePipeInp = () => {
setRowDataMap(new Map());
}
if (updateLastSearchedValues) setLastSearchedValues(values);
applyEnterableFromStatus(null, checkEnterable, setIsEnterable);
return true;
};
@@ -299,8 +430,21 @@ const SewagePipeInp = () => {
return;
}
const checkEnterable = periodResponse.data;
periodEnterableRef.current = checkEnterable;
const periodOk = isInputPeriodCurrent(periodResponse.data);
periodEnterableRef.current = periodOk;
setInputPeriodEnterable(periodOk);
/** 연도가 바뀐 검색만 전 탭 캐시 초기화 + 첫 탭으로 이동. 연도 외 조건만 바뀌면 다른 탭은 이전 조회 데이터 유지 */
const yearChangedFromLastSearch =
lastSearchedValues != null &&
(lastSearchedValues.year ?? "") !== (values.year ?? "");
if (yearChangedFromLastSearch) {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setStatsCd("");
setToolbarSnapshotByTab(new Map());
}
lazyTabLoggedRef.current.clear();
@@ -312,7 +456,15 @@ const SewagePipeInp = () => {
setHasSearched(true);
setLastSearchedYear(values.year ?? null);
await loadSewagePipeActiveTabData(activeTab, searchParam, values, checkEnterable, true, true);
const tabKeyToLoad = yearChangedFromLastSearch ? firstTabKey : activeTab;
if (yearChangedFromLastSearch) {
activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey);
} else {
activeTabRef.current = activeTab;
}
await loadSewagePipeActiveTabData(tabKeyToLoad, searchParam, values, true, true);
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
} finally {
@@ -321,7 +473,9 @@ const SewagePipeInp = () => {
};
const handleActiveTabChange = async (tabKey: string) => {
activeTabRef.current = tabKey;
setActiveTab(tabKey);
setStatsCd(tabStatsCdMap.get(tabKey) ?? "");
const hasActiveTabData = tabRowDataExists(rowDataMap, tabKey);
@@ -339,14 +493,7 @@ const SewagePipeInp = () => {
try {
setIsLoading(true);
const ok = await loadSewagePipeActiveTabData(
tabKey,
searchParam,
values,
periodEnterableRef.current,
true,
false,
);
const ok = await loadSewagePipeActiveTabData(tabKey, searchParam, values, true, false);
if (ok) lazyTabLoggedRef.current.add(tabKey);
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
@@ -355,11 +502,6 @@ const SewagePipeInp = () => {
}
};
const SubmitExceptionFunc = () => {
console.log('submit exception');
// 예외 제출 로직
};
const getLastYearDataFunc = async () => {
const values = lastSearchedValues ?? searchValues;
if (!values?.sido || values.sido === 'ALL') {
@@ -373,7 +515,7 @@ const SewagePipeInp = () => {
try {
setIsLoading(true);
const param = buildSewagePipeSearchParam(values, { mode: 'PRE' });
await loadSewagePipeActiveTabData(activeTab, param, values, periodEnterableRef.current, true, false);
await loadSewagePipeActiveTabData(activeTab, param, values, true, false);
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
} finally {
@@ -386,115 +528,237 @@ const SewagePipeInp = () => {
// 제출 로직
};
const saveFunc = async () => {
if (activeTab !== "ST00020001") {
const refreshConnectionStatusAfterPopupSave = async () => {
const searchParam = searchParamRef.current;
const values = lastSearchedValues ?? searchValues;
if (!searchParam || !values) return;
await loadSewagePipeActiveTabData("ST00020006", searchParam, values, true, false);
};
const openVerificationHistoryDialog = () => {
const activeTabStatsCd = tabStatsCdMap.get(activeTab) ?? statsCd;
if (!searchValues?.year || !searchValues?.sido || !searchValues?.bsnmCd || !activeTabStatsCd) {
alert("먼저 조회를 진행해주세요.");
return;
}
setVerificationHistoryOpen(true);
};
const saveFunc = async () => {
if (!(await confirm("저장 하시겠습니까?"))) return;
const values = lastSearchedValues ?? searchValues;
if (!values?.year) {
alert("검색 후 저장해 주세요.");
return;
}
try {
const currentData = getGridDataRef.current?.();
if (!currentData || currentData.length === 0) {
alert("저장할 데이터가 없습니다.");
return;
}
const values = lastSearchedValues ?? searchValues;
if (!values?.year) {
alert("검색 후 저장해 주세요.");
return;
}
showGlobalProgress({ label: "저장 진행 중...", progress: 0 });
const columnFormatMap = new Map<string, string>();
RegionalFactStatusInpColumn.columns.forEach((col) => {
if (col.fieldName && col.dataFormat) {
columnFormatMap.set(col.fieldName, col.dataFormat);
switch (activeTab) {
case "ST00020001": {
const currentData = getGridDataRef.current?.();
if (!currentData || currentData.length === 0) {
alert("저장할 데이터가 없습니다.");
return;
}
const columnFormatMap = new Map<string, string>();
RegionalFactStatusInpColumn.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 userInfo = await getUserInfo();
const saveList = currentData.map((d) => ({
admdstCd: String(d.admdstCd ?? ""),
sggNm: String(d.sggNm ?? ""),
admdstNm: String(d.admdstNm ?? ""),
goalYr: String(d.goalYr ?? ""),
wholPplMtncPlanPrlg: formatNumber(d.wholPplMtncPlanPrlg, "wholPplMtncPlanPrlg"),
wholPlanPrlg: formatNumber(d.wholPlanPrlg, "wholPlanPrlg"),
wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"),
swagePplPsplyRt: formatNumber(d.swagePplPsplyRt, "swagePplPsplyRt"),
cbsysPplMtncPlanArea: formatNumber(d.cbsysPplMtncPlanArea, "cbsysPplMtncPlanArea"),
cbsysPplMtncPlanPrlg: formatNumber(d.cbsysPplMtncPlanPrlg, "cbsysPplMtncPlanPrlg"),
cbsysPlanPrlg: formatNumber(d.cbsysPlanPrlg, "cbsysPlanPrlg"),
cbsysFcltPrlg: formatNumber(d.cbsysFcltPrlg, "cbsysFcltPrlg"),
cbsysSqrePrlg: formatNumber(d.cbsysSqrePrlg, "cbsysSqrePrlg"),
cbsysCiclPrlg: formatNumber(d.cbsysCiclPrlg, "cbsysCiclPrlg"),
cbsysOpnPrlg: formatNumber(d.cbsysOpnPrlg, "cbsysOpnPrlg"),
cbsysGttrPrlg: formatNumber(d.cbsysGttrPrlg, "cbsysGttrPrlg"),
drtwPplMtncPlanArea: formatNumber(d.drtwPplMtncPlanArea, "drtwPplMtncPlanArea"),
drtwPplMtncPlanPrlg: formatNumber(d.drtwPplMtncPlanPrlg, "drtwPplMtncPlanPrlg"),
drtwPplPlanPrlg: formatNumber(d.drtwPplPlanPrlg, "drtwPplPlanPrlg"),
drtwPplFcltPrlg: formatNumber(d.drtwPplFcltPrlg, "drtwPplFcltPrlg"),
drtwPplSqrePrlg: formatNumber(d.drtwPplSqrePrlg, "drtwPplSqrePrlg"),
drtwPplCiclPrlg: formatNumber(d.drtwPplCiclPrlg, "drtwPplCiclPrlg"),
exlnPplMtncPlanArea: formatNumber(d.exlnPplMtncPlanArea, "exlnPplMtncPlanArea"),
exlnPplMtncPlanPrlg: formatNumber(d.exlnPplMtncPlanPrlg, "exlnPplMtncPlanPrlg"),
exlnPplPlanPrlg: formatNumber(d.exlnPplPlanPrlg, "exlnPplPlanPrlg"),
exlnPplFcltPrlg: formatNumber(d.exlnPplFcltPrlg, "exlnPplFcltPrlg"),
exlnPplSqrePrlg: formatNumber(d.exlnPplSqrePrlg, "exlnPplSqrePrlg"),
exlnPplCiclPrlg: formatNumber(d.exlnPplCiclPrlg, "exlnPplCiclPrlg"),
exlnPplOpnPrlg: formatNumber(d.exlnPplOpnPrlg, "exlnPplOpnPrlg"),
exlnPplGttrPrlg: formatNumber(d.exlnPplGttrPrlg, "exlnPplGttrPrlg"),
cbsysMnhlCnt: formatNumber(d.cbsysMnhlCnt, "cbsysMnhlCnt"),
sprsysDrtwMnhlCnt: formatNumber(d.sprsysDrtwMnhlCnt, "sprsysDrtwMnhlCnt"),
sprsysExlnMnhlCnt: formatNumber(d.sprsysExlnMnhlCnt, "sprsysExlnMnhlCnt"),
drtwRcvprpCnt: formatNumber(d.drtwRcvprpCnt, "drtwRcvprpCnt"),
exlnRcvprpCnt: formatNumber(d.exlnRcvprpCnt, "exlnRcvprpCnt"),
stofcbrCnt: formatNumber(d.stofcbrCnt, "stofcbrCnt"),
exlnOtflCnt: formatNumber(d.exlnOtflCnt, "exlnOtflCnt"),
sewerLdgrEdpPrlg: formatNumber(d.sewerLdgrEdpPrlg, "sewerLdgrEdpPrlg"),
sewerLdgrEdpRt: formatNumber(d.sewerLdgrEdpRt, "sewerLdgrEdpRt"),
rmrk: (d.rmrk as string | null) ?? null,
}));
const param: SeweragePipeInputRegionalFactStatusSaveRequest = {
statsYr: values.year ?? "",
ctpvNm: values.sido ?? "",
sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""),
sewerBzmnCd: values.bsnmCd ?? "",
userId: userInfo.userUnqId,
superAdmin: isAdminInfo ? "superAdmin" : "",
saveList,
};
const response =
await statisticsInputSewPipeApiService.saveRegionalFactStatus(param);
if (response.status === "success") {
setGlobalProgress(50);
const refreshedParam = buildSewagePipeSearchParam(values);
await loadSewagePipeActiveTabData(activeTab, refreshedParam, values, true, false);
setGlobalProgress(100);
alert("저장되었습니다.");
} else {
alert(response.message || "데이터 저장 중 오류가 발생했습니다.");
}
break;
}
});
case "ST00020002": {
const currentData = getFactInstallGridDataRef.current?.();
if (!currentData || currentData.length === 0) {
alert("저장할 데이터가 없습니다.");
return;
}
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 columnFormatMap = new Map<string, string>();
FactInstallStatusInpColumn.columns.forEach((col) => {
if (col.fieldName && col.dataFormat) {
columnFormatMap.set(col.fieldName, col.dataFormat);
}
});
const userInfo = await getUserInfo();
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 saveList = currentData.map((d) => ({
admdstCd: String(d.admdstCd ?? ""),
sggNm: String(d.sggNm ?? ""),
admdstNm: String(d.admdstNm ?? ""),
goalYr: String(d.goalYr ?? ""),
wholPplMtncPlanPrlg: formatNumber(d.wholPplMtncPlanPrlg, "wholPplMtncPlanPrlg"),
wholPlanPrlg: formatNumber(d.wholPlanPrlg, "wholPlanPrlg"),
wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"),
swagePplPsplyRt: formatNumber(d.swagePplPsplyRt, "swagePplPsplyRt"),
cbsysPplMtncPlanArea: formatNumber(d.cbsysPplMtncPlanArea, "cbsysPplMtncPlanArea"),
cbsysPplMtncPlanPrlg: formatNumber(d.cbsysPplMtncPlanPrlg, "cbsysPplMtncPlanPrlg"),
cbsysPlanPrlg: formatNumber(d.cbsysPlanPrlg, "cbsysPlanPrlg"),
cbsysFcltPrlg: formatNumber(d.cbsysFcltPrlg, "cbsysFcltPrlg"),
cbsysSqrePrlg: formatNumber(d.cbsysSqrePrlg, "cbsysSqrePrlg"),
cbsysCiclPrlg: formatNumber(d.cbsysCiclPrlg, "cbsysCiclPrlg"),
cbsysOpnPrlg: formatNumber(d.cbsysOpnPrlg, "cbsysOpnPrlg"),
cbsysGttrPrlg: formatNumber(d.cbsysGttrPrlg, "cbsysGttrPrlg"),
drtwPplMtncPlanArea: formatNumber(d.drtwPplMtncPlanArea, "drtwPplMtncPlanArea"),
drtwPplMtncPlanPrlg: formatNumber(d.drtwPplMtncPlanPrlg, "drtwPplMtncPlanPrlg"),
drtwPplPlanPrlg: formatNumber(d.drtwPplPlanPrlg, "drtwPplPlanPrlg"),
drtwPplFcltPrlg: formatNumber(d.drtwPplFcltPrlg, "drtwPplFcltPrlg"),
drtwPplSqrePrlg: formatNumber(d.drtwPplSqrePrlg, "drtwPplSqrePrlg"),
drtwPplCiclPrlg: formatNumber(d.drtwPplCiclPrlg, "drtwPplCiclPrlg"),
exlnPplMtncPlanArea: formatNumber(d.exlnPplMtncPlanArea, "exlnPplMtncPlanArea"),
exlnPplMtncPlanPrlg: formatNumber(d.exlnPplMtncPlanPrlg, "exlnPplMtncPlanPrlg"),
exlnPplPlanPrlg: formatNumber(d.exlnPplPlanPrlg, "exlnPplPlanPrlg"),
exlnPplFcltPrlg: formatNumber(d.exlnPplFcltPrlg, "exlnPplFcltPrlg"),
exlnPplSqrePrlg: formatNumber(d.exlnPplSqrePrlg, "exlnPplSqrePrlg"),
exlnPplCiclPrlg: formatNumber(d.exlnPplCiclPrlg, "exlnPplCiclPrlg"),
exlnPplOpnPrlg: formatNumber(d.exlnPplOpnPrlg, "exlnPplOpnPrlg"),
exlnPplGttrPrlg: formatNumber(d.exlnPplGttrPrlg, "exlnPplGttrPrlg"),
cbsysMnhlCnt: formatNumber(d.cbsysMnhlCnt, "cbsysMnhlCnt"),
sprsysDrtwMnhlCnt: formatNumber(d.sprsysDrtwMnhlCnt, "sprsysDrtwMnhlCnt"),
sprsysExlnMnhlCnt: formatNumber(d.sprsysExlnMnhlCnt, "sprsysExlnMnhlCnt"),
drtwRcvprpCnt: formatNumber(d.drtwRcvprpCnt, "drtwRcvprpCnt"),
exlnRcvprpCnt: formatNumber(d.exlnRcvprpCnt, "exlnRcvprpCnt"),
stofcbrCnt: formatNumber(d.stofcbrCnt, "stofcbrCnt"),
exlnOtflCnt: formatNumber(d.exlnOtflCnt, "exlnOtflCnt"),
sewerLdgrEdpPrlg: formatNumber(d.sewerLdgrEdpPrlg, "sewerLdgrEdpPrlg"),
sewerLdgrEdpRt: formatNumber(d.sewerLdgrEdpRt, "sewerLdgrEdpRt"),
rmrk: (d.rmrk as string | null) ?? null,
}));
const userInfo = await getUserInfo();
const param: SeweragePipeInputRegionalFactStatusSaveRequest = {
statsYr: values.year ?? "",
ctpvNm: values.sido ?? "",
sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""),
sewerBzmnCd: values.bsnmCd ?? "",
userId: userInfo.userUnqId,
superAdmin: isAdminInfo ? "superAdmin" : "",
saveList,
};
const saveList = 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 ?? ""),
}));
console.log(param);
const param: SeweragePipeInputFactInstallStatusSaveRequest = {
statsYr: values.year ?? "",
ctpvNm: values.sido ?? "",
sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""),
sewerBzmnCd: values.bsnmCd ?? "",
userId: userInfo.userUnqId,
superAdmin: isAdminInfo ? "superAdmin" : "",
saveList,
};
const response =
await statisticsInputSewPipeApiService.saveFactInstallStatus(param);
if (response.status === "success") {
setGlobalProgress(50);
const refreshedParam = buildSewagePipeSearchParam(values);
await loadSewagePipeActiveTabData(activeTab, refreshedParam, values, true, false);
setGlobalProgress(100);
alert("저장되었습니다.");
} else {
alert(response.message || "데이터 저장 중 오류가 발생했습니다.");
}
break;
}
default:
alert("현재 탭 저장 API 연동 전입니다.");
break;
}
} catch {
alert("저장 준비 중 오류가 발생했습니다.");
alert("데이터 저장 중 오류가 발생했습니다.");
} finally {
hideGlobalProgress();
}
};
const gugunOkOrNotDoList = doList.includes(lastSearchedValues?.bsnmCd || '')
? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL')
: true;
const gugunOkOrNotDoList = buildGugunOkForToolbar(lastSearchedValues);
const showSaveInToolbar = (isEnterable || isAdminInfo) && gugunOkOrNotDoList;
const showEnterableToolbarButtons = isEnterable && gugunOkOrNotDoList;
const searchContextForToolbar = lastSearchedValues ?? searchValues;
const activeTabToolbar =
toolbarSnapshotByTab.get(activeTab) ??
computeTabToolbarSnapshot(
tabStatusInfoMap.get(activeTab) ?? null,
tabRowDataExists(rowDataMap, activeTab),
hasSearched,
searchContextForToolbar,
inputPeriodEnterable,
isAdminInfo,
);
/** rowDataMap — 행이 하나라도 있을 때만 툴바·엑셀 의미 있음 */
const hasRowData = Array.from(rowDataMap.values()).some((rows) => rows.length > 0);
const excelToolbarEnabled = hasRowData && isEnterable && gugunOkOrNotDoList;
const showActionToolbarCard =
(hasRowData || hasSearched) &&
(showSaveInToolbar ||
(statusInfo?.laeSbmsnSttsCd === '2' && (isAdminInfo || (isEnterable && gugunOkOrNotDoList))));
const {
showSaveInToolbar,
showEnterableToolbarButtons,
showActionToolbarCard,
} = activeTabToolbar;
return (
<>
@@ -502,9 +766,11 @@ const SewagePipeInp = () => {
{showActionToolbarCard && (
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
/>
{showEnterableToolbarButtons && (
<>
<SubmitExceptionButton SubmitExceptionFunc={SubmitExceptionFunc} />
<LastYearDataButton getLastYearDataFunc={() => void getLastYearDataFunc()} />
<SubmitButton submitFunc={submitFunc} />
</>
@@ -513,7 +779,7 @@ const SewagePipeInp = () => {
</div>
</div>
)}
{isLoading && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
{isLoading && !globalProgressVisible && <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>
@@ -534,6 +800,14 @@ const SewagePipeInp = () => {
</TabsList>
{columnInfo.map((info) => {
const tabKey = info.tabKey;
const tabEnterableForGrid = computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable,
);
const excelToolbarEnabledForTab =
tabRowDataExists(rowDataMap, tabKey) &&
tabEnterableForGrid &&
gugunOkOrNotDoList;
if (info.isSplitGrid && info.left && info.right) {
const leftKey = info.left.tabKey || `${tabKey}_left`;
@@ -544,7 +818,7 @@ const SewagePipeInp = () => {
const showSplitTopButton =
Boolean(splitTopButton?.text) &&
(splitTopButton?.visible ?? true) &&
excelToolbarEnabled;
excelToolbarEnabledForTab;
const leftColumnInfo: ColumnInfo = {
columns: info.left.columns ?? [],
columnLayout: info.left.columnLayout ?? [],
@@ -563,6 +837,16 @@ const SewagePipeInp = () => {
text={splitTopButton?.text ?? ""}
onClick={splitTopButton?.onClick}
statsYr={lastSearchedYear ?? undefined}
initialSearchValues={
lastSearchedValues
? {
sido: lastSearchedValues.sido,
gugun: lastSearchedValues.gugun,
bsnmCd: lastSearchedValues.bsnmCd,
}
: null
}
onSaveSuccess={refreshConnectionStatusAfterPopupSave}
/>
)}
</div>
@@ -618,6 +902,7 @@ const SewagePipeInp = () => {
return (
<TabsContent key={tabKey} value={tabKey}>
<DataGrid
key={(tabEnterableForGrid || isAdminInfo) ? "enterable" : "readonly"}
columnInfo={gridColumnInfo}
rowStartNum={info.rowStartNum}
excelExportFileSuffix={info.tabLabel || tabKey}
@@ -632,18 +917,69 @@ const SewagePipeInp = () => {
return next;
});
}}
isExcelImport={excelToolbarEnabled}
isExcelImport={excelToolbarEnabledForTab}
isExcelExport
showHeaderCheckbox={false}
fixedColumns={info.fixedColumns}
enableSort={false}
getGridDataRef={tabKey === "ST00020001" ? getGridDataRef : undefined}
statusInfo={tabStatusInfoMap.get(tabKey) ?? null}
getGridDataRef={
tabKey === "ST00020001"
? getGridDataRef
: tabKey === "ST00020002"
? getFactInstallGridDataRef
: undefined
}
cellStyleCallback={({ row, fieldName }) => {
const verification = (row as GridRowWithVerification).verificationResult;
if (!verification) return;
const errors = verification[fieldName];
if (Array.isArray(errors) && errors.length > 0) {
const hasImr = errors.some((e) => e.vrfcCd === "IMR");
return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error";
}
}}
cellTooltipCallback={({ row, fieldName }) => {
const verification = (row as GridRowWithVerification).verificationResult;
if (!verification) return;
const errors = verification[fieldName];
if (Array.isArray(errors) && errors.length > 0) {
return errors
.map((e) => {
const prefix =
e.vrfcCd === "IMR" ? "IMR검증 - " : "일반검증 - ";
const comp = e.compVlCn ?? "";
const now = e.nowVlCn ?? "";
return `${prefix}${e.rsnExpln}(검증 기준값: ${comp} / 현재 입력값: ${now})`;
})
.filter(Boolean)
.join("\n");
}
}}
/>
</TabsContent>
);
})}
</Tabs>
</div>
<VerificationResultHistoryDialog
open={verificationHistoryOpen}
onOpenChange={setVerificationHistoryOpen}
queryContext={
searchValues?.year &&
searchValues?.sido &&
searchValues?.bsnmCd &&
(tabStatsCdMap.get(activeTab) ?? statsCd)
? {
sewerBzmnCd: searchValues.bsnmCd,
ctpvNm: searchValues.sido,
sggNm: searchValues.gugun === "ALL" ? "" : (searchValues.gugun ?? ""),
statsYr: searchValues.year,
statsCd: tabStatsCdMap.get(activeTab) ?? statsCd,
}
: null
}
/>
</>
);
};
@@ -24,13 +24,16 @@ import { getUserInfo } from "../../../../../../packages/common/components/src/ap
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import type {
SewerageSupplyRateItem,
SewerageSupplyRateRequestParams,
SewerageSupplyRateSaveItem,
SewerageSupplyRateVerificationResult,
SewerageSupplyRateSaveRequest,
SewerageSupplyRateStatusInfo,
import {
type CheckInputPeriodItem,
type SewerageSupplyRateItem,
type SewerageSupplyRateRequestParams,
type SewerageSupplyRateSaveItem,
type SewerageSupplyRateVerificationResult,
type SewerageSupplyRateSaveRequest,
type SewerageSupplyRateStatusInfo,
getVerificationPeriodActionBlockedMessage,
isInputPeriodCurrent,
} from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
import type {
cancelSubmitRequest,
@@ -100,6 +103,8 @@ const SeweragePenetrationRateInp = () => {
const [lastSearchedYear, setLastSearchedYear] = useState<string | null>(null);
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
const [statusInfo, setStatusInfo] = useState<SewerageSupplyRateStatusInfo | null>(null);
/** 마지막 조회 시 check-input-period 응답 (검증기간 여부 판단) */
const [periodCheckList, setPeriodCheckList] = useState<CheckInputPeriodItem[]>([]);
const [showDailog, setShowDailog] = useState<boolean>(false);
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false);
@@ -147,6 +152,7 @@ const SeweragePenetrationRateInp = () => {
const handleReset = () => {
setRowData([]);
setStatusInfo(null);
setPeriodCheckList([]);
setLastSearchedYear(null);
setLastSearchedValues(null);
setFooterFromApi(null);
@@ -218,8 +224,9 @@ const SeweragePenetrationRateInp = () => {
const response = await statisticsInputSewSupplyRateApi.checkInputPeriod(values.year || '');
if (response.status === 'success') {
const checkEnterable = response.data;
// setIsEnterable(response.data);
const periods = Array.isArray(response.data) ? response.data : [];
setPeriodCheckList(periods);
const checkEnterable = isInputPeriodCurrent(periods);
setLastSearchedYear(values.year || null);
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values), checkEnterable);
setLastSearchedValues(values);
@@ -232,6 +239,11 @@ const SeweragePenetrationRateInp = () => {
};
const SubmitExceptionFunc = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
if (verificationBlocked) {
alert(verificationBlocked);
return;
}
const userInfo = await getUserInfo();
const params: SubmitExceptionRequest = {
@@ -309,6 +321,11 @@ const SeweragePenetrationRateInp = () => {
};
const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
if (verificationBlocked) {
alert(verificationBlocked);
return;
}
try {
showGlobalProgress({ label: "제출 진행 중...", progress: 0 });
const userInfo = await getUserInfo();
@@ -388,7 +405,7 @@ const SeweragePenetrationRateInp = () => {
const response = await statisticsInputSewSupplyRateApi.submitSewerageSupplyRate(requestData);
if (response.status === 'success') {
setGlobalProgress(50);
if (response.data.finalStatus === '5') {
const imrParam = {
user_id: userInfo.userUnqId,
@@ -442,6 +459,11 @@ const SeweragePenetrationRateInp = () => {
const saveFunc = async () => {
try {
if (await confirm('저장 하시겠습니까?')) {
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
if (verificationBlocked) {
alert(verificationBlocked);
return;
}
showGlobalProgress({ label: "저장 진행 중...", progress: 0 });
const userInfo = await getUserInfo();
@@ -520,9 +542,9 @@ const SeweragePenetrationRateInp = () => {
sewerBzmnFrgnrWholPpltnCnt: footerFromApi?.sewerBzmnFrgnrWholPpltnCnt // 행정참고자료 외국인 합계
};
// if (isAdminInfo) {
// requestData.superAdmin = 'superAdmin';
// }
if (isAdminInfo) {
requestData.superAdmin = 'superAdmin';
}
const response = await statisticsInputSewSupplyRateApi.saveSewerageSupplyRate(requestData);
@@ -547,6 +569,11 @@ const SeweragePenetrationRateInp = () => {
const cnacelSubmitFunc = async () => {
try {
if (await confirm('제출된 내용을 취소하시겠습니까?')) {
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
if (verificationBlocked) {
alert(verificationBlocked);
return;
}
const userInfo = await getUserInfo();
@@ -600,11 +627,11 @@ const SeweragePenetrationRateInp = () => {
{showActionToolbarCard && (
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
/>
{showEnterableToolbarButtons && (
<>
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
/>
<LastYearDataButton getLastYearDataFunc={() => getLastYearDataFunc(searchValues ?? {})} />
<SubmitButton submitFunc={() => submitFunc(false)} />
</>
@@ -12,7 +12,6 @@ import type {
ApiResponse,
SearchCondition,
Sgg,
StatisticsLog,
StatisticsLogRequest,
} from "@/entities/statistics/model/statistics-interface";
import {
@@ -0,0 +1,22 @@
const CellColorInfo = () => {
return (
<div className="m-[0.6rem] flex justify-end">
<div className="flex justify-end items-center gap-[1.2rem] !text-xl bg-white w-fit py-3 px-5 border border-slate-200/80 rounded-md">
<div className="flex justify-center items-center gap-x-2">
<div className="w-8 h-6 bg-[#feee85]"></div>
<span>요약표</span>
</div>
<div className="flex justify-center items-center gap-x-2">
<div className="w-8 h-6 bg-[#fff7bf]"></div>
<span>합계</span>
</div>
<div className="flex justify-center items-center gap-x-2">
<div className="w-8 h-6 bg-[#ffe9cf]"></div>
<span>소계</span>
</div>
</div>
</div>
);
};
export default CellColorInfo;
@@ -1,10 +1,202 @@
import { useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib";
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SearchForm from "@/components/module/SearchForm";
import DataGrid from "@/components/module/DataGrid";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewagePipeMaintainColumnListTot } from "@/components/util/column/total/sewagePipeMaintain/sewagePipeMaintainColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import type { SewagePipeMaintainTotalRequest } from "@/entities/statistics/model/statistics-total/statistics-sewage-pipe-maintain-total-interface";
const firstTabKey = SewagePipeMaintainColumnListTot[0]?.tabKey ?? "ST00030001";
function buildSewagePipeMaintainTotSearchParam(values: SearchFormValues): SewagePipeMaintainTotalRequest {
return {
ctpvNm: values.sido === "ALL" ? "" : values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun,
sewerBzmnCd: values.bsnmCd,
statsYr: values.year,
rowDataYn: values.isDetail,
};
}
const SewagePipeMaintainTot = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
const [isLoading, setIsLoading] = useState(false);
const [activeTab, setActiveTab] = useState(firstTabKey);
const lastSearchedValuesRef = useRef<SearchFormValues | null>(null);
const columnInfo = SewagePipeMaintainColumnListTot;
const handleReset = () => {
setRowDataMap(new Map());
setActiveTab(firstTabKey);
lastSearchedValuesRef.current = null;
};
const loadSewagePipeMaintainTotTabData = async (
tabKey: string,
values: SearchFormValues,
mergeIntoExistingMap: boolean,
): Promise<boolean> => {
const param = buildSewagePipeMaintainTotSearchParam(values);
const applyRows = (rows: Record<string, string | number>[]) => {
if (mergeIntoExistingMap) {
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(tabKey, rows);
return next;
});
} else {
const next = new Map<string, Record<string, string | number>[]>();
next.set(tabKey, rows);
setRowDataMap(next);
}
};
if (tabKey === "ST00030001") {
const response = await statisticsTotalStatusApi.getSewagePipeMaintainRepairTotal(param);
if (response.status === "success" && response.data) {
applyRows(toGridRows(response.data as object[]));
return true;
}
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
return false;
}
if (tabKey === "ST00030002") {
const response = await statisticsTotalStatusApi.getSewagePipeMaintainDredgingTotal(param);
if (response.status === "success" && response.data) {
applyRows(toGridRows(response.data as object[]));
return true;
}
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
return false;
}
return false;
};
const handleSearch = async (values: SearchFormValues) => {
try {
setIsLoading(true);
let ok = false;
if (activeTab === "ST00030001" || activeTab === "ST00030002") {
ok = await loadSewagePipeMaintainTotTabData(activeTab, values, true);
}
if (ok) {
lastSearchedValuesRef.current = values;
} else {
lastSearchedValuesRef.current = null;
}
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
lastSearchedValuesRef.current = null;
} finally {
setIsLoading(false);
}
};
const handleActiveTabChange = async (tabKey: string) => {
setActiveTab(tabKey);
const values = lastSearchedValuesRef.current;
if (!values) return;
const existing = rowDataMap.get(tabKey);
const hasData = Array.isArray(existing) && existing.length > 0;
if (hasData) return;
if (tabKey === "ST00030001" || tabKey === "ST00030002") {
try {
setIsLoading(true);
await loadSewagePipeMaintainTotTabData(tabKey, values, true);
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
}
};
return (
<>
<SearchForm />
</>
<div className="relative">
<SearchForm isDetail={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]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<Tabs
value={activeTab}
onValueChange={(v) => void handleActiveTabChange(v)}
className="w-full"
variant="default"
>
<TabsList>
{columnInfo.map((info) => (
<TabsTrigger key={info.tabKey} value={info.tabKey}>
{info.tabLabel || info.tabKey}
</TabsTrigger>
))}
</TabsList>
{columnInfo.map((info) => {
const tabKey = info.tabKey;
if (!info.columns || !info.columnLayout) {
return null;
}
const rowData = rowDataMap.get(tabKey) ?? [];
const gridColumnInfo: ColumnInfo = {
columns: info.columns,
columnLayout: info.columnLayout,
};
return (
<TabsContent key={tabKey} value={tabKey}>
<DataGrid
columnInfo={gridColumnInfo}
rowStartNum={info.rowStartNum}
excelExportFileSuffix={info.tabLabel || tabKey}
rowData={rowData}
setRowData={(updater) => {
setRowDataMap((prev) => {
const next = new Map(prev);
const cur = next.get(tabKey) ?? [];
const n =
typeof updater === "function" ? updater(cur) : updater;
next.set(tabKey, n);
return next;
});
}}
isExcelImport={false}
isExcelExport
showHeaderCheckbox={false}
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";
}}
/>
</TabsContent>
);
})}
</Tabs>
</div>
</div>
);
};
@@ -1,4 +1,4 @@
import { useMemo, useState } from "react";
import { useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib";
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
@@ -6,71 +6,165 @@ import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "
import SearchForm from "@/components/module/SearchForm";
import DataGrid from "@/components/module/DataGrid";
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
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";
const firstTabKey = SewagePipeColumnInfoListTot[0]?.tabKey ?? "ST00020001";
function buildSewagePipeTotSearchParam(values: SearchFormValues): SewagePipeTotalRequest {
return {
ctpvNm: values.sido === "ALL" ? "" : values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun,
sewerBzmnCd: values.bsnmCd,
statsYr: values.year,
rowDataYn: values.isDetail,
};
}
const SewagePipeTot = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
const [isLoading, setIsLoading] = useState(false);
const [activeTab, setActiveTab] = useState(firstTabKey);
/** 조회 성공 후 탭 전환 시 동일 조건으로 지연 조회 */
const lastSearchedValuesRef = useRef<SearchFormValues | null>(null);
const columnInfo = useMemo(
() =>
SewagePipeColumnInfoListTot.map((tab: ColumnInfoList) => ({
...tab,
columns: (tab.columns ?? []).map((col: ColumnConfig) => ({
...col,
editable: false as const,
styleName: col.styleName ?? ("rg-cell-readonly" as const),
})),
})),
[],
);
const columnInfo = SewagePipeColumnInfoListTot;
const handleReset = () => {
setRowDataMap(new Map());
setActiveTab(firstTabKey);
lastSearchedValuesRef.current = null;
};
/** SewagePipeInp의 `loadSewagePipeActiveTabData`와 동일 역할: 탭키별 API 호출 후 `rowDataMap` 반영 */
const loadSewagePipeTotTabData = async (
tabKey: string,
values: SearchFormValues,
mergeIntoExistingMap: boolean,
): Promise<boolean> => {
const param = buildSewagePipeTotSearchParam(values);
const applyRows = (rows: Record<string, string | number>[]) => {
if (mergeIntoExistingMap) {
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(tabKey, rows);
return next;
});
} else {
const next = new Map<string, Record<string, string | number>[]>();
next.set(tabKey, rows);
setRowDataMap(next);
}
};
if (tabKey === "ST00020001") {
const response = await statisticsTotalStatusApi.getSewagePipeRegionalStatusList(param);
if (response.status === "success" && response.data) {
applyRows(toGridRows(response.data as object[]));
return true;
}
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
return false;
}
if (tabKey === "ST00020002") {
const response = await statisticsTotalStatusApi.getSewagePipeFactInstallStatus(param);
if (response.status === "success" && response.data) {
applyRows(toGridRows(response.data as object[]));
return true;
}
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
return false;
}
if (tabKey === "ST00020003") {
const response = await statisticsTotalStatusApi.getSewagePipePipeTypeFacilityStatus(param);
if (response.status === "success" && response.data) {
applyRows(toGridRows(response.data as object[]));
return true;
}
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
return false;
}
if (tabKey === "ST00020004") {
const response = await statisticsTotalStatusApi.getSewagePipePipeDiamStatus(param);
if (response.status === "success" && response.data) {
applyRows(toGridRows(response.data as object[]));
return true;
}
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
return false;
}
return false;
};
const handleSearch = async (values: SearchFormValues) => {
setActiveTab(firstTabKey);
setRowDataMap(new Map());
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.getSewagePipeRegionalStatusList(param);
if (response.status === "success" && response.data) {
const next = new Map<string, Record<string, string | number>[]>();
next.set("ST00020001", toGridRows(response.data as object[]));
setRowDataMap(next);
// SewagePipeInp과 동일: 검색 시 활성 탭 데이터만 조회·맵 병합
let ok = false;
if (
activeTab === "ST00020001" ||
activeTab === "ST00020002" ||
activeTab === "ST00020003" ||
activeTab === "ST00020004"
) {
ok = await loadSewagePipeTotTabData(activeTab, values, true);
}
if (ok) {
lastSearchedValuesRef.current = values;
} else {
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
lastSearchedValuesRef.current = null;
}
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
lastSearchedValuesRef.current = null;
} finally {
setIsLoading(false);
}
};
const handleActiveTabChange = async (tabKey: string) => {
setActiveTab(tabKey);
const values = lastSearchedValuesRef.current;
if (!values) return;
const existing = rowDataMap.get(tabKey);
const hasData = Array.isArray(existing) && existing.length > 0;
if (hasData) return;
if (
tabKey === "ST00020001" ||
tabKey === "ST00020002" ||
tabKey === "ST00020003" ||
tabKey === "ST00020004"
) {
try {
setIsLoading(true);
await loadSewagePipeTotTabData(tabKey, values, true);
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
return;
}
console.log("[SewagePipeTot] tabKey (API 미연결):", tabKey);
};
return (
<div className="relative">
<SearchForm isDetail={true} onSubmit={handleSearch} onReset={handleReset} />
<SearchForm isDetail={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]">
@@ -81,7 +175,7 @@ const SewagePipeTot = () => {
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<Tabs
value={activeTab}
onValueChange={setActiveTab}
onValueChange={(v) => void handleActiveTabChange(v)}
className="w-full"
variant="default"
>
@@ -8,6 +8,7 @@ import { useAlertStore } from "@/shared/lib";
import SearchForm from "@/components/module/SearchForm";
import DataGrid from "@/components/module/DataGrid";
import CellColorInfo from "./CellColorInfo";
import { LoadingWithAnimation } from "@common/components";
const SeweragePenetrationRateTot = () => {
@@ -19,12 +20,17 @@ const SeweragePenetrationRateTot = () => {
try {
setIsLoading(true);
const isSummaryEnabled = values.isSummary !== 'ALL';
const summaryYearCnt = isSummaryEnabled ? Number(values.isSummary) : undefined;
const param = {
ctpvNm: values.sido === 'ALL' ? '' : values.sido,
sggNm: values.gugun === 'ALL' ? '' : values.gugun,
sewerBzmnCd: values.bsnmCd,
statsYr: values.year,
rowDataYn: values.isDetail
rowDataYn: values.isDetail,
summaryYn: isSummaryEnabled,
...(isSummaryEnabled ? { summaryYearCnt } : {}),
};
const response = await statisticsTotalStatusApi.getSewerageSupplyRateList(param);
@@ -35,7 +41,8 @@ const SeweragePenetrationRateTot = () => {
} else {
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
}
} catch {
} catch (error) {
console.log(error)
alert('데이터 조회 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
@@ -44,12 +51,13 @@ const SeweragePenetrationRateTot = () => {
return (
<>
<SearchForm isDetail={true} onSubmit={handleSearch} sidoSelectText="전체" />
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} 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]">
<LoadingWithAnimation color="primary" />
</div>
</div>}
<CellColorInfo />
<DataGrid
columnInfo={SeweragePentrationRateTotColumn}
rowStartNum={2}
@@ -61,6 +69,7 @@ const SeweragePenetrationRateTot = () => {
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';
}}
/>
@@ -1,14 +1,14 @@
import { useEffect, useMemo, useState } from "react";
import { useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib";
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
import { toGridRows } from "@/components/util/grid/gridFunction";
import SearchForm from "@/components/module/SearchForm";
import TabDataGrid from "@/components/module/TabDataGrid";
import { LoadingWithAnimation } from "@common/components";
import DataGrid from "@/components/module/DataGrid";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import type { ColumnConfig, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { ComplaintInpColumn } from "@/components/util/column/total/sewerageServiceLevel/ComplaintTotColumn";
import { ResidentFriendlyFactTotColumn } from "@/components/util/column/total/sewerageServiceLevel/ResidentFriendlyFactTotColumn";
@@ -17,81 +17,111 @@ const SewerageServiceLevelInpTabList = [
ComplaintInpColumn,
ResidentFriendlyFactTotColumn,
];
const SUPPORTED_TAB_KEYS = [ComplaintInpColumn.tabKey, ResidentFriendlyFactTotColumn.tabKey] as const;
const SewerageServiceLevelTot = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
const [isLoading, setIsLoading] = useState<boolean>(false);
const [activeTab, setActiveTab] = useState(ComplaintInpColumn.tabKey);
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
const lastSearchValuesRef = useRef<SearchFormValues | null>(null);
const columnInfo = useMemo(
() =>
SewerageServiceLevelInpTabList.map((tab: ColumnInfoList) => ({
...tab,
columns: (tab.columns ?? []).map((col: ColumnConfig) => ({
...col,
editable: false as const,
styleName: col.styleName ?? ("rg-cell-readonly" as const),
})),
})),
[],
);
const columnInfo = SewerageServiceLevelInpTabList;
const handleReset = () => {
setRowDataMap(new Map());
setActiveTab(ComplaintInpColumn.tabKey);
setLastSearchValues(null);
lastSearchValuesRef.current = null;
};
const fetchByTab = async (tabKey: string, values: SearchFormValues) => {
try {
setIsLoading(true);
const loadSewerageServiceLevelTotTabData = async (
tabKey: string,
values: SearchFormValues,
mergeIntoExistingMap: boolean,
) => {
const param = {
ctpvNm: values.sido === "ALL" ? "" : values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun,
sewerBzmnCd: values.bsnmCd,
statsYr: values.year,
rowDataYn: values.isDetail,
};
const param = {
ctpvNm: values.sido === "ALL" ? "" : values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun,
sewerBzmnCd: values.bsnmCd,
statsYr: values.year,
rowDataYn: values.isDetail,
};
const fetcherMap = {
[ComplaintInpColumn.tabKey]: statisticsTotalStatusApi.getSewerageServiceLevelComplaint.bind(
statisticsTotalStatusApi,
),
[ResidentFriendlyFactTotColumn.tabKey]:
statisticsTotalStatusApi.getSewerageServiceLevelResidentFriendlyFact.bind(
statisticsTotalStatusApi,
),
};
const fetcher = fetcherMap[tabKey];
if (!fetcher) {
return false;
}
const response = await fetcher(param);
let response;
if (tabKey === ResidentFriendlyFactTotColumn.tabKey) {
response =
await statisticsTotalStatusApi.getSewerageServiceLevelResidentFriendlyFact(param);
} else {
response =
await statisticsTotalStatusApi.getSewerageServiceLevelComplaint(param);
}
if (response.status === "success" && response.data) {
if (response.status === "success" && response.data) {
const rows = toGridRows(response.data as object[]);
if (mergeIntoExistingMap) {
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(tabKey, toGridRows(response.data as object[]));
next.set(tabKey, rows);
return next;
});
} else {
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
const next = new Map<string, Record<string, string | number>[]>();
next.set(tabKey, rows);
setRowDataMap(next);
}
return true;
}
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
return false;
};
const handleSearch = async (values: SearchFormValues) => {
try {
setIsLoading(true);
let ok = false;
if (SUPPORTED_TAB_KEYS.includes(activeTab)) {
ok = await loadSewerageServiceLevelTotTabData(activeTab, values, true);
}
if (ok) {
lastSearchValuesRef.current = values;
} else {
lastSearchValuesRef.current = null;
}
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
lastSearchValuesRef.current = null;
} finally {
setIsLoading(false);
}
};
const handleSearch = async (values: SearchFormValues) => {
setLastSearchValues(values);
setRowDataMap(new Map());
await fetchByTab(activeTab, values);
};
const handleActiveTabChange = async (tabKey: string) => {
setActiveTab(tabKey);
useEffect(() => {
if (lastSearchValues == null) return;
if (rowDataMap.has(activeTab)) return;
void fetchByTab(activeTab, lastSearchValues);
}, [activeTab]);
const values = lastSearchValuesRef.current;
if (!values) return;
const existing = rowDataMap.get(tabKey);
const hasData = Array.isArray(existing) && existing.length > 0;
if (hasData) return;
if (SUPPORTED_TAB_KEYS.includes(tabKey)) {
try {
setIsLoading(true);
await loadSewerageServiceLevelTotTabData(tabKey, values, true);
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
}
};
return (
<div className="relative">
@@ -103,20 +133,60 @@ const SewerageServiceLevelTot = () => {
</div>
</div>
)}
<TabDataGrid
columnInfo={columnInfo}
isExcelImport={false}
rowDataMap={rowDataMap}
setRowDataMap={setRowDataMap}
activeTab={activeTab}
onActiveTabChange={setActiveTab}
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";
}}
/>
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<Tabs value={activeTab} onValueChange={(v) => void handleActiveTabChange(v)} className="w-full" variant="default">
<TabsList>
{columnInfo.map((info) => (
<TabsTrigger key={info.tabKey} value={info.tabKey}>
{info.tabLabel || info.tabKey}
</TabsTrigger>
))}
</TabsList>
{columnInfo.map((info) => {
const tabKey = info.tabKey;
if (!info.columns || !info.columnLayout) {
return null;
}
const rowData = rowDataMap.get(tabKey) ?? [];
const gridColumnInfo: ColumnInfo = {
columns: info.columns,
columnLayout: info.columnLayout,
};
return (
<TabsContent key={tabKey} value={tabKey}>
<DataGrid
columnInfo={gridColumnInfo}
rowStartNum={info.rowStartNum}
excelExportFileSuffix={info.tabLabel || tabKey}
rowData={rowData}
setRowData={(updater) => {
setRowDataMap((prev) => {
const next = new Map(prev);
const cur = next.get(tabKey) ?? [];
const n = typeof updater === "function" ? updater(cur) : updater;
next.set(tabKey, n);
return next;
});
}}
isExcelImport={false}
isExcelExport
showHeaderCheckbox={false}
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";
}}
/>
</TabsContent>
);
})}
</Tabs>
</div>
</div>
);
};
@@ -129,6 +129,7 @@ const DEFAULT_FILTERS: StatisticsVerificationFilterState = {
statsYr: String(dayjs().year() - 1),
sewerBzmnCd: null,
statsCd: null,
laeSbmsnSttsCd: "2",
};
const DEFAULT_SUB_FILTERS: StatisticsVerificationSubFilterState = {
@@ -156,15 +157,6 @@ function StatisticsVerificationReport() {
useState<StatisticsVerificationFilterState>(DEFAULT_FILTERS);
const [subFilters, setSubFilters] =
useState<StatisticsVerificationSubFilterState>(DEFAULT_SUB_FILTERS);
const [submittedFilters, setSubmittedFilters] =
useState<StatisticsVerificationFilterState>(DEFAULT_FILTERS);
const [submittedParams, setSubmittedParams] = useState<{
main: StatisticsVerificationFilterState;
sub: StatisticsVerificationSubFilterState;
}>({
main: DEFAULT_FILTERS,
sub: DEFAULT_SUB_FILTERS,
});
const [selectedReportRows, setSelectedReportRows] = useState<
StatsInputStatusList[]
>([]);
@@ -182,11 +174,30 @@ function StatisticsVerificationReport() {
const { data: sigunguData } = useStAnlsSigunguCombo(
filters.sewerBzmnCd ?? "",
);
const detailParams = useMemo(
() => ({
statsYr: filters.statsYr,
ctpvNm:
subFilters.ctpvNm !== "" ? subFilters.ctpvNm : filters.ctpvNm,
sggNm: subFilters.sggNm !== "" ? subFilters.sggNm : filters.sggNm,
sewerBzmnCd:
subFilters.sewerBzmnCd !== ""
? subFilters.sewerBzmnCd
: filters.sewerBzmnCd,
statsCd:
subFilters.statsCd !== ""
? subFilters.statsCd
: filters.statsCd,
prcsYnList: subFilters.prcsYnList,
pbcpAprvYn: subFilters.pbcpAprvYn,
laeSbmsnSttsCd: subFilters.laeSbmsnSttsCd,
}),
[filters, subFilters],
);
const statisticsVerificationReportQuery =
useStatsVrfcRptData(submittedFilters);
const statisticsVerificationReportQuery = useStatsVrfcRptData(filters);
const statisticsVerificationResultQuery =
useStatsVrfcRsltData(submittedParams);
useStatsVrfcRsltData(detailParams);
const statisticsVerificationReportData =
statisticsVerificationReportQuery.data ?? [];
@@ -253,11 +264,17 @@ function StatisticsVerificationReport() {
const nextRows = statisticsVerificationResultGridRows;
const prevLength = prevLengthRef.current;
if (!dataProvider || nextRows.length === 0) {
if (!dataProvider) {
prevLengthRef.current = nextRows.length;
return;
}
if (nextRows.length === 0) {
dataProvider.setRows?.([]);
prevLengthRef.current = 0;
return;
}
if (nextRows.length > prevLength && prevLength > 0) {
// 무한스크롤 시 데이터 처리
const newRows = nextRows.slice(prevLength);
@@ -327,17 +344,16 @@ function StatisticsVerificationReport() {
statisticsVerificationReportQuery.isError,
]);
function handleSearch(
nextFilters: StatisticsVerificationFilterState,
nextSubFilters: StatisticsVerificationSubFilterState,
) {
setSelectedReportRows([]);
function resetResultState() {
setSelectedResultRows([]);
setSubmittedFilters({ ...nextFilters });
setSubmittedParams({
main: { ...nextFilters },
sub: { ...nextSubFilters },
});
editedResultRowMapRef.current = new Map();
prevLengthRef.current = 0;
resultGridRef.current?.dataProvider?.setRows?.([]);
}
function resetReportState() {
setSelectedReportRows([]);
resetResultState();
}
async function handleLoadMoreDetail() {
@@ -350,14 +366,6 @@ function StatisticsVerificationReport() {
}
}
function handleResultSearch() {
setSelectedResultRows([]);
setSubmittedParams({
main: { ...filters },
sub: { ...subFilters },
});
}
function handleFilterChange<
K extends keyof StatisticsVerificationFilterState,
>(key: K) {
@@ -365,6 +373,7 @@ function StatisticsVerificationReport() {
value?: StatisticsVerificationFilterState[K],
optionData?: { label: string; value: string },
) => {
resetReportState();
if (key === "sewerBzmnCd") {
setFilters((prev) => ({
...prev,
@@ -372,8 +381,17 @@ function StatisticsVerificationReport() {
sewerBzmnCd: optionData?.value,
sggNm: "",
}));
setSubFilters((prev) => ({
...prev,
ctpvNm: "",
sggNm: "",
sewerBzmnCd: "",
}));
return;
}
if (key === "statsCd") {
setSubFilters((prev) => ({ ...prev, statsCd: "" }));
}
setFilters((prev) => ({ ...prev, [key]: value }));
};
}
@@ -382,10 +400,16 @@ function StatisticsVerificationReport() {
K extends keyof StatisticsVerificationSubFilterState,
>(key: K) {
return (value?: StatisticsVerificationSubFilterState[K]) => {
resetResultState();
setSubFilters((prev) => ({ ...prev, [key]: value }));
};
}
function handleSubFilterReset() {
setSubFilters(DEFAULT_SUB_FILTERS);
resetResultState();
}
function handleCellEdited(params: CellEditedCallbackParams) {
const { itemIndex, fieldName, newValue } = params;
const dataProvider = resultGridRef.current?.dataProvider as
@@ -403,11 +427,7 @@ function StatisticsVerificationReport() {
function handleReset() {
setFilters(DEFAULT_FILTERS);
setSubFilters(DEFAULT_SUB_FILTERS);
setSelectedReportRows([]);
setSelectedResultRows([]);
editedResultRowMapRef.current = new Map();
prevLengthRef.current = 0;
handleSearch(DEFAULT_FILTERS, DEFAULT_SUB_FILTERS);
resetReportState();
}
function handleApprove() {
@@ -532,9 +552,6 @@ function StatisticsVerificationReport() {
<Button color="white" onClick={handleReset}>
초기화
</Button>
<Button onClick={() => handleSearch(filters, subFilters)}>
조회
</Button>
<Button
color="green"
onClick={handleApprove}
@@ -557,6 +574,15 @@ function StatisticsVerificationReport() {
setRowData={() => {}}
showHeaderCheckbox={true}
gridHeight={30}
onRowClick={(row: StatsInputStatusList) => {
setSubFilters((prev) => ({
...prev,
ctpvNm: row?.ctpvNm ?? "",
sggNm: row?.sggNm ?? "",
sewerBzmnCd: row?.sewerBzmnCd ?? "",
statsCd: row?.statsCd ?? "",
}));
}}
/>
<h3 className="mt-10 px-2 text-[1.8rem] font-bold text-title">
@@ -578,14 +604,15 @@ function StatisticsVerificationReport() {
/>
</div>
<div className="flex items-end justify-end gap-2">
<Button onClick={handleResultSearch}>조회</Button>
<Button color="white" onClick={handleSubFilterReset}>
초기화
</Button>
<Button
color="red"
onClick={handleReject}
disabled={
selectedResultRows.length === 0
// ||
// selectedResultRows.some((row) => row.prcsYn !== "2")
selectedResultRows.length === 0 ||
selectedResultRows.some((row) => row.prcsYn !== "2")
}
>
반려
@@ -65,21 +65,15 @@ const Indicators: React.FC<IndicatorsProps> = ({ anlsSn, vrfcTrgtSn }) => {
return (
<div className="card card-border !m-0 grid min-h-[10rem] grid-cols-4 gap-2">
<IndicatorItem label="r 중위수" value={dtl?.rtCnterVl} />
<IndicatorItem label="R 중위수" value={dtl?.rtCnterVl} />
<IndicatorItem label="E 중위수" value={dtl?.lastTrsfScrCnterVl} />
<IndicatorItem label="하위 기준값" value={dtl?.lastTrsfScrLlmtVl} />
<IndicatorItem
label="(E_m)E 중위수"
value={dtl?.lastTrsfScrCnterVl}
/>
<IndicatorItem
label="(E_l)E 하한값"
value={dtl?.lastTrsfScrLlmtVl}
/>
<IndicatorItem
label="(E_u)E 상한값"
label="상위 기준값"
value={dtl?.lastTrsfScrUplmtVl}
/>
<IndicatorItem label="E_D_L" value={dtl?.llmtVl} />
<IndicatorItem label="E_D_U" value={dtl?.uplmtVl} />
<IndicatorItem label="이상치 하한값" value={dtl?.llmtVl} />
<IndicatorItem label="이상치 상한값" value={dtl?.uplmtVl} />
<IndicatorItem label="이상치 하한선" value={dtl?.otlrLlmtLine} />
<IndicatorItem label="이상치 상한선" value={dtl?.otlrUplmtLine} />
</div>
@@ -46,11 +46,6 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
selectedRowId,
appliedFilter,
}) => {
const [selectedSido, setSelectedSido] = useState<string>("");
const [selectedGugun, setSelectedGugun] = useState<string>("");
const [selectedStatus, setSelectedStatus] = useState<string>("");
const [showAllOnEmptyFilter, setShowAllOnEmptyFilter] =
useState<boolean>(true);
const [isDownloading, setIsDownloading] = useState<boolean>(false);
const [gridRowData, setGridRowData] = useState<
@@ -58,7 +53,7 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
>([]);
const { data: sidoData } = useStAnlsSidoCombo();
const { data: gugunData } = useStAnlsSigunguCombo(selectedSido);
const { data: gugunData } = useStAnlsSigunguCombo(appliedFilter.sido ?? "");
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
@@ -69,14 +64,7 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
const rowData = useMemo(() => {
let data = (resultData?.content ?? []) as StAnlsRsltResponse[];
if (
!appliedFilter.sido &&
!appliedFilter.gugun &&
!appliedFilter.status
) {
data = showAllOnEmptyFilter ? data : [];
} else {
{
const sidoCd = appliedFilter.sido;
if (sidoCd) {
data = data.filter((item) =>
@@ -116,32 +104,18 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
}));
return mappedData;
}, [resultData, appliedFilter, showAllOnEmptyFilter]);
}, [resultData, appliedFilter]);
const handleSidoChange = (val: string) => {
setSelectedSido(val);
setSelectedGugun("");
};
const handleSearch = () => {
onFilterChange({
sido: selectedSido,
gugun: selectedGugun,
status: selectedStatus,
});
setShowAllOnEmptyFilter(false);
onFilterChange({ ...appliedFilter, sido: val, gugun: "" });
};
const handleReset = () => {
setSelectedSido("");
setSelectedGugun("");
setSelectedStatus("");
onFilterChange({
sido: "",
gugun: "",
status: "",
});
setShowAllOnEmptyFilter(true);
};
const handleDownload = async () => {
@@ -182,7 +156,7 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
<div className="flex justify-between">
<div className="flex items-center gap-2">
<Select
value={selectedSido || ""}
value={appliedFilter.sido || ""}
onValueChange={handleSidoChange}
>
<SelectTrigger className="w-[12rem] bg-white">
@@ -197,9 +171,11 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
</SelectContent>
</Select>
<Select
value={selectedGugun}
onValueChange={setSelectedGugun}
disabled={!selectedSido}
value={appliedFilter.gugun || ""}
onValueChange={(val) =>
onFilterChange({ ...appliedFilter, gugun: val })
}
disabled={!appliedFilter.sido}
>
<SelectTrigger className="w-[12rem] bg-white">
<SelectValue placeholder="시군구 선택" />
@@ -213,8 +189,10 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
</SelectContent>
</Select>
<Select
value={selectedStatus}
onValueChange={setSelectedStatus}
value={appliedFilter.status || ""}
onValueChange={(val) =>
onFilterChange({ ...appliedFilter, status: val })
}
>
<SelectTrigger className="w-[10rem] bg-white">
<SelectValue placeholder="상태 선택" />
@@ -231,7 +209,7 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
<Button color="white" onClick={handleReset}>
초기화
</Button>
<Button onClick={handleSearch}>조회</Button>
{/* <Button onClick={handleSearch}>조회</Button> */}
<Button
color="green"
onClick={handleDownload}
@@ -104,10 +104,12 @@ const ScatterChart: React.FC<ScatterChartProps> = ({
anlsSn,
vrfcTrgtSn,
);
console.log("resultData: ", resultData);
const { data: chartData, isLoading: isLoadingChart } = useStAnlsChartData(
anlsSn,
vrfcTrgtSn,
);
console.log("chartData: ", chartData);
const isLoading = isLoadingGrid || isLoadingChart;
@@ -158,10 +160,12 @@ const ScatterChart: React.FC<ScatterChartProps> = ({
item,
};
if (item.otlrYnCd === "0") {
normalPoints.push(pt);
} else {
if (item.otlrYnCd === "2") {
return;
} else if (item.otlrYnCd === "1") {
outlierPoints.push(pt);
} else {
normalPoints.push(pt);
}
});
@@ -3,7 +3,6 @@ import { useEffect, useRef, useState } from 'react';
import { NoData } from '@/shared/ui/no-data';
import {
Button,
Input,
Table,
TableBody,
TableCell,
+80 -74
View File
@@ -3,9 +3,15 @@ import React from "react";
import { ValueType } from "realgrid";
import type { RealGridReact } from "realgrid-react";
import { type SewerageSupplyRateStatusInfo } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
// RealGrid 타입 정의
/** DataGrid 상단 진행상황/제출일/담당자 표시에 사용 (보급률·하수관로 등 공통) */
export type DataGridStatusBannerInfo = {
laeSbmsnSttsCd: string | null;
laeSbmsnYmd: string | null;
laePicNm: string | null;
laePicTelNo: string | null;
};
/** setColumns에 전달하는 컬럼 정보 (footer 배열/단일 객체 포함) */
export type RealGridColumnInfo = {
name: string;
@@ -19,30 +25,30 @@ export type RealGridColumnInfo = {
mergeRule?: { criteria: "value" | string };
sortable?: boolean;
footer?:
| {
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: ColumnFooter,
value: number | string | undefined,
) => number | string;
}
| Array<{
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: ColumnFooter,
value: number | string | undefined,
) => number | string;
}>;
| {
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: ColumnFooter,
value: number | string | undefined,
) => number | string;
}
| Array<{
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: ColumnFooter,
value: number | string | undefined,
) => number | string;
}>;
};
/** setColumns 메서드가 있는 GridView */
@@ -111,21 +117,21 @@ export type DataFieldValueCallback = (
export type ComputedFieldConfig =
| { fields: string[]; type: "sum" | "avg" }
| {
numeratorFields: string[];
denominatorFields: string[];
type: "pblswrgPopltnSuprtRate";
}
numeratorFields: string[];
denominatorFields: string[];
type: "pblswrgPopltnSuprtRate";
}
| {
numeratorFields: string[];
denominatorFields: string[];
type: "adtrtPopltnSuprtRate";
}
numeratorFields: string[];
denominatorFields: string[];
type: "adtrtPopltnSuprtRate";
}
| {
numeratorFields: string[];
denominatorFields: string[];
thirdFields: string[];
type: "swrgInstlrtRate";
};
numeratorFields: string[];
denominatorFields: string[];
thirdFields: string[];
type: "swrgInstlrtRate";
};
export type ColumnConfig = {
name: string;
@@ -148,31 +154,31 @@ export type ColumnConfig = {
valueType?: ValueType;
dataFormat?: string;
footer?:
| {
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: ColumnFooter,
value: number | string | undefined,
) => number | string;
multiRow?: boolean; // 여러 행 footer 사용 여부
}
| Array<{
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: ColumnFooter,
value: number | string | undefined,
) => number | string;
}>; // RealGrid에서 footer를 배열로 설정할 수 있음
| {
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: ColumnFooter,
value: number | string | undefined,
) => number | string;
multiRow?: boolean; // 여러 행 footer 사용 여부
}
| Array<{
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: ColumnFooter,
value: number | string | undefined,
) => number | string;
}>; // RealGrid에서 footer를 배열로 설정할 수 있음
};
export type FooterUserSpan = {
@@ -184,16 +190,16 @@ export type FooterUserSpan = {
export type ColumnLayoutItem =
| string
| {
name: string;
hideChildHeaders?: boolean;
header: { text: string };
items: (string | ColumnLayoutItem)[];
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
}
name: string;
hideChildHeaders?: boolean;
header: { text: string };
items: (string | ColumnLayoutItem)[];
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
}
| {
column: string; // 컬럼 이름 (footerUserSpans 사용 시)
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
};
column: string; // 컬럼 이름 (footerUserSpans 사용 시)
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
};
export type ColumnInfo = {
columns: ColumnConfig[];
@@ -285,7 +291,7 @@ export type DataGridProps = {
/** CheckBar 헤더 전체 선택/해제 */
onItemAllChecked?: (checked: boolean) => void;
onScrollToBottom?: () => void;
statusInfo?: SewerageSupplyRateStatusInfo | null;
statusInfo?: DataGridStatusBannerInfo | null;
/** 강조 및 스크롤할 행의 ID */
highlightedRowId?: string | number | null;
/** ID로 사용할 필드명 (기본값: 'id') */
@@ -6,6 +6,7 @@ export type SearchFormValues = {
fctName?: string;
bsnmCd?: string;
isDetail?: boolean;
isSummary?: string;
};
export type SearchFormProps = {
@@ -17,6 +18,7 @@ export type SearchFormProps = {
fctName?: boolean;
userMenual?: boolean;
isDetail?: boolean;
isSummary?: boolean;
onSubmit?: (values: SearchFormValues) => void;
onReset?: () => void;
onValuesChange?: (values: SearchFormValues) => void;
@@ -33,4 +35,5 @@ export const defaultSearchFormProps: Required<
fctName: false,
userMenual: false,
isDetail: false,
isSummary: false,
};
@@ -20,4 +20,15 @@ export const needBsnmFrgnrPoplSmSidoList = [
"4700000000",
"4800000000",
"5000000000",
];
export const doNameList = [
"경기도",
"강원특별자치도",
"충청북도",
"충청남도",
"전북특별자치도",
"전라남도",
"경상북도",
"경상남도",
];
@@ -49,18 +49,18 @@ export const AdminAreaConnectionInpColumn = {
export const TreatPlantConnectionInpColumn = {
columns: [
// 하수처리장 정보
{ name: 'bscFctNm', fieldName: 'bscFctNm', header: { text: '하수처리장명' }, editable: false },
{ name: 'fctCpc', fieldName: 'fctCpc', header: { text: '용량' }, valueType: ValueType.NUMBER, dataFormat: '#,##0', editable: false },
{ name: 'bscFctCd', fieldName: 'bscFctCd', header: { text: '하수처리장코드' }, editable: false },
{ name: 'fcltNm', fieldName: 'fcltNm', header: { text: '하수처리장명' }, editable: false },
{ name: 'fcltCpct', fieldName: 'fcltCpct', header: { text: '용량' }, valueType: ValueType.NUMBER, dataFormat: '#,##0', editable: false },
{ name: 'fcltSn', fieldName: 'fcltSn', header: { text: '하수처리장코드' }, editable: false },
// 연결여부
{ name: 'isConnection', fieldName: 'isConnection', header: { text: '연결여부' }, editable: false },
// 연결지역
{ name: 'connectionAreaList', fieldName: 'connectionAreaList', header: { text: '연결지역' }, editable: false },
],
columnLayout: [
'bscFctNm',
'fctCpc',
'bscFctCd',
'fcltNm',
'fcltCpct',
'fcltSn',
'isConnection',
'connectionAreaList',
],
@@ -58,7 +58,7 @@ export const FactInstallStatusInpColumn = {
// 식별·키 필드 (API SeweragePipeInputFactInstallStatusItem, 그리드 비표시 — fixedColumns와 겹치지 않도록 맨 뒤)
{ name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, editable: false, styleName: 'rg-cell-readonly' },
{ name: 'prcsFcltSn', fieldName: 'prcsFcltSn', header: { text: '처리시설일련번호' }, visible: false, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'fcltSn', fieldName: 'fcltSn', header: { text: '처리시설일련번호' }, visible: false, editable: false, valueType: ValueType.TEXT },
{ name: 'swtrmtFcltCd', fieldName: 'swtrmtFcltCd', header: { text: '하수처리시설코드' }, visible: false, editable: false, styleName: 'rg-cell-readonly' },
],
columnLayout: [
@@ -146,7 +146,7 @@ export const FactInstallStatusInpColumn = {
},
'rmrk',
'admdstCd',
'prcsFcltSn',
'fcltSn',
'swtrmtFcltCd',
],
rowStartNum: 4,
@@ -1,5 +1,3 @@
import dayjs from "dayjs";
import { headers } from "next/headers";
import { ValueType } from "realgrid";
/**
@@ -7,9 +5,9 @@ import { ValueType } from "realgrid";
* 항목은 검토(SewagePipeRvwColumn)와 동일하나 footer·mergeCells·구분(displayYear)·비고(rmrk) 제외.
* 표시 형식은 SeweragePentrationRateTotColumn과 같이 dataFormat + editable: false.
*/
const currentYear = dayjs().year() - 1;
const startYear = 1996;
const totalCount = currentYear - startYear + 1 + 1;
/** ST00020003 관종별: 입력 `SeweragePipeInputPipeTypeStatusItem`·합계 `SewagePipePipeTypeFacilityStatusItem`과 동일 (yy1=1996년 이전 … yy30) */
const YY_PIPE_TYPE_FACILITY_MAX = 30;
const rawSewagePipeTotList = [
{
@@ -642,11 +640,11 @@ const rawSewagePipeTotList = [
dataFormat: "#,##0.0",
editable: false,
},
...Array.from({ length: totalCount }, (_, i) => {
const dtoNum = totalCount - i;
const wholField = `yy${dtoNum}PrlgWhol`;
const lyngField = `yy${dtoNum}LyngPrlg`;
const dmltnField = `yy${dtoNum}DmltnPrlg`;
...Array.from({ length: YY_PIPE_TYPE_FACILITY_MAX }, (_, i) => {
const yyIdx = i + 1;
const wholField = `yy${yyIdx}PrlgWhol`;
const lyngField = `yy${yyIdx}LyngPrlg`;
const dmltnField = `yy${yyIdx}DmltnPrlg`;
return [
{
name: wholField,
@@ -687,13 +685,18 @@ const rawSewagePipeTotList = [
header: { text: "총계" },
items: ["pipeByPrstWholSum", "lyngPrlgSum", "dmltnPrlgSum"],
},
...Array.from({ length: totalCount }, (_, i) => {
const dtoNum = totalCount - i;
const yearLabel = i === 0 ? "1996년이전" : `${startYear + (i - 1)}년`;
{
name: "year1996Before",
header: { text: "1996년이전" },
items: ["yy1PrlgWhol", "yy1LyngPrlg", "yy1DmltnPrlg"],
},
...Array.from({ length: YY_PIPE_TYPE_FACILITY_MAX - 1 }, (_, i) => {
const yyIdx = i + 2;
const year = 1994 + yyIdx;
return {
name: `group_${yearLabel}`,
header: { text: yearLabel },
items: [`yy${dtoNum}PrlgWhol`, `yy${dtoNum}LyngPrlg`, `yy${dtoNum}DmltnPrlg`],
name: `group_yy${yyIdx}`,
header: { text: `${year}년` },
items: [`yy${yyIdx}PrlgWhol`, `yy${yyIdx}LyngPrlg`, `yy${yyIdx}DmltnPrlg`],
};
}),
],
@@ -0,0 +1,79 @@
import { ValueType } from "realgrid";
// 하수도 통계 관리 > 03.하수관로 유지관리 > tab 2. 관로준설
export const PipeDredgingStatusTotColumn = {
columns: [
// 행정구역
{ name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' } },
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구명' } },
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' } },
// 하수관로 총 시설연장
{ name: 'wholFcltPrlg', fieldName: 'wholFcltPrlg', header: { text: '하수관로총시설연장(m)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
// 준설연장 및 개소수 > 관로(m)
{ name: 'wholSwagePpl', fieldName: 'wholSwagePpl', header: { text: '합계' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'cbsysPplPrlg', fieldName: 'cbsysPplPrlg', header: { text: '합류식' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
// 준설연장 및 개소수 > 관로(m) > 분류식
{ name: 'sprsysExlnPrlg', fieldName: 'sprsysExlnPrlg', header: { text: '우수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'sprsysDrtwPrlg', fieldName: 'sprsysDrtwPrlg', header: { text: '오수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
// 준설연장 및 개소수 > 맨홀, 오·우수받이, 토실·토구
{ name: 'mnhlCnt', fieldName: 'mnhlCnt', header: { text: '맨홀(개소)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'exlnDrtwRcvprpCnt', fieldName: 'exlnDrtwRcvprpCnt', header: { text: '오·우수받이(개소)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'stofcbrOtflCnt', fieldName: 'stofcbrOtflCnt', header: { text: '토실·토구(개소)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
// 계획 & 비율
{ name: 'drdgnPlanPrlg', fieldName: 'drdgnPlanPrlg', header: { text: '준설 계획 연장(m)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'drdgnTrgtCtrsDrdgnRt', fieldName: 'drdgnTrgtCtrsDrdgnRt', header: { text: '계획대비 준설율(%)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'wholPrlgCtrsDrdgnRt', fieldName: 'wholPrlgCtrsDrdgnRt', header: { text: '총시설연장대비 준설율(%)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
// 준설실적
{ name: 'drdgnAmt', fieldName: 'drdgnAmt', header: { text: '금액(백만원)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'drdgnQnt', fieldName: 'drdgnQnt', header: { text: '준설량(m³)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
// 준설토사처리
{ name: 'drdgnUntprcAmt', fieldName: 'drdgnUntprcAmt', header: { text: '준설토처리비(백만원)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'wholDrdgnPrcsQnt', fieldName: 'wholDrdgnPrcsQnt', header: { text: '처리량 계(톤)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'rclmQnt', fieldName: 'rclmQnt', header: { text: '매립' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'oceanEmsnQnt', fieldName: 'oceanEmsnQnt', header: { text: '해양투기' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'rcycQnt', fieldName: 'rcycQnt', header: { text: '재활용' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'etcQnt', fieldName: 'etcQnt', header: { text: '기타(소각, 고형화, 시멘트원료 등)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
].map((col) => ({ ...col, editable: false })),
columnLayout: [
{ name: 'group_admin', header: { text: '행정구역' }, items: ['ctpvNm', 'sggNm', 'admdstNm'] },
'wholFcltPrlg',
{
name: 'group_dredge_info',
header: { text: '준설연장 및 개소수' },
items: [
{
name: 'group_pipe',
header: { text: '관로(m)' },
items: [
'wholSwagePpl', 'cbsysPplPrlg',
{ name: 'group_sep', header: { text: '분류식' }, items: ['sprsysExlnPrlg', 'sprsysDrtwPrlg'] }
]
},
'mnhlCnt', 'exlnDrtwRcvprpCnt', 'stofcbrOtflCnt'
]
},
'drdgnPlanPrlg', 'drdgnTrgtCtrsDrdgnRt', 'wholPrlgCtrsDrdgnRt',
{
name: 'group_perf',
header: { text: '준설실적' },
items: ['drdgnAmt', 'drdgnQnt']
},
{
name: 'group_soil_treat',
header: { text: '준설토사처리' },
items: ['drdgnUntprcAmt', 'wholDrdgnPrcsQnt', 'rclmQnt', 'oceanEmsnQnt', 'rcycQnt', 'etcQnt']
},
],
rowStartNum: 4,
fixedColumns: 1,
tabKey: 'ST00030002',
tabLabel: '관로준설'
};
@@ -0,0 +1,112 @@
import { ValueType } from "realgrid";
// 하수도 통계 관리 > 03.하수관로 유지관리 > tab 1. 개보수
export const RepairStatusTotColumn = {
columns: [
// 행정구역
{ name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, editable: false },
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구명' }, editable: false },
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, editable: false },
// 개·보수관로 > 전체보수(m)
{ name: 'mendngWhol', fieldName: 'mendngWhol', header: { text: '계' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'cbsysMendngPrlg', fieldName: 'cbsysMendngPrlg', header: { text: '합류식' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
// 개·보수관로 > 전체보수(m) > 분류식
{ name: 'sprsysExlnPrlg', fieldName: 'sprsysExlnPrlg', header: { text: '우수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'sprsysDrtwPrlg', fieldName: 'sprsysDrtwPrlg', header: { text: '오수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
// 개·보수관로 > 부분보수(개소)
{ name: 'partMendngWhol', fieldName: 'partMendngWhol', header: { text: '계' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'cbsysPartMendngPrlg', fieldName: 'cbsysPartMendngPrlg', header: { text: '합류식' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
// 개·보수관로 > 부분보수(개소) > 분류식
{ name: 'sprsysExlnMendngPrlg', fieldName: 'sprsysExlnMendngPrlg', header: { text: '우수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'sprsysDrtwMendngPrlg', fieldName: 'sprsysDrtwMendngPrlg', header: { text: '오수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
// 맨홀(개소)
{ name: 'cbsysMnhlCnt', fieldName: 'cbsysMnhlCnt', header: { text: '합류식맨홀' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
// 맨홀(개소) > 분류식
{ name: 'sprsysDrtwMnhlCnt', fieldName: 'sprsysDrtwMnhlCnt', header: { text: '오수맨홀' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'sprsysExlnMnhlCnt', fieldName: 'sprsysExlnMnhlCnt', header: { text: '우수맨홀' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
// 받이
{ name: 'drtwRcvprpCnt', fieldName: 'drtwRcvprpCnt', header: { text: '오수받이(개소)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'exlnRcvprpCnt', fieldName: 'exlnRcvprpCnt', header: { text: '빗물받이(개소)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
// 토실, 토구
{ name: 'stofcbrCnt', fieldName: 'stofcbrCnt', header: { text: '토실(개소)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'otflCnt', fieldName: 'otflCnt', header: { text: '토구(개소)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
// 수밀검사CCTV조사량(m)
{ name: 'cctvExmnPrlg', fieldName: 'cctvExmnPrlg', header: { text: '수밀검사·CCTV조사량(m)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
].map((col) => ({ ...col, editable: false })),
columnLayout: [
{
name: 'group_admin',
header: { text: '행정구역' },
items: ['ctpvNm', 'sggNm', 'admdstNm']
},
{
name: 'group_repair_main',
header: { text: '개·보수관로' },
items: [
{
name: 'group_full_repair',
header: { text: '전체보수(m)' },
items: [
'mendngWhol',
'cbsysMendngPrlg',
{
name: 'group_full_sep',
header: { text: '분류식' },
items: ['sprsysExlnPrlg', 'sprsysDrtwPrlg']
}
]
},
{
name: 'group_part_repair',
header: { text: '부분보수(개소)' },
items: [
'partMendngWhol',
'cbsysPartMendngPrlg',
{
name: 'group_part_sep',
header: { text: '분류식' },
items: ['sprsysExlnMendngPrlg', 'sprsysDrtwMendngPrlg']
}
]
}
]
},
{
name: 'group_manhole',
header: { text: '맨홀(개소)' },
items: [
'cbsysMnhlCnt',
{
name: 'group_mh_sep',
header: { text: '분류식' },
items: ['sprsysDrtwMnhlCnt', 'sprsysExlnMnhlCnt']
}
]
},
{
name: 'group_inlet',
header: { text: '받이' },
items: ['drtwRcvprpCnt', 'exlnRcvprpCnt']
},
{
name: 'group_chamber',
header: { text: '토실,토구' },
items: ['stofcbrCnt', 'otflCnt']
},
'cctvExmnPrlg',
],
rowStartNum: 4,
fixedColumns: 1,
tabKey: 'ST00030001',
tabLabel: '개보수'
};
@@ -0,0 +1,12 @@
import { PipeDredgingStatusTotColumn } from "./PipeDredgingStatusTotColumn"
import { RepairStatusTotColumn } from "./RepairStatusTotColumn"
const sewagePipeMaintainColumn = [
RepairStatusTotColumn,
PipeDredgingStatusTotColumn
];
export const SewagePipeMaintainColumnListTot = sewagePipeMaintainColumn.map((tab) => ({
...tab,
columns: tab.columns.map((col) => ({ ...col, editable: false as const })),
}));
@@ -0,0 +1,31 @@
import { useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import type { RecentPostSearchParams } from "../model/post-interface";
import { postApi } from "./post-service";
export function useRecentPostList(params?: RecentPostSearchParams) {
return useQuery({
queryKey: createRecentPostListQueryKey(params),
queryFn: () => postApi.getRecentPostList(params),
refetchOnWindowFocus: false,
});
}
function createRecentPostListQueryKey(params?: RecentPostSearchParams) {
return [
ENDPOINT.BOARD_SERVICE.POSTS,
"recentPosts",
params?.page ?? "",
params?.size ?? "",
params?.sort ?? "",
params?.keywordType ?? "",
params?.keyword ?? "",
params?.bbsId ?? "",
params?.delYn ?? "",
params?.frDateYmd ?? "",
params?.toDateYmd ?? "",
];
}
@@ -0,0 +1,22 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type {
PostService,
RecentPostListResponse,
RecentPostSearchParams,
} from "../model/post-interface";
async function getRecentPostList(
params?: RecentPostSearchParams,
): Promise<RecentPostListResponse> {
const response = await client.get<RecentPostListResponse>(
`${ENDPOINT.BOARD_SERVICE.POSTS}/recentPosts`,
{ params },
);
return response.data;
}
export const postApi: PostService = {
getRecentPostList,
};
+8
View File
@@ -0,0 +1,8 @@
export { useRecentPostList } from "./api/post-queries";
export { postApi } from "./api/post-service";
export {
type PostService,
type PstRecentListResponse,
type RecentPostListResponse,
type RecentPostSearchParams,
} from "./model/post-interface";
@@ -0,0 +1,42 @@
import type { PageableList, PageableParams } from "@/shared/model";
export interface RecentPostSearchParams extends PageableParams {
keywordType?: string;
keyword?: string;
bbsId?: number;
delYn?: string;
frDateYmd?: string;
toDateYmd?: string;
}
export interface PstRecentListResponse {
rowNo: number;
bbsId: number;
pstNo: number;
pstTtl: string;
pstCn: string;
inqNocs: number;
rfrncUrl: string;
popupYn: string;
upndFixYn: string;
prdPstgYn: string;
rootPath: string;
pstPath: string;
ansPstn: number;
wrtrNm: string;
wrtrCplc: string;
atflCd: string;
regDt: string;
rgtrNm: string;
cmntCnt: number;
bbsTypeCd: string;
bbsNm: string;
}
export type RecentPostListResponse = PageableList<PstRecentListResponse>;
export interface PostService {
getRecentPostList(
params?: RecentPostSearchParams,
): Promise<RecentPostListResponse>;
}
@@ -0,0 +1,29 @@
import { useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import type { ScheduleSearchParams } from "../model/schedule-interface";
import { scheduleApi } from "./schedule-service";
export function useSchedules(params?: ScheduleSearchParams) {
return useQuery({
queryKey: createSchedulesQueryKey(params),
queryFn: () => scheduleApi.getSchedules(params),
refetchOnWindowFocus: false,
});
}
function createSchedulesQueryKey(params?: ScheduleSearchParams) {
return [
ENDPOINT.CMS_SERVICE.SCHEDULES,
"list",
params?.siteId ?? "",
params?.schdlTtl ?? "",
params?.schdlSeCd ?? "",
params?.schdlBgngYmd ?? "",
params?.schdlEndYmd ?? "",
params?.reptYn ?? "",
params?.commSchdlYn ?? "",
];
}
@@ -0,0 +1,22 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type {
Schedule,
ScheduleSearchParams,
ScheduleService,
} from "../model/schedule-interface";
async function getSchedules(
params?: ScheduleSearchParams,
): Promise<Schedule[]> {
const response = await client.get<Schedule[]>(
ENDPOINT.CMS_SERVICE.SCHEDULES,
{ params },
);
return response.data;
}
export const scheduleApi: ScheduleService = {
getSchedules,
};
+7
View File
@@ -0,0 +1,7 @@
export { useSchedules } from "./api/schedule-queries";
export { scheduleApi } from "./api/schedule-service";
export {
type Schedule,
type ScheduleSearchParams,
type ScheduleService,
} from "./model/schedule-interface";
@@ -0,0 +1,27 @@
export interface ScheduleSearchParams {
siteId?: string;
schdlTtl?: string;
schdlSeCd?: string;
schdlBgngYmd?: string;
schdlEndYmd?: string;
reptYn?: string;
commSchdlYn?: string;
}
export interface Schedule {
schdlBgngDt: string;
schdlEndDt: string;
actualDate: string;
schdlSn: number;
siteId: string;
schdlTtl: string;
schdlSeCd: string;
schdlCn: string;
reptYn: string;
commSchdlYn: string;
atflCd: string;
}
export interface ScheduleService {
getSchedules(params?: ScheduleSearchParams): Promise<Schedule[]>;
}
@@ -0,0 +1,106 @@
import { useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import type { DashboardSearchParams } from "../../model/dashboard/dashboard-interface";
import { dashboardApi } from "./dashboard-service";
// 이상치 선별보고서 조회
export function useDashboardVrfcReport(params: DashboardSearchParams | null) {
return useQuery({
queryKey: createDashboardQueryKey("vrfc-report", params),
queryFn: async () => {
if (!params)
throw new Error("Dashboard search params are required.");
const response = await dashboardApi.getVrfcReport(params);
return response.data;
},
enabled: hasRequiredDashboardParams(params),
refetchOnWindowFocus: false,
});
}
// 검증오류 수 조회
export function useDashboardVrfcErrCount(params: DashboardSearchParams | null) {
return useQuery({
queryKey: createDashboardQueryKey("vrfc-err-count", params),
queryFn: async () => {
if (!params)
throw new Error("Dashboard search params are required.");
const response = await dashboardApi.getVrfcErrCount(params);
return response.data;
},
enabled: hasRequiredDashboardParams(params),
refetchOnWindowFocus: false,
});
}
// 미제출 수 & 목록 조회
export function useDashboardUnSubmitCount(
params: DashboardSearchParams | null,
) {
return useQuery({
queryKey: createDashboardQueryKey("un-submit-count", params),
queryFn: async () => {
if (!params)
throw new Error("Dashboard search params are required.");
const response = await dashboardApi.getUnSubmitCount(params);
return response.data;
},
enabled: hasRequiredDashboardParams(params),
refetchOnWindowFocus: false,
});
}
// 통계입력현황 조회
export function useDashboardStatsSubmissionRate(
params: DashboardSearchParams | null,
) {
return useQuery({
queryKey: createDashboardQueryKey("stats-submission-rate", params),
queryFn: async () => {
if (!params)
throw new Error("Dashboard search params are required.");
const response = await dashboardApi.getStatsSubmissionRate(params);
return response.data;
},
enabled: hasRequiredDashboardParams(params),
refetchOnWindowFocus: false,
});
}
// 예외신청 수 조회
export function useDashboardEcptReqCount(params: DashboardSearchParams | null) {
return useQuery({
queryKey: createDashboardQueryKey("ecpt-req-count", params),
queryFn: async () => {
if (!params)
throw new Error("Dashboard search params are required.");
const response = await dashboardApi.getEcptReqCount(params);
return response.data;
},
enabled: hasRequiredDashboardParams(params),
refetchOnWindowFocus: false,
});
}
function createDashboardQueryKey(
name: string,
params: DashboardSearchParams | null,
) {
return [
ENDPOINT.ST_SERVICE.DASHBOARD,
name,
params?.statsYr ?? "",
params?.dtlInstCd ?? "",
params?.enoCd ?? "",
params?.laeSeCd ?? "",
];
}
function hasRequiredDashboardParams(
params: DashboardSearchParams | null,
): params is DashboardSearchParams {
return Boolean(params?.statsYr && params.dtlInstCd);
}
@@ -0,0 +1,66 @@
import { stClient } from "@/shared/api/client";
import { ENDPOINT } from "@/shared/config";
import type {
DashboardApiResponse,
DashboardSearchParams,
DashboardService,
EcptReqCountResponse,
StatsSubmissionResponse,
UnSubmitCountResponse,
VrfcErrCountResponse,
VrfcReportResponse,
} from "../../model/dashboard/dashboard-interface";
async function getVrfcReport(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<VrfcReportResponse>> {
const response = await stClient.get<
DashboardApiResponse<VrfcReportResponse>
>(`${ENDPOINT.ST_SERVICE.DASHBOARD}/vrfc-report`, { params });
return response.data;
}
async function getVrfcErrCount(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<VrfcErrCountResponse>> {
const response = await stClient.get<
DashboardApiResponse<VrfcErrCountResponse>
>(`${ENDPOINT.ST_SERVICE.DASHBOARD}/vrfc-err-count`, { params });
return response.data;
}
async function getUnSubmitCount(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<UnSubmitCountResponse>> {
const response = await stClient.get<
DashboardApiResponse<UnSubmitCountResponse>
>(`${ENDPOINT.ST_SERVICE.DASHBOARD}/un-submit-count`, { params });
return response.data;
}
async function getStatsSubmissionRate(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<StatsSubmissionResponse>> {
const response = await stClient.get<
DashboardApiResponse<StatsSubmissionResponse>
>(`${ENDPOINT.ST_SERVICE.DASHBOARD}/stats-submission-rate`, { params });
return response.data;
}
async function getEcptReqCount(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<EcptReqCountResponse>> {
const response = await stClient.get<
DashboardApiResponse<EcptReqCountResponse>
>(`${ENDPOINT.ST_SERVICE.DASHBOARD}/ecpt-req-count`, { params });
return response.data;
}
export const dashboardApi: DashboardService = {
getVrfcReport,
getVrfcErrCount,
getUnSubmitCount,
getStatsSubmissionRate,
getEcptReqCount,
};
@@ -0,0 +1,35 @@
import { useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import type { ScheduleInfoSearchParams } from "../../model/dashboard/schedule-info-interface";
import { scheduleInfoApi } from "./schedule-info-service";
export function useScheduleInfo(params: ScheduleInfoSearchParams | null) {
return useQuery({
queryKey: createScheduleInfoQueryKey(params),
queryFn: async () => {
if (!params)
throw new Error("Schedule info search params are required.");
const response = await scheduleInfoApi.getScheduleInfo(params);
return response.data;
},
enabled: hasRequiredScheduleInfoParams(params),
refetchOnWindowFocus: false,
});
}
function createScheduleInfoQueryKey(params: ScheduleInfoSearchParams | null) {
return [
ENDPOINT.ST_SERVICE.DASHBOARD,
"schedule-info",
params?.statsYr ?? "",
];
}
function hasRequiredScheduleInfoParams(
params: ScheduleInfoSearchParams | null,
): params is ScheduleInfoSearchParams {
return Boolean(params?.statsYr);
}
@@ -0,0 +1,22 @@
import { stClient } from "@/shared/api/client";
import { ENDPOINT } from "@/shared/config";
import type {
ScheduleInfoApiResponse,
ScheduleInfoResponse,
ScheduleInfoSearchParams,
ScheduleInfoService,
} from "../../model/dashboard/schedule-info-interface";
async function getScheduleInfo(
params: ScheduleInfoSearchParams,
): Promise<ScheduleInfoApiResponse<ScheduleInfoResponse>> {
const response = await stClient.get<
ScheduleInfoApiResponse<ScheduleInfoResponse>
>(`${ENDPOINT.ST_SERVICE.DASHBOARD}/schedule-info`, { params });
return response.data;
}
export const scheduleInfoApi: ScheduleInfoService = {
getScheduleInfo,
};
@@ -3,6 +3,8 @@ import { stClient } from "@/shared/api/client";
import type {
SeweragePipeFacilityAdminAreaListRequest,
SeweragePipeFacilityAdminAreaListResponse,
SeweragePipeFactConnectionSaveRequest,
SeweragePipeFactConnectionSaveResponse,
SeweragePipeFacilitiesListRequest,
SeweragePipeFacilitiesListResponse,
SeweragePipeInputConnectionStatusResponse,
@@ -12,9 +14,13 @@ import type {
SeweragePipeInputPipeTypeResponse,
SeweragePipeInputRegionalFactStatusSaveRequest,
SeweragePipeInputRegionalFactStatusSaveResponse,
SeweragePipeInputRegionalFactStatusSubmitRequest,
SeweragePipeInputRegionalFactStatusSubmitResponse,
SeweragePipeInputRegionalResponse,
SeweragePipeRequestParams,
StatisticsInputSewPipeService,
SeweragePipeInputFactInstallStatusSaveRequest,
SeweragePipeInputFactInstallStatusSaveResponse,
} from "../../model/input/statistics-sewage-pipe-input-interface";
export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeService {
@@ -82,6 +88,14 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS
return response.data;
}
public async saveFactConnection(param: SeweragePipeFactConnectionSaveRequest): Promise<SeweragePipeFactConnectionSaveResponse> {
const response = await stClient.post<SeweragePipeFactConnectionSaveResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/facility-connection-status/popup/save`,
param,
);
return response.data;
}
public async saveRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSaveRequest): Promise<SeweragePipeInputRegionalFactStatusSaveResponse> {
const response = await stClient.post<SeweragePipeInputRegionalFactStatusSaveResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/regional-facility-status/save`,
@@ -89,6 +103,22 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS
);
return response.data;
}
public async submitRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSubmitRequest): Promise<SeweragePipeInputRegionalFactStatusSubmitResponse> {
const response = await stClient.post<SeweragePipeInputRegionalFactStatusSubmitResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/regional-facility-status/submit`,
param,
);
return response.data;
}
public async saveFactInstallStatus(param: SeweragePipeInputFactInstallStatusSaveRequest): Promise<SeweragePipeInputFactInstallStatusSaveResponse> {
const response = await stClient.post<SeweragePipeInputFactInstallStatusSaveResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/facility-install-status/save`,
param,
);
return response.data;
}
};
export const statisticsInputSewPipeApiService = new StatisticsInputSewPipeApiService();
+19 -22
View File
@@ -14,11 +14,6 @@ import { stAnlsRsltApi } from "./statistics-analysis-result-api";
const DETAIL_PAGE_SIZE = 100;
interface StatisticsVerificationDetailSubmittedParams {
main: StatisticsVerificationFilterState;
sub: StatisticsVerificationSubFilterState;
}
export const useStAnlsSidoCombo = () => {
return useQuery({
queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST, "sido"],
@@ -102,23 +97,24 @@ export const useStatsVrfcRptData = (
};
export const useStatsVrfcRsltData = (
submittedDetailParams: StatisticsVerificationDetailSubmittedParams | null,
params: StatisticsVerificationSubFilterState | null,
) => {
return useInfiniteQuery({
queryKey: [
ENDPOINT.ST_SERVICE.INPUTVERIFY,
"verify-results",
submittedDetailParams?.main.statsYr ?? "",
submittedDetailParams?.main.ctpvNm ?? "",
submittedDetailParams?.main.sggNm ?? "",
submittedDetailParams?.main.sewerBzmnCd ?? "",
submittedDetailParams?.main.statsCd ?? "",
submittedDetailParams?.sub.prcsYnList ?? "",
submittedDetailParams?.sub.pbcpAprvYn ?? "",
params?.statsYr ?? "",
params?.ctpvNm ?? "",
params?.sggNm ?? "",
params?.sewerBzmnCd ?? "",
params?.statsCd ?? "",
params?.prcsYnList ?? "",
params?.pbcpAprvYn ?? "",
params?.laeSbmsnSttsCd ?? "",
],
initialPageParam: 1,
queryFn: async ({ pageParam }) => {
if (!submittedDetailParams)
if (!params)
return {
verifyResultList: [],
pageInfo: {
@@ -134,13 +130,14 @@ export const useStatsVrfcRsltData = (
page: pageParam,
size: DETAIL_PAGE_SIZE,
searchDto: {
statsYr: submittedDetailParams.main.statsYr,
ctpvNm: submittedDetailParams.main.ctpvNm,
sggNm: submittedDetailParams.main.sggNm,
sewerBzmnCd: submittedDetailParams.main.sewerBzmnCd,
statsCd: submittedDetailParams.main.statsCd,
prcsYnList: submittedDetailParams.sub.prcsYnList,
pbcpAprvYn: submittedDetailParams.sub.pbcpAprvYn,
statsYr: params.statsYr,
ctpvNm: params.ctpvNm,
sggNm: params.sggNm,
sewerBzmnCd: params.sewerBzmnCd,
statsCd: params.statsCd,
prcsYnList: params.prcsYnList,
pbcpAprvYn: params.pbcpAprvYn,
laeSbmsnSttsCd: params.laeSbmsnSttsCd,
},
});
@@ -153,7 +150,7 @@ export const useStatsVrfcRsltData = (
if (currentPage + 1 <= totalPages) return currentPage + 1;
return undefined;
},
enabled: Boolean(submittedDetailParams),
enabled: Boolean(params),
refetchOnWindowFocus: false,
});
};
@@ -2,7 +2,18 @@ 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";
import type { SewagePipeTotalRequest, SewagePipeTotalResponse } from "../model/statistics-total/statistics-sewage-pipe-total-interface";
import type {
SewagePipeMaintainDredgingTotalResponse,
SewagePipeMaintainTotalRequest,
SewagePipeMaintainRepairTotalResponse,
} from "../model/statistics-total/statistics-sewage-pipe-maintain-total-interface";
import type {
SewagePipeFactInstallStatusResponse,
SewagePipePipeDiamStatusResponse,
SewagePipePipeTypeFacilityStatusResponse,
SewagePipeTotalRequest,
SewagePipeTotalResponse,
} from "../model/statistics-total/statistics-sewage-pipe-total-interface";
import type {
SewerageServiceLevelComplaintResponse,
SewerageServiceLevelRequest,
@@ -41,6 +52,60 @@ export class StatisticsTotalApiService {
);
return response.data;
}
public async getSewagePipeFactInstallStatus(
params: SewagePipeTotalRequest,
): Promise<SewagePipeFactInstallStatusResponse> {
const response = await stClient.get<SewagePipeFactInstallStatusResponse>(
`${ENDPOINT.ST_SERVICE.TOTAL}/sewer-pipe/facility-installation-status`,
{ params },
);
return response.data;
}
/** ST00020003 — 관종·관경별 시설현황(합계) */
public async getSewagePipePipeTypeFacilityStatus(
params: SewagePipeTotalRequest,
): Promise<SewagePipePipeTypeFacilityStatusResponse> {
const response = await stClient.get<SewagePipePipeTypeFacilityStatusResponse>(
`${ENDPOINT.ST_SERVICE.TOTAL}/sewer-pipe/pipe-type-facility-status`,
{ params },
);
return response.data;
}
/** ST00020004 — 관경별 시설현황(합계) */
public async getSewagePipePipeDiamStatus(
params: SewagePipeTotalRequest,
): Promise<SewagePipePipeDiamStatusResponse> {
const response = await stClient.get<SewagePipePipeDiamStatusResponse>(
`${ENDPOINT.ST_SERVICE.TOTAL}/sewer-pipe/pipe-diam-status`,
{ params },
);
return response.data;
}
/** ST00030001 — 하수관로 유지관리 > 개보수(합계) */
public async getSewagePipeMaintainRepairTotal(
params: SewagePipeMaintainTotalRequest,
): Promise<SewagePipeMaintainRepairTotalResponse> {
const response = await stClient.get<SewagePipeMaintainRepairTotalResponse>(
`${ENDPOINT.ST_SERVICE.TOTAL}/sewer-pipe-mtc/repair`,
{ params },
);
return response.data;
}
/** ST00030002 — 하수관로 유지관리 > 관로준설(합계) */
public async getSewagePipeMaintainDredgingTotal(
params: SewagePipeMaintainTotalRequest,
): Promise<SewagePipeMaintainDredgingTotalResponse> {
const response = await stClient.get<SewagePipeMaintainDredgingTotalResponse>(
`${ENDPOINT.ST_SERVICE.TOTAL}/sewer-pipe-mtc/dredging`,
{ params },
);
return response.data;
}
}
export const statisticsTotalStatusApi = new StatisticsTotalApiService();
+28
View File
@@ -1,3 +1,13 @@
export {
useDashboardEcptReqCount,
useDashboardStatsSubmissionRate,
useDashboardUnSubmitCount,
useDashboardVrfcErrCount,
useDashboardVrfcReport,
} from "./api/dashboard/dashboard-queries";
export { dashboardApi } from "./api/dashboard/dashboard-service";
export { useScheduleInfo } from "./api/dashboard/schedule-info-queries";
export { scheduleInfoApi } from "./api/dashboard/schedule-info-service";
export {
useStAnlsSidoCombo,
useStAnlsSigunguCombo,
@@ -14,6 +24,24 @@ export {
type JobStatusDto,
type OutlierRecordDto,
} from "./model/analysis/analysis-interface";
export {
type DashboardApiResponse,
type DashboardSearchParams,
type DashboardService,
type EcptReqCountResponse,
type StatsSubmissionResponse,
type UnSubmitCountResponse,
type UnSubmitStatsResponse,
type VrfcErrCountResponse,
type VrfcReportResponse,
type VrfcReportSelect,
} from "./model/dashboard/dashboard-interface";
export {
type ScheduleInfoApiResponse,
type ScheduleInfoResponse,
type ScheduleInfoSearchParams,
type ScheduleInfoService,
} from "./model/dashboard/schedule-info-interface";
export { type VerifyResultItem } from "./model/general-verification-interface";
export {
IMR_DETAIL_VISIBLE_COLUMNS_BY_STATS_CD,
@@ -0,0 +1,74 @@
export interface DashboardSearchParams {
statsYr: string;
dtlInstCd: string;
enoCd?: string;
laeSeCd?: string;
}
export interface DashboardApiResponse<T> {
status: string;
message: string;
data: T;
code: string;
httpStatus: number;
}
export interface VrfcReportResponse {
typeCounts: Record<string, number>;
list: VrfcReportSelect[];
}
export interface VrfcReportSelect {
vrfcCd: string;
statsNm: string;
rsnExpln: string;
admdstNm: string;
nowVlCn: string;
compVlCn: string;
}
export interface VrfcErrCountResponse {
statsYr: string;
vrfcErrCount: number;
}
export interface UnSubmitCountResponse {
statsYr: string;
unSubmitCount: number;
unSubmitList: UnSubmitStatsResponse[];
}
export interface UnSubmitStatsResponse {
statsCd: string;
statsNm: string;
}
export interface StatsSubmissionResponse {
totalCount: number;
submittedCount: number;
submissionRate: number;
statusLevel: string;
}
export interface EcptReqCountResponse {
statsYr: string;
ecptReqCount: number;
}
export interface DashboardService {
getVrfcReport(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<VrfcReportResponse>>;
getVrfcErrCount(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<VrfcErrCountResponse>>;
getUnSubmitCount(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<UnSubmitCountResponse>>;
getStatsSubmissionRate(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<StatsSubmissionResponse>>;
getEcptReqCount(
params: DashboardSearchParams,
): Promise<DashboardApiResponse<EcptReqCountResponse>>;
}
@@ -0,0 +1,30 @@
export interface ScheduleInfoSearchParams {
statsYr: string;
}
export interface ScheduleInfoApiResponse<T> {
status: string;
message: string;
data: T;
code: string;
httpStatus: number;
}
export interface ScheduleInfoResponse {
statsYr: string;
inptSeCd: number;
inptBgngYmd: string;
inptEndYmd: string;
vrfcSeCd: number;
vrfcBgngYmd: string;
vrfcEndYmd: string;
currentSe: string;
dday: string;
ddayLabel: string;
}
export interface ScheduleInfoService {
getScheduleInfo(
params: ScheduleInfoSearchParams,
): Promise<ScheduleInfoApiResponse<ScheduleInfoResponse>>;
}
@@ -196,11 +196,12 @@ export interface CopyVerifyDataRequest {
}
export interface VerifyResultSearchRequest {
sewerBzmnCd?: string | null;
ctpvNm?: string | null;
sggNm?: string | null;
sewerBzmnCd?: string | null;
statsYr: string;
statsCd?: string | null;
laeSbmsnSttsCd?: string | null;
vrfcMthdCd?: string;
prcsYnList?: string; // 검증 상태 리스트 (0: 미검증, 1: 수정완료, 2:예외통과, 3: 반려)
pbcpAprvYn?: string; // 공단 승인 여부 (Y/N)
@@ -6,6 +6,7 @@ export interface StatsInputStatusListRequestParams {
sewerBzmnCd?: string | null;
statsYr: string;
statsCd?: string | null;
laeSbmsnSttsCd?: string | null;
}
export interface StatsInputStatusList {
@@ -22,6 +22,10 @@ export interface SeweragePipeInputStatusInfo {
export interface VerifyResultItem {
vrfcCd: string;
rsnExpln: string;
/** 현재값(문자열로 내려오는 경우가 많음) */
nowVlCn?: string;
/** 비교값 */
compVlCn?: string;
}
/** 필드명 -> 검증 결과 배열 */
@@ -106,6 +110,12 @@ export type SeweragePipeInputRegionalFactStatusSaveItem = Omit<
"verificationResult"
>;
/** Tab2 저장 — saveList */
export type SeweragePipeInputFactInstallStatusSaveItem = Omit<
SeweragePipeInputFactInstallStatusItem,
"verificationResult"
>;
/** 지역별 시설현황 저장 API 요청 */
export interface SeweragePipeInputRegionalFactStatusSaveRequest {
statsYr: string;
@@ -118,7 +128,50 @@ export interface SeweragePipeInputRegionalFactStatusSaveRequest {
}
export interface SeweragePipeInputRegionalFactStatusSaveResponse {
data: string;
data?: string;
message?: string;
status: string;
}
/** 지역별 시설현황 제출 API 요청 (save item 구조 재사용) */
export interface SeweragePipeInputRegionalFactStatusSubmitRequest {
statsYr: string;
ctpvNm: string;
sggNm: string;
sewerBzmnCd: string;
userId: string;
laePicNm: string;
laePicTelNo: string;
laeKbrdrDeptNm: string;
laeKbrdrJbgdNm: string;
laeIdfrNm: string;
laeIdfrTelNo: string;
laeIdfrDeptNm: string;
laeIdfrJbgdNm: string;
submitList: SeweragePipeInputRegionalFactStatusSaveItem[];
}
export interface SeweragePipeInputRegionalFactStatusSubmitResponse {
data: {
finalStatus: string;
nvpassed: boolean;
}
message?: string;
status: string;
}
export interface SeweragePipeInputFactInstallStatusSaveRequest {
statsYr: string;
ctpvNm: string;
sggNm: string;
sewerBzmnCd: string;
userId: string;
superAdmin: string;
saveList: SeweragePipeInputFactInstallStatusSaveItem[];
}
export interface SeweragePipeInputFactInstallStatusSaveResponse {
data?: string;
message?: string;
status: string;
}
@@ -128,7 +181,7 @@ export interface SeweragePipeInputFactInstallStatusItem {
admdstCd: string;
sggNm: string;
admdstNm: string;
prcsFcltSn: number;
fcltSn: string;
swtrmtFcltCd: string;
swtrmtFcltNm: string;
prcsFcltCpct: number;
@@ -425,9 +478,9 @@ export interface SeweragePipeInputManholeResponse {
/** 시설별 현황 관리 - 하수처리장별 연결 현황 목록 항목 */
export interface SeweragePipeInputFacilityConnectionItem {
bscFctNm: string;
fctCpc: number;
bscFctCd: string;
fcltNm: string;
fcltCpct: number;
fcltSn: string;
isConnection: string;
connectionAreaList: string;
}
@@ -472,9 +525,10 @@ export interface SeweragePipeFacilitiesListItem {
ctpvNm: string;
sggNm: string;
admdstCd: string;
bscFctNm: string;
fctCpc: number;
bscFctCd: string;
fcltNm: string;
fcltCpct: number;
fcltCd: string;
fcltSn: string;
}
/** facilitiesList API 응답 */
@@ -490,7 +544,7 @@ export interface SeweragePipeFacilityAdminAreaListRequest {
swtrmtFcltCd: string;
sewerBzmnCd: string;
ctpvNm: string;
sggNm: string;
sggNm?: string;
}
/** 처리시설별 행정구역 목록 항목 */
@@ -509,6 +563,34 @@ export interface SeweragePipeFacilityAdminAreaListResponse {
message?: string;
}
/** 시설별 연결관계 저장 — saveList 항목 */
export interface SeweragePipeFactConnectionSaveItem {
admdstCd: string;
admdstNm: string;
}
/** 시설별 연결관계 저장 요청 */
export interface SeweragePipeFactConnectionSaveRequest {
ctpvNm: string;
sggNm: string;
fcltSn: string;
statsYr: string;
swtrmtFcltCd: string;
swtrmtFcltNm: string;
prcsFcltCpct: number;
sewerBzmnCd: string;
userId: string;
saveList: SeweragePipeFactConnectionSaveItem[];
deleteList: string[];
}
/** 시설별 연결관계 저장 응답 */
export interface SeweragePipeFactConnectionSaveResponse {
status: string;
data?: string;
message?: string;
}
export interface StatisticsInputSewPipeService {
getRegionalFactStatus(param: SeweragePipeRequestParams): Promise<SeweragePipeInputRegionalResponse>;
getFactInstallStatus(param: SeweragePipeRequestParams): Promise<SeweragePipeInputFactInstallResponse>;
@@ -518,7 +600,8 @@ export interface StatisticsInputSewPipeService {
getConnectionStatus(param: SeweragePipeRequestParams): Promise<SeweragePipeInputConnectionStatusResponse>;
getFacilitiesList(param: SeweragePipeFacilitiesListRequest): Promise<SeweragePipeFacilitiesListResponse>;
getFacilityAdminAreaList(param: SeweragePipeFacilityAdminAreaListRequest): Promise<SeweragePipeFacilityAdminAreaListResponse>;
saveRegionalFactStatus(
param: SeweragePipeInputRegionalFactStatusSaveRequest,
): Promise<SeweragePipeInputRegionalFactStatusSaveResponse>;
saveFactConnection(param: SeweragePipeFactConnectionSaveRequest): Promise<SeweragePipeFactConnectionSaveResponse>;
saveRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSaveRequest): Promise<SeweragePipeInputRegionalFactStatusSaveResponse>;
submitRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSubmitRequest): Promise<SeweragePipeInputRegionalFactStatusSubmitResponse>;
saveFactInstallStatus(param: SeweragePipeInputFactInstallStatusSaveRequest): Promise<SeweragePipeInputFactInstallStatusSaveResponse>;
}
@@ -167,11 +167,61 @@ export interface SewerageSupplyRateSubmitResponse {
status: string;
}
// 하수도 통계 입력 기간 조회
// 하수도 통계 입력 기간 조회 (입력 INPT / 검증 VRFC 구간)
export interface CheckInputPeriodItem {
statsYr: string;
seCd: number;
seType: string;
bgngYmd: string;
endYmd: string;
current: boolean;
}
export interface CheckInputPeriodResponse {
status: string;
message: string;
data: boolean;
data: CheckInputPeriodItem[];
}
/** 입력기간(INPT) 중 현재 구간이면 true */
export function isInputPeriodCurrent(periods: CheckInputPeriodItem[] | null | undefined): boolean {
return (periods ?? []).some((p) => p.seType === "INPT" && p.current === true);
}
/** `YYYYMMDD` → `yyyy-mm-dd` (그 외 형식은 그대로 반환) */
export function formatCheckInputYmdAsDash(ymd: string | null | undefined): string {
if (ymd == null || ymd === "") return "";
if (ymd.length === 8 && /^\d{8}$/.test(ymd)) {
return `${ymd.slice(0, 4)}-${ymd.slice(4, 6)}-${ymd.slice(6, 8)}`;
}
return ymd;
}
/** 검증기간(VRFC) 중 `current`인 항목 (없으면 null) */
export function getCurrentVerificationPeriod(
periods: CheckInputPeriodItem[] | null | undefined,
): CheckInputPeriodItem | null {
return (periods ?? []).find((p) => p.seType === "VRFC" && p.current === true) ?? null;
}
/** 검증기간(VRFC) 중 현재 구간이면 true — 저장·제출·제출 취소 불가 */
export function isVerificationPeriodCurrent(periods: CheckInputPeriodItem[] | null | undefined): boolean {
return getCurrentVerificationPeriod(periods) !== null;
}
/**
* 검증기간이면 차단 안내 문구(줄바꿈 + yyyy-mm-dd ~ yyyy-mm-dd), 아니면 null
*/
export function getVerificationPeriodActionBlockedMessage(
periods: CheckInputPeriodItem[] | null | undefined,
): string | null {
const v = getCurrentVerificationPeriod(periods);
if (!v) return null;
const from = formatCheckInputYmdAsDash(v.bgngYmd);
const to = formatCheckInputYmdAsDash(v.endYmd);
const range =
from && to ? `${from} ~ ${to}` : [from, to].filter(Boolean).join(" ~ ");
return `검증기간에는 저장, 제출, 제출 취소를 할 수 없습니다.\n검증기간: ${range}`;
}
export interface StatisticsInputSewSupplyRateService {
@@ -47,7 +47,7 @@ export const regionalGridColumnInfo: ColumnInfo = {
{
name: "chgRt",
fieldName: "chgRt",
header: { text: "R" },
header: { text: "전년대비 비율" },
width: 60,
valueType: ValueType.NUMBER,
dataFormat: "#,##0.00",
@@ -55,7 +55,7 @@ export const regionalGridColumnInfo: ColumnInfo = {
{
name: "trsfRt",
fieldName: "trsfRt",
header: { text: "S" },
header: { text: "S_i" },
width: 60,
valueType: ValueType.NUMBER,
dataFormat: "#,##0.00",
@@ -63,7 +63,7 @@ export const regionalGridColumnInfo: ColumnInfo = {
{
name: "otlrTrsfRt",
fieldName: "otlrTrsfRt",
header: { text: "E" },
header: { text: "기준값" },
width: 60,
valueType: ValueType.NUMBER,
dataFormat: "#,##0.00",
@@ -0,0 +1,77 @@
export interface SewagePipeMaintainTotalRequest {
ctpvNm?: string;
sggNm?: string;
statsYr?: string;
sewerBzmnCd?: string;
rowDataYn?: boolean;
}
export interface SewagePipeMaintainRepairTotalItem {
ctpvNm: string;
sggNm: string | null;
statsYr: string;
rowType: string;
sewerBzmnCd: string | null;
admdstNm: string;
mendngWhol: number;
cbsysMendngPrlg: number;
sprsysDrtwPrlg: number;
sprsysExlnPrlg: number;
partMendngWhol: number;
cbsysPartMendngPrlg: number;
sprsysDrtwMendngPrlg: number;
sprsysExlnMendngPrlg: number;
cbsysMnhlCnt: number;
sprsysDrtwMnhlCnt: number;
sprsysExlnMnhlCnt: number;
drtwRcvprpCnt: number;
exlnRcvprpCnt: number;
stofcbrCnt: number;
otflCnt: number;
cctvExmnPrlg: number;
rmrk: string | null;
}
export interface SewagePipeMaintainRepairTotalResponse {
status: string;
message?: string;
data: SewagePipeMaintainRepairTotalItem[];
}
/** `GET .../sewer-pipe-mtc/dredging` 목록 항목 */
export interface SewagePipeMaintainDredgingTotalItem {
ctpvNm: string;
sggNm: string | null;
admdstNm: string;
admdstCd: string;
statsYr: string;
rowType: string;
sewerBzmnCd: string | null;
wholFcltPrlg: number;
wholSwagePpl: number;
cbsysPplPrlg: number;
sprsysDrtwPrlg: number;
sprsysExlnPrlg: number;
mnhlCnt: number;
exlnDrtwRcvprpCnt: number;
stofcbrOtflCnt: number;
drdgnPlanPrlg: number;
drdgnTrgtCtrsDrdgnRt: number;
wholPrlgCtrsDrdgnRt: number;
drdgnAmt: number;
drdgnQnt: number;
drdgnUntprcAmt: number;
wholDrdgnPrcsQnt: number;
rclmQnt: number;
oceanEmsnQnt: number;
rcycQnt: number;
etcQnt: number;
rmrk: string | null;
}
/** `GET .../sewer-pipe-mtc/dredging` 응답 */
export interface SewagePipeMaintainDredgingTotalResponse {
status: string;
message?: string;
data: SewagePipeMaintainDredgingTotalItem[];
}
@@ -57,3 +57,180 @@ export interface SewagePipeTotalResponse {
message: string;
data: SewagePipeTotalItem[];
}
export interface SewagePipeFactInstallStatusItem {
statsYr: string;
ctpvNm: string;
sggNm: string | null;
admdstCd: string;
admdstNm: string;
swtrmtFcltNm: string;
prcsFcltCpct: number | null;
rowType: string;
prcsZoneArea: number;
cbsysPrcsZoneArea: number;
sprsysPrcsZoneArea: number;
sprsysVartnRt: number;
mltvEqpmCnt: number;
cbsysMltvEqpmCnt: number;
sprsysMltvEqpmCnt: number;
sprsysMltvLnkgCnt: number;
sprsysMltvYetLnkgCnt: number;
planPrcsZoneArea: number;
cbsysPlanPrcsZoneArea: number;
sprsysPlanPrcsZoneArea: number;
wholFcltPrlg: number;
wholIcptgAtlfcPrlg: number | null;
icptgPrlg: number;
atlfcsPrlg: number;
cbsysStofcbrCnt: number;
sprsysStofcbrCnt: number;
sprsysExlnOtflCnt: number;
wholBrchlnPrlg: number;
cbsysBrchlnPrlg: number;
exlnBrchlnPrlg: number;
drtwBrchlnPrlg: number;
}
export interface SewagePipeFactInstallStatusResponse {
status: string;
message?: string;
data: SewagePipeFactInstallStatusItem[];
}
/** ST00020003 합계 — `GET .../sewer-pipe/pipe-type-facility-status` */
export interface SewagePipePipeTypeFacilityStatusItem {
pipeByPrstWhol: number;
lyngPrlg: number;
dmltnPrlg: number;
yy1PrlgWhol: number;
yy1LyngPrlg: number;
yy1DmltnPrlg: number;
yy2PrlgWhol: number;
yy2LyngPrlg: number;
yy2DmltnPrlg: number;
yy3PrlgWhol: number;
yy3LyngPrlg: number;
yy3DmltnPrlg: number;
yy4PrlgWhol: number;
yy4LyngPrlg: number;
yy4DmltnPrlg: number;
yy5PrlgWhol: number;
yy5LyngPrlg: number;
yy5DmltnPrlg: number;
yy6PrlgWhol: number;
yy6LyngPrlg: number;
yy6DmltnPrlg: number;
yy7PrlgWhol: number;
yy7LyngPrlg: number;
yy7DmltnPrlg: number;
yy8PrlgWhol: number;
yy8LyngPrlg: number;
yy8DmltnPrlg: number;
yy9PrlgWhol: number;
yy9LyngPrlg: number;
yy9DmltnPrlg: number;
yy10PrlgWhol: number;
yy10LyngPrlg: number;
yy10DmltnPrlg: number;
yy11PrlgWhol: number;
yy11LyngPrlg: number;
yy11DmltnPrlg: number;
yy12PrlgWhol: number;
yy12LyngPrlg: number;
yy12DmltnPrlg: number;
yy13PrlgWhol: number;
yy13LyngPrlg: number;
yy13DmltnPrlg: number;
yy14PrlgWhol: number;
yy14LyngPrlg: number;
yy14DmltnPrlg: number;
yy15PrlgWhol: number;
yy15LyngPrlg: number;
yy15DmltnPrlg: number;
yy16PrlgWhol: number;
yy16LyngPrlg: number;
yy16DmltnPrlg: number;
yy17PrlgWhol: number;
yy17LyngPrlg: number;
yy17DmltnPrlg: number;
yy18PrlgWhol: number;
yy18LyngPrlg: number;
yy18DmltnPrlg: number;
yy19PrlgWhol: number;
yy19LyngPrlg: number;
yy19DmltnPrlg: number;
yy20PrlgWhol: number;
yy20LyngPrlg: number;
yy20DmltnPrlg: number;
yy21PrlgWhol: number;
yy21LyngPrlg: number;
yy21DmltnPrlg: number;
yy22PrlgWhol: number;
yy22LyngPrlg: number;
yy22DmltnPrlg: number;
yy23PrlgWhol: number;
yy23LyngPrlg: number;
yy23DmltnPrlg: number;
yy24PrlgWhol: number;
yy24LyngPrlg: number;
yy24DmltnPrlg: number;
yy25PrlgWhol: number;
yy25LyngPrlg: number;
yy25DmltnPrlg: number;
yy26PrlgWhol: number;
yy26LyngPrlg: number;
yy26DmltnPrlg: number;
yy27PrlgWhol: number;
yy27LyngPrlg: number;
yy27DmltnPrlg: number;
yy28PrlgWhol: number;
yy28LyngPrlg: number;
yy28DmltnPrlg: number;
yy29PrlgWhol: number;
yy29LyngPrlg: number;
yy29DmltnPrlg: number;
yy30PrlgWhol: number;
yy30LyngPrlg: number;
yy30DmltnPrlg: number;
ctpvNm: string;
sggNm: string | null;
admdstCd: string;
admdstNm: string;
swrgPipeNm: string;
statsYr: string;
rowType: string;
rmrk: string | null;
}
export interface SewagePipePipeTypeFacilityStatusResponse {
status: string;
message?: string;
data: SewagePipePipeTypeFacilityStatusItem[];
}
/** ST00020004 합계 — `GET .../sewer-pipe/pipe-diam-status` */
export interface SewagePipePipeDiamStatusItem {
ctpvNm: string;
sggNm: string | null;
admdstCd: string;
admdstNm: string;
statsYr: string;
swrgPipeNm: string;
rowType: string;
pdmtByTsmWhol: number;
step1PdmtPrlg: number;
step2PdmtPrlg: number;
step3PdmtPrlg: number;
step4PdmtPrlg: number;
step5PdmtPrlg: number;
sqrePdmtPrlg: number;
etcPdmtPrlg: number;
rmrk: string | null;
}
export interface SewagePipePipeDiamStatusResponse {
status: string;
message?: string;
data: SewagePipePipeDiamStatusItem[];
}
@@ -4,6 +4,8 @@ export interface SewerageSupplyRateRequest {
statsYr?: string;
sewerBzmnCd?: string;
rowDataYn?: boolean;
summaryYn?: boolean;
summaryYearCnt?: number;
}
export interface SewerageSupplyRateTotalItem {
@@ -0,0 +1,16 @@
import {
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from "@dnd-kit/core";
import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
export function useDndSensors() {
return useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
}
@@ -0,0 +1,64 @@
import { useEffect, useState } from "react";
import type { DragEndEvent } from "@dnd-kit/core";
import { arrayMove } from "@dnd-kit/sortable";
import type { PanelItem } from "../../model/types";
interface UsePanelOrderProps {
initialPanels: PanelItem[];
storageKey: string;
}
export function usePanelOrder({
initialPanels,
storageKey,
}: UsePanelOrderProps) {
const [panels, setPanels] = useState<PanelItem[]>(initialPanels);
const [isInitialized, setIsInitialized] = useState(false);
useEffect(() => {
if (initialPanels.length === 0 || isInitialized) return;
const saved = localStorage.getItem(storageKey);
if (saved) {
try {
const savedIds = JSON.parse(saved);
const restoredPanels = savedIds
.map((id: string) => initialPanels.find((p) => p.id === id))
.filter(Boolean);
if (restoredPanels.length === initialPanels.length) {
setPanels(restoredPanels);
}
} catch {
setPanels(initialPanels);
}
} else {
setPanels(initialPanels);
}
setIsInitialized(true);
}, [storageKey, initialPanels, isInitialized]);
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (over && active.id !== over.id) {
setPanels((items) => {
const oldIndex = items.findIndex(
(item) => item.id === active.id,
);
const newIndex = items.findIndex((item) => item.id === over.id);
const newItems = arrayMove(items, oldIndex, newIndex);
localStorage.setItem(
storageKey,
JSON.stringify(newItems.map((item) => item.id)),
);
return newItems;
});
}
};
return { panels, handleDragEnd };
}
@@ -0,0 +1,60 @@
import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface";
import type { ScheduleInfoSearchParams } from "@/entities/statistics/model/dashboard/schedule-info-interface";
import { NoticePanel } from "../ui/panels/notice-panel";
import { ScheduleCalendarPanel } from "../ui/panels/schedule-calendar-panel";
import { ScheduleInfoPanel } from "../ui/panels/schedule-info-panel";
import { StatsSubmissionPanel } from "../ui/panels/stats-submission-panel";
import { UnSubmitPanel } from "../ui/panels/unsubmit-panel";
// import { VrfcErrPanel } from "../ui/panels/vrfc-err-panel";
import { VrfcReportPanel } from "../ui/panels/vrfc-report-panel";
import type { PanelItem } from "./types";
interface GetInitialPanelsParams {
dashboardParams: DashboardSearchParams | null;
scheduleInfoParams: ScheduleInfoSearchParams | null;
}
export function getInitialPanels({
dashboardParams,
scheduleInfoParams,
}: GetInitialPanelsParams): PanelItem[] {
return [
{
id: "panel-a",
component: <StatsSubmissionPanel params={dashboardParams} />,
className: "",
},
{
id: "panel-b",
component: <ScheduleInfoPanel params={scheduleInfoParams} />,
className: "",
},
{
id: "panel-c",
component: <NoticePanel />,
className: "",
},
// {
// id: "panel-d",
// component: <VrfcErrPanel params={dashboardParams} />,
// className: "",
// },
{
id: "panel-d",
component: <ScheduleCalendarPanel />,
className: "",
},
{
id: "panel-e",
component: <VrfcReportPanel params={dashboardParams} />,
className: "",
},
{
id: "panel-f",
component: <UnSubmitPanel params={dashboardParams} />,
className: "",
},
];
}
@@ -0,0 +1,5 @@
export interface PanelItem {
id: string;
component: React.ReactNode;
className?: string;
}
@@ -0,0 +1,88 @@
"use client";
import { useRouter } from "next/navigation";
import { createColumnHelper, getCoreRowModel } from "@tanstack/react-table";
import dayjs from "dayjs";
import { useDataTable } from "@/shared/lib";
import { type PstRecentListResponse, useRecentPostList } from "@/entities/post";
import { Button } from "@common/components";
import { useDragHandle } from "../sortable-panel";
export function NoticePanel() {
const { dragHandleProps, isDragging } = useDragHandle();
const router = useRouter();
const recentPostListQuery = useRecentPostList({
page: 0,
size: 4,
keywordType: "bbsTypeCd",
keyword: "BBSNOTICE",
});
const handleRowClick = (post: PstRecentListResponse) => {
router.push(
`/cms/${post.bbsTypeCd.toLowerCase()}/${post.bbsId}/${post.pstNo}`,
);
};
const handleMoreClick = () => {
router.push("/cms/bbsnotice/7");
};
const { tableComponent } = useDataTable({
query: recentPostListQuery,
tableOptions: {
columns: noticeColumns,
getCoreRowModel: getCoreRowModel(),
},
dataTableOptions: {
variant: "line",
onRowClick: handleRowClick,
},
});
return (
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
<div
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
{...dragHandleProps}
title="헤더를 드래그하여 패널 순서 변경"
>
<h3 className="text-2xl font-semibold">공지사항</h3>
<Button
size="sm"
className="!text-xl"
onClick={handleMoreClick}
>
더보기
</Button>
</div>
{tableComponent}
</div>
);
}
const columnHelper = createColumnHelper<PstRecentListResponse>();
const noticeColumns = [
columnHelper.accessor("pstTtl", {
header: "제목",
cell: ({ row }) => (
<div className="min-w-[20rem] truncate">{row.original.pstTtl}</div>
),
}),
columnHelper.accessor("wrtrNm", {
header: "작성자",
}),
columnHelper.accessor("regDt", {
header: "등록일",
cell: ({ row }) => formatDate(row.original.regDt),
}),
];
function formatDate(value: string) {
return dayjs(value).format("YYYY-MM-DD");
}
@@ -0,0 +1,81 @@
"use client";
import dynamic from "next/dynamic";
import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid";
import dayjs from "dayjs";
import { SITE_ID } from "@/shared/config";
import { useSchedules } from "@/entities/schedule";
import { LoadingWithAnimation } from "@common/components";
import { useDragHandle } from "../sortable-panel";
// FullCalendar는 클라이언트 전용이므로 dynamic import
const FullCalendar = dynamic(() => import("@fullcalendar/react"), {
ssr: false,
});
export function ScheduleCalendarPanel() {
const { dragHandleProps, isDragging } = useDragHandle();
// TBD: 초기 날짜 범위 설정 로직
const today = dayjs();
const startOfMonth = today.startOf("month").format("YYYY-MM-DD");
const endOfMonth = today.endOf("month").format("YYYY-MM-DD");
const { data, isLoading } = useSchedules({
siteId: SITE_ID,
schdlBgngYmd: startOfMonth,
schdlEndYmd: endOfMonth,
});
return (
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
<div
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
{...dragHandleProps}
title="헤더를 드래그하여 패널 순서 변경"
>
<h3 className="text-2xl font-semibold">일정관리</h3>
</div>
<div className="card-body max-h-[calc(100%-3.25rem)] py-2.5">
{isLoading && (
<div className="flex h-full items-center justify-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
{data && (
<div className="h-[48rem]">
<FullCalendar
plugins={[dayGridPlugin, timeGridPlugin]}
initialView="dayGridMonth"
locale="ko"
headerToolbar={{
left: "prev,next",
center: "title",
right: "dayGridMonth,timeGridWeek",
}}
buttonText={{
month: "월",
week: "주",
}}
events={data.map((schedule) => ({
id: schedule.schdlSn.toString(),
title: schedule.schdlTtl,
start: schedule.schdlBgngDt,
end: schedule.schdlEndDt,
// TBD: allDay 판단 기준 (1439분 이상?)
allDay: true,
}))}
height="100%"
/>
</div>
)}
</div>
</div>
);
}
@@ -0,0 +1,138 @@
"use client";
import dayjs from "dayjs";
import type { ScheduleInfoSearchParams } from "@/entities/statistics";
import { useScheduleInfo } from "@/entities/statistics";
import {
Badge,
type BadgeProps,
LoadingWithAnimation,
} from "@common/components";
import { useDragHandle } from "../sortable-panel";
interface ScheduleInfoPanelProps {
params: ScheduleInfoSearchParams | null;
}
const CURRENT_STATUS_LABEL_MAP = {
INPT: "입력중",
VRFC: "검증중",
NONE: "기간외",
};
const CURRENT_STATUS_COLOR_MAP = {
INPT: "",
VRFC: "gray",
NONE: "lightGray",
};
export function ScheduleInfoPanel({ params }: ScheduleInfoPanelProps) {
const { dragHandleProps, isDragging } = useDragHandle();
const { data, isLoading, error } = useScheduleInfo(params);
return (
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
<div
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
{...dragHandleProps}
title="헤더를 드래그하여 패널 순서 변경"
>
<h3 className="text-2xl font-semibold">일정안내</h3>
{data && (
<div className="flex items-center gap-2">
<span className="text-xl tracking-tight">
{data.ddayLabel}
</span>
<Badge
size="lg"
color="orange"
className="rounded-full !px-3 text-xl font-semibold"
>
{data.dday}
</Badge>
</div>
)}
</div>
<div className="card-body max-h-[calc(100%-3.25rem)] py-2.5">
{isLoading && (
<div className="flex h-full items-center justify-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
{error && (
<div className="flex h-full items-center justify-center">
데이터를 불러올 수 없습니다.
</div>
)}
{data && (
<>
<div className="space-y-4">
<div className="mt-2 flex items-center justify-between">
<h4 className="px-2 text-2xl font-semibold tracking-tight text-label">
이번 주 주요 일정
</h4>
<div className="flex items-center gap-2">
<span className="tracking-tight text-label">
현재 진행 상태
</span>
<Badge
color={
CURRENT_STATUS_COLOR_MAP[
data.currentSe as keyof typeof CURRENT_STATUS_COLOR_MAP
] as BadgeProps["color"]
}
className="rounded-full !px-3"
>
{
CURRENT_STATUS_LABEL_MAP[
data.currentSe as keyof typeof CURRENT_STATUS_LABEL_MAP
]
}
</Badge>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between rounded-sm border border-border p-3">
<Badge size="lg" className="font-semibold">
{data.inptSeCd}차 입력기간
</Badge>
<p className="text-2xl">
{dayjs(data.inptBgngYmd).format(
"YYYY.MM.DD",
)}{" "}
~{" "}
{dayjs(data.inptEndYmd).format(
"YYYY.MM.DD",
)}
</p>
</div>
<div className="flex items-center justify-between rounded-sm border border-border p-3">
<Badge
size="lg"
color="gray"
className="font-semibold"
>
{data.vrfcSeCd}차 검증기간
</Badge>
<p className="text-2xl">
{dayjs(data.vrfcBgngYmd).format(
"YYYY.MM.DD",
)}{" "}
~{" "}
{dayjs(data.vrfcEndYmd).format(
"YYYY.MM.DD",
)}
</p>
</div>
</div>
</div>
</>
)}
</div>
</div>
);
}
@@ -0,0 +1,163 @@
"use client";
import { Frown, Laugh, Meh, OctagonAlert, OctagonX, Smile } from "lucide-react";
import {
useDashboardEcptReqCount,
useDashboardStatsSubmissionRate,
useDashboardVrfcErrCount,
} from "@/entities/statistics";
import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface";
import { LoadingWithAnimation, Progress } from "@common/components";
import { useDragHandle } from "../sortable-panel";
interface StatsSubmissionPanelProps {
params: DashboardSearchParams | null;
}
const STATUS_LEVEL_TEXT_COLOR = {
GREAT: "text-blue-500",
GOOD: "text-green-500",
WARNING: "text-yellow-500",
BAD: "text-red-500",
};
const STATUS_LEVEL_EMOJI = {
GREAT: <Laugh size={160} className={STATUS_LEVEL_TEXT_COLOR.GREAT} />,
GOOD: <Smile size={160} className={STATUS_LEVEL_TEXT_COLOR.GOOD} />,
WARNING: <Meh size={160} className={STATUS_LEVEL_TEXT_COLOR.WARNING} />,
BAD: <Frown size={160} className={STATUS_LEVEL_TEXT_COLOR.BAD} />,
};
export function StatsSubmissionPanel({ params }: StatsSubmissionPanelProps) {
const { dragHandleProps, isDragging } = useDragHandle();
const { data, isLoading, error } = useDashboardStatsSubmissionRate(params);
const vrfcErrQuery = useDashboardVrfcErrCount(params);
const ecptReqQuery = useDashboardEcptReqCount(params);
return (
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
{/* <div
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
{...dragHandleProps}
title="헤더를 드래그하여 패널 순서 변경"
>
<h3 className="text-2xl font-semibold">통계입력현황</h3>
</div> */}
<div
className={`card-body cursor-grab py-2 active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
{...dragHandleProps}
>
<div className="flex h-full gap-6 px-2">
<div className="h-full flex-1 flex-col">
<h3 className="text-2xl font-semibold">통계입력현황</h3>
{isLoading && (
<div className="flex h-full items-center justify-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
{error && (
<div className="flex h-full items-center justify-center py-8 text-center">
데이터를 불러올 수 없습니다.
</div>
)}
<div className="h-full">
{data && (
<div className="flex h-full flex-col items-center gap-4">
{/* 통계제출률 상태 이모지 */}
<div className="flex justify-center">
{
STATUS_LEVEL_EMOJI[
data.statusLevel as keyof typeof STATUS_LEVEL_EMOJI
]
}
</div>
{/* 통계제출률 프로그레스바 */}
<div className="w-full space-y-3">
<div className="flex w-full justify-between">
<span className="text-2xl">
통계제출률
</span>
<span className="text-2xl font-semibold">
{data.submissionRate?.toFixed(
1,
)}
%
<span className="ml-1">
({data.submittedCount}/
{data.totalCount})
</span>
</span>
</div>
<Progress
value={data.submissionRate}
className="h-6 bg-muted"
/>
</div>
</div>
)}
</div>
</div>
<div className="h-full flex-[0.5]">
<h3 className="text-2xl font-semibold">
검증오류 및 예외
</h3>
<div className="mt-4 space-y-4">
{/* 검증오류 수 카드 */}
<div className="flex flex-col items-center justify-center gap-2 rounded-lg border border-red-200 bg-red-100 p-4 text-center">
<div className="flex flex-col items-center gap-2">
<OctagonX
className="text-red-600"
size={20}
/>
<span className="text-lg">검증오류 수</span>
</div>
{vrfcErrQuery.isLoading && (
<div className="text-2xl font-bold">-</div>
)}
{vrfcErrQuery.error && (
<div className="text-sm">
데이터를 불러올 수 없습니다.
</div>
)}
{vrfcErrQuery.data && (
<div className="text-5xl font-bold text-red-600">
{vrfcErrQuery.data.vrfcErrCount.toLocaleString()}
</div>
)}
</div>
{/* 예외신청 수 카드 */}
<div className="flex flex-col items-center justify-center gap-2 rounded-lg border border-orange-200 bg-orange-100 p-4 text-center">
<div className="flex flex-col items-center gap-2">
<OctagonAlert
className="text-orange-600"
size={20}
/>
<span className="text-lg">예외신청 수</span>
</div>
{ecptReqQuery.isLoading && (
<div className="text-2xl font-bold">-</div>
)}
{ecptReqQuery.error && (
<div className="text-sm">
데이터를 불러올 수 없습니다.
</div>
)}
{ecptReqQuery.data && (
<div className="text-5xl font-bold text-orange-600">
{ecptReqQuery.data.ecptReqCount.toLocaleString()}
</div>
)}
</div>
</div>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,69 @@
"use client";
import { useDashboardUnSubmitCount } from "@/entities/statistics";
import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface";
import { Badge, LoadingWithAnimation } from "@common/components";
import { useDragHandle } from "../sortable-panel";
interface UnSubmitPanelProps {
params: DashboardSearchParams | null;
}
export function UnSubmitPanel({ params }: UnSubmitPanelProps) {
const { dragHandleProps, isDragging } = useDragHandle();
const { data, isLoading, error } = useDashboardUnSubmitCount(params);
return (
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
<div
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
{...dragHandleProps}
title="헤더를 드래그하여 패널 순서 변경"
>
<h3 className="text-2xl font-semibold">미제출 지표</h3>
{data && (
<Badge size="lg" className="rounded-full !px-3">
{data.unSubmitCount}건
</Badge>
)}
</div>
<div className="card-body max-h-[calc(100%-3.25rem)] py-2.5">
{isLoading && (
<div className="flex h-full items-center justify-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
{error && (
<div className="flex h-full items-center justify-center">
데이터를 불러올 수 없습니다.
</div>
)}
{data && (
<div className="h-full">
{data.unSubmitList.length === 0 ? (
<div className="flex h-full items-center justify-center">
미제출 지표가 없습니다.
</div>
) : (
<div className="max-h-[48rem] space-y-2 overflow-y-auto">
{data.unSubmitList.map((item) => (
<div
key={item.statsCd}
className="rounded-sm border border-border p-3"
>
<p className="font-medium">
{item.statsNm}
</p>
</div>
))}
</div>
)}
</div>
)}
</div>
</div>
);
}
@@ -0,0 +1,81 @@
"use client";
import { OctagonAlert, OctagonX } from "lucide-react";
import {
useDashboardEcptReqCount,
useDashboardVrfcErrCount,
} from "@/entities/statistics";
import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface";
import { useDragHandle } from "../sortable-panel";
interface VrfcErrPanelProps {
params: DashboardSearchParams | null;
}
export function VrfcErrPanel({ params }: VrfcErrPanelProps) {
const { dragHandleProps, isDragging } = useDragHandle();
const vrfcErrQuery = useDashboardVrfcErrCount(params);
const ecptReqQuery = useDashboardEcptReqCount(params);
return (
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
<div
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
{...dragHandleProps}
title="헤더를 드래그하여 패널 순서 변경"
>
<h3 className="text-2xl font-semibold">검증오류 및 예외</h3>
</div>
<div className="card-body max-h-[calc(100%-3.25rem)] py-2.5">
<div className="grid h-full grid-cols-2 gap-4">
{/* 검증오류 수 카드 */}
<div className="flex flex-col items-center justify-center gap-2 rounded-lg border border-red-200 bg-red-100 p-4 text-center">
<div className="flex flex-col items-center gap-2">
<OctagonX className="text-red-600" size={20} />
<span className="text-lg">검증오류 수</span>
</div>
{vrfcErrQuery.isLoading && (
<div className="text-2xl font-bold">-</div>
)}
{vrfcErrQuery.error && (
<div className="text-sm">
데이터를 불러올 수 없습니다.
</div>
)}
{vrfcErrQuery.data && (
<div className="text-5xl font-bold text-red-600">
{vrfcErrQuery.data.vrfcErrCount.toLocaleString()}
</div>
)}
</div>
{/* 예외신청 수 카드 */}
<div className="flex flex-col items-center justify-center gap-2 rounded-lg border border-orange-200 bg-orange-100 p-4 text-center">
<div className="flex flex-col items-center gap-2">
<OctagonAlert
className="text-orange-600"
size={20}
/>
<span className="text-lg">예외신청 수</span>
</div>
{ecptReqQuery.isLoading && (
<div className="text-2xl font-bold">-</div>
)}
{ecptReqQuery.error && (
<div className="text-sm">
데이터를 불러올 수 없습니다.
</div>
)}
{ecptReqQuery.data && (
<div className="text-5xl font-bold text-orange-600">
{ecptReqQuery.data.ecptReqCount.toLocaleString()}
</div>
)}
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,131 @@
"use client";
import { useDashboardVrfcReport } from "@/entities/statistics";
import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface";
import {
Badge,
type BadgeProps,
LoadingWithAnimation,
} from "@common/components";
import { useDragHandle } from "../sortable-panel";
interface VrfcReportPanelProps {
params: DashboardSearchParams | null;
}
const TYPE_CODE_NAME_MAP: Record<string, string> = {
NV: "일반",
IMR: "I-MR",
};
const TYPE_CODE_COLOR_MAP: Record<string, BadgeProps["color"]> = {
NV: "gray",
IMR: "red",
};
export function VrfcReportPanel({ params }: VrfcReportPanelProps) {
const { dragHandleProps, isDragging } = useDragHandle();
const { data, isLoading, error } = useDashboardVrfcReport(params);
return (
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
<div
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
{...dragHandleProps}
title="헤더를 드래그하여 패널 순서 변경"
>
<h3 className="text-2xl font-semibold">이상치 선별보고서</h3>
</div>
<div className="card-body max-h-[calc(100%-3.25rem)] py-2.5">
{isLoading && (
<div className="flex h-full items-center justify-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
{error && (
<div className="flex h-full items-center justify-center">
데이터를 불러올 수 없습니다.
</div>
)}
{data && (
<div className="space-y-2">
{/* TBD: typeCounts 표시 방법 (탭? 필터?) */}
{data.typeCounts &&
Object.keys(data.typeCounts).length > 0 && (
<div className="flex flex-wrap gap-2">
{Object.entries(data.typeCounts).map(
([type, count]) => (
<Badge
key={type}
color={
TYPE_CODE_COLOR_MAP[type]
}
className="rounded-full px-2 py-1 text-lg"
>
{TYPE_CODE_NAME_MAP[type]}:{" "}
{count}
</Badge>
),
)}
</div>
)}
{/* TBD: 테이블 컬럼 정의, 증감 표시 방법 */}
<div className="max-h-[44rem] overflow-y-auto">
{data.list.length === 0 ? (
<div className="flex h-full items-center justify-center">
이상치 항목이 없습니다.
</div>
) : (
<div className="space-y-2 tracking-tight">
{data.list.map((item, index) => (
<div
key={index}
className="flex flex-col rounded-sm border border-border p-3"
>
<div className="mb-2 flex items-center justify-between">
<span className="font-medium">
{item.statsNm}
</span>
<Badge
color={
TYPE_CODE_COLOR_MAP[
item.vrfcCd
]
}
className="text-sm"
>
{
TYPE_CODE_NAME_MAP[
item.vrfcCd
]
}
</Badge>
</div>
<p className="text-lg text-muted-foreground">
{item.rsnExpln}
</p>
<div className="flex justify-between text-lg text-muted-foreground">
<span>{item.admdstNm}</span>
<span>
{/* TBD: 증감 표시 방법 (nowVlCn - compVlCn 계산?) */}
현재: {item.nowVlCn ?? "-"}{" "}
/ 비교:{" "}
{item.compVlCn ?? "-"}
</span>
</div>
{/* TBD: 등급/점수/단계/업데이트 시각 필드 미제공 */}
</div>
))}
</div>
)}
</div>
</div>
)}
</div>
</div>
);
}
@@ -0,0 +1,60 @@
import { createContext, useContext } from "react";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
interface SortablePanelProps {
id: string;
children: React.ReactNode;
className?: string;
}
interface DragHandleContextValue {
dragHandleProps: React.HTMLAttributes<HTMLElement>;
isDragging: boolean;
}
const DragHandleContext = createContext<DragHandleContextValue | null>(null);
export function SortablePanel({ id, children, className }: SortablePanelProps) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
};
const contextValue: DragHandleContextValue = {
dragHandleProps: { ...attributes, ...listeners },
isDragging,
};
return (
<DragHandleContext.Provider value={contextValue}>
<div ref={setNodeRef} style={style} className={className}>
{children}
</div>
</DragHandleContext.Provider>
);
}
export function useDragHandle() {
const context = useContext(DragHandleContext);
if (!context) {
return {
dragHandleProps: {},
isDragging: false,
};
}
return context;
}
@@ -1,26 +1,28 @@
"use client";
import { Suspense, useRef, useEffect } from "react";
import { Suspense, useEffect, useRef } from "react";
import dynamic from "next/dynamic";
import { useTabStore } from "@/store/tab-store";
import { useAdminStore } from "@/store/admin-store";
import Dashboard from "@/components/Dashboard";
import { SkeletonPage } from "@/shared/ui/skeleton-page";
import "@rsone/components/index.css";
import Dashboard from "@/components/statistics/Dashboard";
import { useAdminStore } from "@/store/admin-store";
import { useTabStore } from "@/store/tab-store";
const StatisticsPage = () => {
const { tabs, activeTabId } = useTabStore();
const fetchAdminInfo = useAdminStore((state) => state.fetchAdminInfo);
// 컴포넌트 캐시를 useRef로 유지 (재렌더링 시에도 유지됨)
const componentCache = useRef(new Map<string, React.ComponentType>());
// 닫힌 탭의 컴포넌트는 캐시에서 제거 (메모리 정리)
useEffect(() => {
const currentTabIds = new Set(tabs.map(tab => tab.id));
const currentTabIds = new Set(tabs.map((tab) => tab.id));
const cachedTabIds = Array.from(componentCache.current.keys());
cachedTabIds.forEach((cachedId) => {
if (!currentTabIds.has(cachedId)) {
componentCache.current.delete(cachedId);
@@ -35,28 +37,28 @@ const StatisticsPage = () => {
// 탭이 없으면 Dashboard 표시
if (tabs.length === 0) {
return (
<div className="card card-border !mt-6">
<div className="card card-border !mt-6 border-border bg-background">
<Dashboard />
</div>
);
}
return (
<div className="card card-border !mt-6">
<div className="card card-border !mt-6 border-border bg-background">
{tabs.map((tab) => {
// 렌더링 시점에 컴포넌트가 없으면 바로 로드
if (!componentCache.current.has(tab.id)) {
const componentName = tab.urlPath;
const parentComponentName = tab.parentEngNm;
const DynamicComponent = dynamic(
() => import(`@/components/statistics/${parentComponentName}/${componentName}`),
{
ssr: false,
loading: () => <SkeletonPage />
}
() =>
import(
`@/components/statistics${parentComponentName ? `/${parentComponentName}` : ""}/${componentName}`
),
{ ssr: false, loading: () => <SkeletonPage /> },
);
componentCache.current.set(tab.id, DynamicComponent);
}
@@ -68,7 +70,7 @@ const StatisticsPage = () => {
return (
<div
key={tab.id}
style={{ display: isActive ? 'block' : 'none' }}
style={{ display: isActive ? "block" : "none" }}
>
<Suspense fallback={<SkeletonPage />}>
<Component />
@@ -80,4 +82,4 @@ const StatisticsPage = () => {
);
};
export default StatisticsPage;
export default StatisticsPage;
+1
View File
@@ -74,6 +74,7 @@ export const ENDPOINT = {
INPUTVERIFY: "/st-input-service/api/v1/verify",
TOTAL: "/st-management-service/api/v1/stats",
APPROVAL: "/st-management-service/api/stats",
DASHBOARD: "/st-management-service/api/v1/dashboard",
},
STATVERIFY_SERVICE: {
VRFCTGT_MNG: "/statverify-service/api/v1/vrfc-trgt-mng",
+30 -26
View File
@@ -1,42 +1,46 @@
import type { HTMLAttributes } from "react";
import type { UseQueryResult } from "@tanstack/react-query";
import { useReactTable, type TableOptions } from "@tanstack/react-table";
import { type TableOptions, useReactTable } from "@tanstack/react-table";
import { DataTable } from "@common/components";
import { NoData } from "../ui/no-data";
import type { PageableList } from "../model";
import { NoData } from "../ui/no-data";
import { matchQueryStatus } from "./match-query-status";
type UseDataTableOptions<T> = {
query: UseQueryResult<PageableList<T>>;
tableOptions: Omit<TableOptions<T>, "data" | "rowCount">;
dataTableOptions?: DataTableOptions<T>;
query: UseQueryResult<PageableList<T>>;
tableOptions: Omit<TableOptions<T>, "data" | "rowCount">;
dataTableOptions?: DataTableOptions<T>;
};
type DataTableOptions<T> = {
variant?: "default" | "secondary" | "line";
className?: HTMLAttributes<HTMLTableElement>["className"];
viewPagination?: boolean;
onRowClick?: (row: T) => void;
variant?: "default" | "secondary" | "line";
className?: HTMLAttributes<HTMLTableElement>["className"];
viewPagination?: boolean;
onRowClick?: (row: T) => void;
};
export const useDataTable = <T,>({
query,
tableOptions,
dataTableOptions,
query,
tableOptions,
dataTableOptions,
}: UseDataTableOptions<T>) => {
const table = useReactTable({
...tableOptions,
data: query.data?.content ?? [],
rowCount: query.data?.totalElements ?? 0,
});
const table = useReactTable({
...tableOptions,
data: query.data?.content ?? [],
rowCount: query.data?.totalElements ?? 0,
});
return {
tableComponent: matchQueryStatus(query, {
Empty: <NoData />,
Success: () => {
return <DataTable table={table} {...dataTableOptions} />;
},
}),
tableInstance: table,
};
return {
tableComponent: matchQueryStatus(query, {
Empty: <NoData />,
Success: () => {
return <DataTable table={table} {...dataTableOptions} />;
},
}),
tableInstance: table,
};
};
+9 -7
View File
@@ -1,14 +1,16 @@
import { useStore } from "zustand";
import { useContext } from "react";
import type { UserStore } from "../store/user-store";
import { useStore } from "zustand";
import { StoreContext } from "../providers/store-provider";
import type { UserStore } from "../store/user-store";
export const useUserStore = <T>(selector: (store: UserStore) => T): T => {
const storeContext = useContext(StoreContext);
const storeContext = useContext(StoreContext);
if (!storeContext) {
throw new Error("useUserStore must be used within StoreProvider");
}
if (!storeContext) {
throw new Error("useUserStore must be used within StoreProvider");
}
return useStore(storeContext.userStore, selector);
return useStore(storeContext.userStore, selector);
};
+43 -41
View File
@@ -1,59 +1,61 @@
import { createStore } from "zustand/vanilla";
export interface IUserInfo {
regDt: string;
mdfcnDt: string;
rgtr: string;
mdfr: string;
adminRoleYn: string;
userUnqId: string;
userId: string;
userName: string;
email: string;
encryptedPassword: string;
refreshToken: string;
userStateCode: string;
lastLoginDate: string;
loginFailCount: number;
pswdHnt: string;
pswdAns: string;
mphn: string;
telno: string;
zip: string;
addr: string;
daddr: string;
laeSeCd: string;
/** 기관 구분 코드 — 통계 공통 조회 등 query `instSeCd`로 전달 */
instSeCd?: string;
ogdpSeCd: string;
aprvYn: string;
autzrId: string;
expryYmd: string;
lastPswdChgDt: string;
googleId: string;
kakaoId: string;
naverId: string;
isSocialUser: boolean;
hasPassword: boolean;
regDt: string;
mdfcnDt: string;
rgtr: string;
mdfr: string;
adminRoleYn: string;
userUnqId: string;
userId: string;
userName: string;
email: string;
encryptedPassword: string;
enoCd: string;
refreshToken: string;
userStateCode: string;
lastLoginDate: string;
loginFailCount: number;
pswdHnt: string;
pswdAns: string;
mphn: string;
telno: string;
zip: string;
addr: string;
daddr: string;
dtlInstCd: string;
laeSeCd: string;
/** 기관 구분 코드 — 통계 공통 조회 등 query `instSeCd`로 전달 */
instSeCd?: string;
ogdpSeCd: string;
aprvYn: string;
autzrId: string;
expryYmd: string;
lastPswdChgDt: string;
googleId: string;
kakaoId: string;
naverId: string;
isSocialUser: boolean;
hasPassword: boolean;
}
export type UserState = {
userInfo: IUserInfo | null;
userInfo: IUserInfo | null;
};
export type UserActions = {
setUserInfo: (userInfo: IUserInfo | null) => void;
setUserInfo: (userInfo: IUserInfo | null) => void;
};
export type UserStore = UserState & UserActions;
export const defaultInitState: UserState = {
userInfo: null,
userInfo: null,
};
export const createUserStore = (initState: UserState = defaultInitState) => {
return createStore<UserStore>()((set) => ({
...initState,
setUserInfo: (userInfo: IUserInfo | null) => set(() => ({ userInfo })),
}));
return createStore<UserStore>()((set) => ({
...initState,
setUserInfo: (userInfo: IUserInfo | null) => set(() => ({ userInfo })),
}));
};