Merge branch 'iipbs/st/dev'
Deploy Frontend / deploy (push) Successful in 2m48s

This commit is contained in:
2026-06-09 10:08:45 +09:00
35 changed files with 1340 additions and 343 deletions
+2 -1
View File
@@ -4,4 +4,5 @@ NEXT_PUBLIC_PROXY_HOST=http://localhost:3001
SERVER_API_URL=http://192.168.0.102:8000
NEXT_PUBLIC_SITE_ID=ST
NEXT_PUBLIC_SUB_DOMAIN=http://localhost:3000
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
NEXT_PUBLIC_ST_DASHBOARD_BBS_ID=13
+1 -1
View File
@@ -6,7 +6,7 @@
}
/* 셀 클래스가 있으면 위 기본색보다 우선 (같은 specificity + 나중 규칙) */
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly {
@apply !bg-[#f9f8f8] !text-[#666];
@apply !bg-[#f0f0f0] !text-[#666];
}
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error {
@apply !bg-[#ffe6e6] !text-[#c00];
+107 -3
View File
@@ -457,10 +457,29 @@ function sortRowsGroupPreserving(
}
/** columnInfo → RealGrid DataField 배열 (computed → valueCallback 포함, setFields용) */
function getDecimalPlacesFromDataFormat(dataFormat?: string): number | null {
if (!dataFormat) return null;
const match = dataFormat.match(/\.(0+|#+)/);
const decimalToken = match?.[1];
if (!decimalToken) return null;
return decimalToken.length;
}
function truncateByDecimalPlaces(value: number, decimalPlaces: number): number {
if (!Number.isFinite(value)) return 0;
if (decimalPlaces < 0) return value;
const factor = 10 ** decimalPlaces;
const scaled = value * factor;
// 절삭(truncate): 반올림 없이 소수점 이하를 버린다.
const truncated = scaled >= 0 ? Math.floor(scaled) : Math.ceil(scaled);
return truncated / factor;
}
function buildFieldsFromColumns(
columns: {
fieldName: string;
valueType?: ValueType;
dataFormat?: string;
datetimeFormat?: string;
editor?: { type?: string; datetimeFormat?: string };
computed?: import("../type/DataGrid.type").ComputedFieldConfig;
@@ -509,17 +528,23 @@ function buildFieldsFromColumns(
"subtrahendFields" in col.computed &&
col.computed.minuendFields.length > 0;
const hasSumOfMaxGroups =
col.computed.type === "sumOfMaxOfFieldAndSumGroups" &&
"groups" in col.computed &&
Array.isArray(col.computed.groups) &&
col.computed.groups.length > 0;
const hasPumpGrossPpqty =
col.computed.type === "pumpGrossPpqty" &&
"pumpGroups" in col.computed &&
col.computed.pumpGroups.length > 0;
if (
hasFields ||
hasNumeratorDenominator ||
hasMaxOfFieldAndSum ||
hasDifferenceOfSums ||
hasSumOfMaxGroups
hasSumOfMaxGroups ||
hasPumpGrossPpqty
) {
field.valueCallback = getComputedValueCallback(
const computedCallback = getComputedValueCallback(
col.computed,
) as (
prod: unknown,
@@ -528,6 +553,24 @@ function buildFieldsFromColumns(
fieldNames: string[],
values: unknown[],
) => number;
const decimalPlaces = getDecimalPlacesFromDataFormat(col.dataFormat);
field.valueCallback = (
prod: unknown,
dataRow: number,
fieldName: string,
fieldNames: string[],
values: unknown[],
) => {
const computedValue = computedCallback(
prod,
dataRow,
fieldName,
fieldNames,
values,
);
if (decimalPlaces == null) return computedValue;
return truncateByDecimalPlaces(computedValue, decimalPlaces);
};
}
}
return field;
@@ -1046,6 +1089,14 @@ const DataGrid = ({
}
columns.forEach((column) => {
const gv = gridView as GridViewWithSetColumnProperty;
const exists =
typeof (
gridView as { columnByName?: (name: string) => unknown }
).columnByName === "function" &&
(gridView as { columnByName: (name: string) => unknown }).columnByName(
column.name,
) != null;
if (!exists) return;
gv.setColumnProperty?.(
column.name,
"header",
@@ -1181,10 +1232,63 @@ const DataGrid = ({
const hasFooterUserSpans = checkFooterUserSpans(columnLayout);
if (hasFooterUserSpans) {
const validColumnNames = new Set(columns.map((c) => c.name));
const sanitizeColumnLayout = (
layout: unknown[],
): unknown[] => {
const result: unknown[] = [];
for (const item of layout) {
if (typeof item === "string") {
if (validColumnNames.has(item)) {
result.push(item);
}
continue;
}
if (!item || typeof item !== "object") continue;
if ("column" in item) {
const columnName = (item as { column?: unknown }).column;
if (
typeof columnName === "string" &&
validColumnNames.has(columnName)
) {
result.push(item);
}
continue;
}
if ("items" in item && Array.isArray((item as { items?: unknown[] }).items)) {
const nextItems = sanitizeColumnLayout(
(item as { items: unknown[] }).items,
);
if (nextItems.length > 0) {
result.push({
...(item as Record<string, unknown>),
items: nextItems,
});
}
continue;
}
result.push(item);
}
return result;
};
// setColumnLayout은 그리드가 완전히 렌더링된 후 호출해야 함
setTimeout(() => {
if (gridRef.current?.gridView && columnLayout) {
gridRef.current.gridView.setColumnLayout?.(columnLayout);
const safeLayout = sanitizeColumnLayout(columnLayout as unknown[]);
if (safeLayout.length === 0) return;
try {
(
gridRef.current.gridView.setColumnLayout as
| ((layout: unknown) => void)
| undefined
)?.(safeLayout);
} catch (e) {
console.warn("setColumnLayout skipped due to invalid columns:", e);
}
}
}, 100);
}
@@ -33,7 +33,7 @@ import DataGrid from "@/components/module/DataGrid";
import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination";
import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type";
/** 통계 입력 현황 > 행 클릭 시 예외처리(prcsYn=3) 내역만 조회 */
/** 통계 입력 현황 > 행 클릭 시 사용자예외처리(prcsYn=2) 내역만 조회 */
export interface ExceptionProcessingHistoryQueryContext {
sewerBzmnCd: string;
ctpvNm: string;
@@ -92,13 +92,15 @@ function exceptionPrcsYnNmCellStyle({ fieldName, row }: CellStyleCallbackParams)
const exceptionHistoryColumnInfo: ColumnInfo = {
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 },
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
{ name: "compVlCn", fieldName: "compVlCn", header: { text: "기준값" }, editable: false },
{ name: "prcsYnNm", fieldName: "prcsYnNm", header: { text: "처리여부" }, editable: false },
{ name: "pbcpAprvYn", fieldName: "pbcpAprvYn", header: { text: "공단승인여부" }, editable: false },
{ name: "stVrfRjtComNm", fieldName: "stVrfRjtComNm", header: { text: "반려상태" }, editable: false },
{ name: "prcsOpnnExpln", fieldName: "prcsOpnnExpln", header: { text: "처리의견" }, editable: false },
{ name: "rjctOpnnCn", fieldName: "rjctOpnnCn", header: { text: "반려의견" }, editable: false },
{ name: "regDt", fieldName: "regDt", header: { text: "등록일시" }, editable: false },
{ name: "mdfcnDt", fieldName: "mdfcnDt", header: { text: "갱신일시" }, editable: false },
{
@@ -114,13 +116,15 @@ const exceptionHistoryColumnInfo: ColumnInfo = {
],
columnLayout: [
"ctpvNm",
"sggNm",
"admdstNm",
"vrfcTrgtNm",
"rsnExpln",
"nowVlCn",
"compVlCn",
"prcsYnNm",
"pbcpAprvYn",
"stVrfRjtComNm",
"prcsOpnnExpln",
"rjctOpnnCn",
"regDt",
"mdfcnDt",
"individualReject",
@@ -190,9 +194,9 @@ export function ExceptionProcessingHistoryDialog({
sggNm: queryContext.sggNm,
statsYr: queryContext.statsYr,
statsCd: queryContext.statsCd,
pbcpAprvYn: "N",
vrfcMthdCd,
prcsYnList: PRCS_YN_LIST,
rejectCodeList: "0,1,2",
page: pageIndex + 1,
size: 30,
};
@@ -308,14 +312,16 @@ export function ExceptionProcessingHistoryDialog({
const mapRows = (entries: VerificationResultReportItem[]): ExceptionHistoryGridRow[] =>
entries.map((item) => ({
ctpvNm: item.ctpvNm,
sggNm: item.sggNm,
admdstNm: item.admdstNm ?? item.sggNm ?? "-",
vrfcTrgtNm: item.vrfcTrgtNm,
rsnExpln: item.rsnExpln,
nowVlCn: item.nowVlCn ?? "-",
compVlCn: item.compVlCn ?? "-",
prcsYnNm: item.prcsYnNm ?? "-",
stVrfRjtComNm: item.stVrfRjtComNm ?? "-",
prcsOpnnExpln: item.prcsOpnnExpln ?? "-",
rjctOpnnCn: item.rjctOpnnCn ?? "-",
prcsYn: item.prcsYn ?? "",
pbcpAprvYn: item.pbcpAprvYn,
regDt: (item.regDt ?? "").slice(0, 16),
mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16),
individualReject: "개별 반려",
+1 -1
View File
@@ -41,7 +41,7 @@ const getSggListFetcher = async (
key: [string, string, string, string],
): Promise<ApiResponse<Sgg[]>> => {
const gugunSkipYn = doList.includes(key[3]) ? "N" : "Y";
return statisticsApi.getSggAllList(key[1], {
return statisticsApi.getSggList(key[1], {
instSeCd: key[2],
gugunSkipYn,
});
@@ -92,13 +92,12 @@ const verificationSubmitResultColumnInfo: ColumnInfo = {
{ name: "vrfcMthdNm", fieldName: "vrfcMthdNm", header: { text: "구분" }, editable: false },
{ name: "vrfcMthdCd", fieldName: "vrfcMthdCd", header: { text: "검증유형코드" }, editable: false, visible: false },
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역" }, editable: false },
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
{ name: "compVlCn", fieldName: "compVlCn", header: { text: "기준값" }, editable: false },
{ name: "prcsYn", fieldName: "prcsYn", header: { text: "처리여부" }, editable: false },
{ name: "pbcpAprvYn", fieldName: "pbcpAprvYn", header: { text: "공단승인여부" }, editable: false },
{ name: "regDt", fieldName: "regDt", header: { text: "등록일시" }, editable: false },
{ name: "mdfcnDt", fieldName: "mdfcnDt", header: { text: "갱신일시" }, editable: false },
{
@@ -111,13 +110,12 @@ const verificationSubmitResultColumnInfo: ColumnInfo = {
columnLayout: [
"vrfcMthdNm",
"ctpvNm",
"sggNm",
"admdstNm",
"vrfcTrgtNm",
"rsnExpln",
"nowVlCn",
"compVlCn",
"prcsYn",
"pbcpAprvYn",
"regDt",
"mdfcnDt",
"prcsOpnnExpln",
@@ -202,13 +200,12 @@ export function VerificationResultDialog({
vrfcMthdCd,
vrfcMthdNm: VRFC_MTHD_NAME_MAP[vrfcMthdCd] ?? item.vrfcMthdNm,
ctpvNm: item.ctpvNm,
sggNm: item.sggNm,
admdstNm: item.admdstNm ?? item.sggNm ?? "-",
vrfcTrgtNm: item.vrfcTrgtNm,
rsnExpln: item.rsnExpln,
nowVlCn: item.nowVlCn ?? "-",
compVlCn: item.compVlCn ?? "-",
prcsYn: PRCS_YN_LABEL[item.prcsYn] ?? item.prcsYn,
pbcpAprvYn: item.pbcpAprvYn,
prcsYn: item.prcsYnNm ?? PRCS_YN_LABEL[item.prcsYn] ?? item.prcsYn,
regDt: (item.regDt ?? "").slice(0, 16),
mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16),
prcsOpnnExpln: item.prcsOpnnExpln ?? "",
@@ -226,9 +223,9 @@ export function VerificationResultDialog({
sggNm: stableQueryContext.sggNm,
statsYr: stableQueryContext.statsYr,
statsCd: stableQueryContext.statsCd,
pbcpAprvYn: "N",
vrfcMthdCd,
prcsYnList: "0",
rejectCodeList: "0,1,2",
page: page + 1,
size: 10000,
};
@@ -137,12 +137,15 @@ function verificationPrcsYnNmCellStyle({ fieldName, row }: CellStyleCallbackPara
const verificationResultColumnInfo: ColumnInfo = {
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 },
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
{ name: "compVlCn", fieldName: "compVlCn", header: { text: "기준값" }, editable: false },
{ name: "prcsYnNm", fieldName: "prcsYnNm", header: { text: "처리여부" }, editable: false },
{ name: "stVrfRjtComNm", fieldName: "stVrfRjtComNm", header: { text: "반려상태" }, editable: false },
{ name: "prcsOpnnExpln", fieldName: "prcsOpnnExpln", header: { text: "처리의견" }, editable: false },
{ name: "rjctOpnnCn", fieldName: "rjctOpnnCn", header: { text: "반려의견" }, editable: false },
{ name: "regDt", fieldName: "regDt", header: { text: "등록일시" }, editable: false },
{ name: "mdfcnDt", fieldName: "mdfcnDt", header: { text: "갱신일시" }, editable: false },
{ name: "prcsYn", fieldName: "prcsYn", header: { text: "검증결과상태코드" }, editable: false, visible: false },
@@ -150,12 +153,15 @@ const verificationResultColumnInfo: ColumnInfo = {
],
columnLayout: [
"ctpvNm",
"sggNm",
"admdstNm",
"vrfcTrgtNm",
"rsnExpln",
"nowVlCn",
"compVlCn",
"prcsYnNm",
"stVrfRjtComNm",
"prcsOpnnExpln",
"rjctOpnnCn",
"regDt",
"mdfcnDt",
"prcsYn",
@@ -245,9 +251,9 @@ export function VerificationResultHistoryDialog({
sggNm: stableQueryContext.sggNm,
statsYr: stableQueryContext.statsYr,
statsCd: stableQueryContext.statsCd,
pbcpAprvYn: "N",
vrfcMthdCd,
prcsYnList: "0,2,3",
prcsYnList: "0,2",
rejectCodeList: "0,1,2",
page: pageIndex + 1,
size: PAGE_SIZE,
};
@@ -376,14 +382,16 @@ export function VerificationResultHistoryDialog({
vrfcSn: item.vrfcSn,
__checked: isChecked ? 1 : 0,
ctpvNm: item.ctpvNm,
sggNm: item.sggNm,
admdstNm: item.admdstNm ?? item.sggNm ?? "-",
vrfcTrgtNm: item.vrfcTrgtNm,
rsnExpln: item.rsnExpln,
nowVlCn: item.nowVlCn ?? "-",
compVlCn: item.compVlCn ?? "-",
prcsYnNm: item.prcsYnNm ?? "-",
stVrfRjtComNm: item.stVrfRjtComNm ?? "-",
prcsOpnnExpln: item.prcsOpnnExpln ?? "-",
rjctOpnnCn: item.rjctOpnnCn ?? "-",
prcsYn: item.prcsYn ?? "",
pbcpAprvYn: item.pbcpAprvYn,
regDt: (item.regDt ?? "").slice(0, 16),
mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16),
};
@@ -34,8 +34,8 @@ export async function hasAnyPostSubmitVerificationResult(
sggNm: ctx.sggNm,
statsYr: ctx.statsYr,
statsCd: ctx.statsCd,
pbcpAprvYn: "N" as const,
prcsYnList: "0",
rejectCodeList: "0,1,2",
page: 1,
size: 1,
};
@@ -1,6 +1,6 @@
"use client";
import { useCallback, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import {
type InfiniteData,
keepPreviousData,
@@ -9,12 +9,12 @@ import {
} from "@tanstack/react-query";
import { useTabStore as useGridTabStore } from "@rsone/components";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { verificationInputReportApi } from "@/entities/statistics/api/verification-input-report-service";
import type {
VerificationResultListRequest,
VerificationResultListResponse,
VerificationResultReportItem,
} from "@/entities/statistics/model/statistics-interface";
VerificationInputReportItem,
VerificationInputReportResponse,
VerificationInputReportTypeCountingInfo,
} from "@/entities/statistics/model/verification-input-report-interface";
import { useAlertStore, useMenuStore } from "@/shared/lib";
import {
findMenuItemByUrlPath,
@@ -59,8 +59,8 @@ const TYPE_CODE_NAME_MAP: Record<string, string> = {
const PAGE_SIZE = 100;
const SCROLL_LOAD_THRESHOLD_PX = 120;
function getNextVerificationResultPage(
lastPage: VerificationResultListResponse,
function getNextVerificationInputReportPage(
lastPage: VerificationInputReportResponse,
): number | undefined {
const pageInfo = lastPage?.data?.pageInfo;
if (!pageInfo) return undefined;
@@ -69,19 +69,29 @@ function getNextVerificationResultPage(
return undefined;
}
function flattenVerificationResults(
data: InfiniteData<VerificationResultListResponse> | undefined,
): VerificationResultReportItem[] {
function flattenVerificationInputReportResults(
data: InfiniteData<VerificationInputReportResponse> | undefined,
): VerificationInputReportItem[] {
return (
data?.pages.flatMap((page) => page?.data?.verifyResultList ?? []) ?? []
);
}
function getVerificationResultTotalCount(
data: InfiniteData<VerificationResultListResponse> | undefined,
loadedCount: number,
): number {
return data?.pages[0]?.data?.pageInfo?.totalElements ?? loadedCount;
function getVerifyTypeCountingInfo(
data: InfiniteData<VerificationInputReportResponse> | undefined,
): VerificationInputReportTypeCountingInfo | null {
return data?.pages[0]?.data?.verifyTypeCountingInfo ?? null;
}
function formatReportItemTitle(item: VerificationInputReportItem): string {
return item.statsNm?.trim() || item.vrfcTrgtNm?.trim() || item.statsCd;
}
function formatReportItemRegion(item: VerificationInputReportItem): string {
const parts = [item.ctpvNm, item.sggNm].filter(
(v): v is string => Boolean(v?.trim()),
);
return parts.length > 0 ? parts.join(" ") : "-";
}
function useVerificationReportList(
@@ -89,28 +99,31 @@ function useVerificationReportList(
statsCdQueryKey: string,
vrfcMthdCd: VerificationReportTabKey,
statsCd: string,
enabled: boolean,
) {
return useInfiniteQuery({
queryKey: [
"verificationResultList",
"verificationInputReport",
statsYr,
vrfcMthdCd,
statsCdQueryKey,
],
initialPageParam: 1,
queryFn: ({ pageParam }) =>
statisticsApi.getVerificationResultList({
verificationInputReportApi.getVerificationInputReport({
statsYr,
pbcpAprvYn: "N",
vrfcMthdCd,
page: pageParam,
size: PAGE_SIZE,
prcsYnList: "0",
vrfcMthdCd,
...(!(statsCd === "" || statsCd === "ALL") && { statsCd }),
} satisfies VerificationResultListRequest),
getNextPageParam: getNextVerificationResultPage,
}),
getNextPageParam: getNextVerificationInputReportPage,
placeholderData: keepPreviousData,
refetchOnWindowFocus: false,
// 탭 전환만으로는 재조회하지 않음(명시적 새로고침/조건 변경 시만 조회)
staleTime: Infinity,
refetchOnMount: false,
enabled,
});
}
@@ -132,28 +145,46 @@ const VerificationSelectionReport = ({
const alert = useAlertStore((s) => s.setMessage);
const [activeTab, setActiveTab] = useState<VerificationReportTabKey>("NV");
const [statsCd, setStatsCd] = useState<string>("ALL");
const [isFullReloading, setIsFullReloading] = useState(false);
const statsYr = String(dayjs().year() - 1);
const statsCdQueryKey =
statsCd === "" || statsCd === "ALL" ? "" : statsCd;
const handleStatsCdChange = useCallback(
(nextStatsCd: string) => {
setStatsCd(nextStatsCd);
setActiveTab("NV");
setIsFullReloading(true);
// 통계항목 변경 시 모든 탭 캐시 제거 후 NV부터 다시 조회
void queryClient.removeQueries({
queryKey: ["verificationInputReport", statsYr],
});
},
[queryClient, statsYr],
);
const nvQuery = useVerificationReportList(
statsYr,
statsCdQueryKey,
"NV",
statsCd,
activeTab === "NV",
);
const imrQuery = useVerificationReportList(
statsYr,
statsCdQueryKey,
"IMR",
statsCd,
activeTab === "IMR",
);
const hbQuery = useVerificationReportList(
statsYr,
statsCdQueryKey,
"HB",
statsCd,
activeTab === "HB",
);
const queryByTab = {
@@ -162,44 +193,51 @@ const VerificationSelectionReport = ({
HB: hbQuery,
} as const;
const isInitialLoading =
nvQuery.isPending || imrQuery.isPending || hbQuery.isPending;
const isRefreshing =
(nvQuery.isFetching && !nvQuery.isPending) ||
(imrQuery.isFetching && !imrQuery.isPending) ||
(hbQuery.isFetching && !hbQuery.isPending);
const error = nvQuery.error ?? imrQuery.error ?? hbQuery.error;
const activeQuery = queryByTab[activeTab];
const isRefreshing = activeQuery.isFetching && !activeQuery.isPending;
const error = activeQuery.error;
/** 통계항목 변경·최초 진입: 탭 영역 숨기고 카드 전체 로딩만 표시 */
const hideTabsSection =
isFullReloading ||
(activeTab === "NV" && !nvQuery.data && (nvQuery.isPending || nvQuery.isFetching));
useEffect(() => {
if (!isFullReloading) return;
if (nvQuery.isFetching) return;
if (nvQuery.isSuccess || nvQuery.isError) {
setIsFullReloading(false);
}
}, [isFullReloading, nvQuery.isFetching, nvQuery.isSuccess, nvQuery.isError]);
const handleRefresh = useCallback(() => {
const tabCodes: VerificationReportTabKey[] = ["NV", "IMR", "HB"];
void Promise.all(
tabCodes.map((code) =>
queryClient.resetQueries({
queryKey: [
"verificationResultList",
statsYr,
code,
statsCdQueryKey,
],
}),
),
);
}, [queryClient, statsYr, statsCdQueryKey]);
setStatsCd("ALL");
setActiveTab("NV");
setIsFullReloading(true);
void queryClient.removeQueries({
queryKey: ["verificationInputReport", statsYr],
});
}, [queryClient, statsYr]);
const listsByTab: Record<VerificationReportTabKey, VerificationResultReportItem[]> = {
NV: flattenVerificationResults(nvQuery.data),
IMR: flattenVerificationResults(imrQuery.data),
HB: flattenVerificationResults(hbQuery.data),
const listsByTab: Record<VerificationReportTabKey, VerificationInputReportItem[]> = {
NV: flattenVerificationInputReportResults(nvQuery.data),
IMR: flattenVerificationInputReportResults(imrQuery.data),
HB: flattenVerificationInputReportResults(hbQuery.data),
};
const typeCountingInfo =
getVerifyTypeCountingInfo(nvQuery.data) ??
getVerifyTypeCountingInfo(imrQuery.data) ??
getVerifyTypeCountingInfo(hbQuery.data);
const totalCountByTab: Record<VerificationReportTabKey, number> = {
NV: getVerificationResultTotalCount(nvQuery.data, listsByTab.NV.length),
IMR: getVerificationResultTotalCount(imrQuery.data, listsByTab.IMR.length),
HB: getVerificationResultTotalCount(hbQuery.data, listsByTab.HB.length),
NV: typeCountingInfo?.nvCount ?? listsByTab.NV.length,
IMR: typeCountingInfo?.imrCount ?? listsByTab.IMR.length,
HB: typeCountingInfo?.hbCount ?? listsByTab.HB.length,
};
const openInputFromStatsCd = useCallback(
(item: VerificationResultReportItem) => {
(item: VerificationInputReportItem) => {
const nav = resolveInputPageNavigationFromStatsCd(item.statsCd);
if (!nav) {
alert(
@@ -263,9 +301,21 @@ const VerificationSelectionReport = ({
[],
);
const isTabContentLoading = (tab: VerificationReportTabKey) => {
const query = queryByTab[tab];
return (
activeTab === tab &&
!hideTabsSection &&
(query.isPending ||
(query.isFetching && !query.isFetchingNextPage) ||
(!query.data && !query.error))
);
};
const renderResultList = (
typeCode: VerificationReportTabKey,
items: VerificationResultReportItem[],
items: VerificationInputReportItem[],
isTabLoading: boolean,
hasNextPage: boolean,
isFetchingNextPage: boolean,
fetchNextPage: () => void,
@@ -281,7 +331,13 @@ const VerificationSelectionReport = ({
)
}
>
{items.length === 0 && !isFetchingNextPage ? (
{isTabLoading ? (
<div className="flex h-full min-h-[8rem] items-center justify-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
) : items.length === 0 && !isFetchingNextPage ? (
<div className="flex h-full min-h-[8rem] items-center justify-center text-lg text-muted-foreground">
항목이 없습니다.
</div>
@@ -303,7 +359,7 @@ const VerificationSelectionReport = ({
>
<div className="mb-2 flex items-center justify-between">
<span className="font-medium">
{item.statsNm}
{formatReportItemTitle(item)}
</span>
<Badge
color={
@@ -320,7 +376,7 @@ const VerificationSelectionReport = ({
{item.rsnExpln}
</p>
<div className="flex justify-between text-lg text-muted-foreground">
<span>{item.admdstNm}</span>
<span>{formatReportItemRegion(item)}</span>
<span>
현재: {item.nowVlCn ?? "-"} / 비교:{" "}
{item.compVlCn ?? "-"}
@@ -383,25 +439,25 @@ const VerificationSelectionReport = ({
<CodeSelectWithAll
upCd="STITEM"
value={statsCd}
onValueChange={setStatsCd}
onValueChange={handleStatsCdChange}
placeholder="전체"
className="w-full"
/>
</div>
<div className="card-body p-2.5 flex-1 flex flex-col overflow-hidden relative min-h-0">
{isInitialLoading && (
{hideTabsSection && (
<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>
)}
{error && (
{!hideTabsSection && error && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
데이터를 불러올 수 없습니다.
</div>
)}
{!isInitialLoading && !error && (
{!hideTabsSection && !error && (
<section className="flex min-h-0 flex-1 flex-col">
<Tabs
value={activeTab}
@@ -428,18 +484,14 @@ const VerificationSelectionReport = ({
value={tab.value}
className="mt-0 flex min-h-0 flex-1 flex-col p-0 data-[state=inactive]:hidden"
>
{activeTab === tab.value
? renderResultList(
tab.value,
listsByTab[tab.value],
queryByTab[tab.value]
.hasNextPage ?? false,
queryByTab[tab.value]
.isFetchingNextPage,
queryByTab[tab.value]
.fetchNextPage,
)
: null}
{renderResultList(
tab.value,
listsByTab[tab.value],
isTabContentLoading(tab.value),
queryByTab[tab.value].hasNextPage ?? false,
queryByTab[tab.value].isFetchingNextPage,
queryByTab[tab.value].fetchNextPage,
)}
</TabsContent>
))}
</Tabs>
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react';
import { useEffect, useRef, useState, type Dispatch, type SetStateAction } from 'react';
import { useConfirm } from '@frontend-opensource/use-react-hooks';
@@ -157,7 +157,9 @@ const PublicSewageFacilityInp = () => {
const globalProgressVisible = useGlobalProgressStore((state) => state.visible);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [hasSearched, setHasSearched] = useState(false);
const [isNotApplicable, setIsNotApplicable] = useState(false);
const [isNotApplicableByTab, setIsNotApplicableByTab] = useState<Map<string, boolean>>(
() => new Map(),
);
const [showIsNotApplicable, setShowIsNotApplicable] = useState(false);
const [searchValues, setSearchValues] = useState<SearchFormValues | null>(null);
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
@@ -175,6 +177,19 @@ const PublicSewageFacilityInp = () => {
const [mainFacilityMappingSaveDialogKey, setMainFacilityMappingSaveDialogKey] = useState(0);
const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false);
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
const isNotApplicable = isNotApplicableByTab.get('ST00060001') ?? false;
const setIsNotApplicable: Dispatch<SetStateAction<boolean>> = (next) => {
setIsNotApplicableByTab((prev) => {
const current = prev.get('ST00060001') ?? false;
const resolved =
typeof next === 'function'
? (next as (prevState: boolean) => boolean)(current)
: next;
const nextMap = new Map(prev);
nextMap.set('ST00060001', resolved);
return nextMap;
});
};
const periodEnterableRef = useRef(true);
const searchDoneRef = useRef(false);
@@ -655,8 +670,6 @@ const PublicSewageFacilityInp = () => {
param: PublicSewageFacilityRequestParams,
searchContext: SearchFormValues | null,
): Promise<boolean> => {
// 서버 데이터로 새로 불러오는 시점이므로 "해당없음" 체크는 항상 초기화
setIsNotApplicable(false);
try {
if (tabKey === 'ST00060001') {
const response = await statisticsInputPublicSewageFacilityApiService.getPublicSewageFacility(param);
@@ -856,6 +869,11 @@ const PublicSewageFacilityInp = () => {
setLastSearchedValues(values);
searchDoneRef.current = true;
setHasSearched(true);
setIsNotApplicableByTab((prev) => {
const next = new Map(prev);
next.set('ST00060001', false);
return next;
});
setIsLoading(true);
const param = buildPublicSewageFacilityParams(values);
@@ -870,6 +888,19 @@ const PublicSewageFacilityInp = () => {
};
const handleActiveTabChange = async (tabKey: string) => {
const prevTabKey = activeTabRef.current;
if (prevTabKey === 'ST00060001') {
const latestRowsInPrevTab =
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
if (latestRowsInPrevTab.length > 0 || rowDataMap.has('ST00060001')) {
setRowDataMap((prev) => {
const next = new Map(prev);
next.set('ST00060001', latestRowsInPrevTab);
return next;
});
}
}
activeTabRef.current = tabKey;
setActiveTab(tabKey);
@@ -885,7 +916,7 @@ const PublicSewageFacilityInp = () => {
const handleReset = () => {
setIsLoading(false);
setHasSearched(false);
setIsNotApplicable(false);
setIsNotApplicableByTab(new Map());
setShowIsNotApplicable(false);
setRowDataMap(new Map());
@@ -1215,7 +1246,7 @@ const PublicSewageFacilityInp = () => {
return (
<TabsContent key={tab.tabKey} value={tab.tabKey}>
<DataGrid
key={(tabEnterableForGrid || isAdminInfo) ? 'enterable' : 'readonly'}
key={tab.tabKey}
columnInfo={gridColumnInfo}
rowStartNum={tab.rowStartNum}
excelExportFileSuffix={tab.tabLabel || tab.tabKey}
@@ -5,7 +5,6 @@ import DataGrid from "@/components/module/DataGrid";
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 AddArticleButton from "@/components/button/AddArticleButton";
import DeleteArticleButton from "@/components/button/DeleteArticleButton";
import NotApplicableCheckButton from "@/components/button/NotApplicableCheckButton";
@@ -15,11 +14,6 @@ import { PumpingStationInpColumn } from "@/components/util/column/input/PumpingS
// 하수도 통계 관리 > 05. 펌프장
const PumpingStationInp = () => {
const SubmitExceptionFunc = () => {
console.log('submit exception');
// 예외 제출 로직
};
const getLastYearDataFunc = () => {
console.log('get last year data');
// 지난연도 데이터 가져오기 로직
@@ -53,7 +47,6 @@ const PumpingStationInp = () => {
<SearchForm fctName={true} />
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<SubmitExceptionButton SubmitExceptionFunc={SubmitExceptionFunc} />
<LastYearDataButton getLastYearDataFunc={getLastYearDataFunc} />
<SubmitButton submitFunc={submitFunc} />
<SaveButton saveFunc={saveFunc} />
@@ -67,7 +60,7 @@ const PumpingStationInp = () => {
</div>
</div>
<DataGrid
columnInfo={ PumpingStationInpColumn }
columnInfo={PumpingStationInpColumn}
rowStartNum={3}
fixedColumns={3}
rowData={rowData}
@@ -396,6 +396,8 @@ const SewagePipeInp = () => {
values: SearchFormValues;
tabKey: string;
} | null>(null);
/** ST00020002 무데이터 안내/자동이동은 동일 조회조건에서 1회만 수행 */
const factInstallNoDataHandledKeyRef = useRef<string | null>(null);
const [verificationSearchDefaults, setVerificationSearchDefaults] =
useState<SearchFormValues | null>(null);
@@ -549,6 +551,12 @@ const SewagePipeInp = () => {
mergeIntoExistingMap: boolean,
updateLastSearchedValues: boolean,
): Promise<boolean> => {
const searchConditionKey = JSON.stringify({
statsYr: searchParam.statsYr ?? "",
ctpvNm: searchParam.ctpvNm ?? "",
sggNm: searchParam.sggNm ?? "",
sewerBzmnCd: searchParam.sewerBzmnCd ?? "",
});
const applyRowMap = (apply: (m: Map<string, Record<string, string | number>[]>) => void) => {
if (mergeIntoExistingMap) {
setRowDataMap((prev) => {
@@ -595,6 +603,23 @@ const SewagePipeInp = () => {
ctpvNm: commonInfo.ctpvNm,
...d,
}));
if (convertedData.length === 0) {
const alreadyHandledForSameSearch =
factInstallNoDataHandledKeyRef.current === searchConditionKey;
if (!alreadyHandledForSameSearch) {
factInstallNoDataHandledKeyRef.current = searchConditionKey;
alert("시설별 설치 현황에 조회된 데이터가 없습니다.\n시설별 현황 관리에서 설정해주세요");
activeTabRef.current = "ST00020006";
setActiveTab("ST00020006");
return await loadSewagePipeActiveTabData(
"ST00020006",
searchParam,
values,
true,
updateLastSearchedValues,
);
}
}
applyRowMap((m) => m.set('ST00020002', toGridRows(convertedData)));
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
@@ -795,7 +820,39 @@ const SewagePipeInp = () => {
}, [searchFormRemountKey]);
const getCurrentRowsForTab = (tabKey: string): Record<string, string | number>[] => {
switch (tabKey) {
case REGIONAL_FACT_TAB_KEY:
return getGridDataRef.current?.() ?? [];
case "ST00020002":
return getFactInstallGridDataRef.current?.() ?? [];
case "ST00020003":
return getPipeTypeGridDataRef.current?.() ?? [];
case "ST00020004":
return getPipeDiameterGridDataRef.current?.() ?? [];
case "ST00020005":
return getManholeGridDataRef.current?.() ?? [];
case "ST00020006":
return [
...(rowDataMap.get("treatmentPlantConnectionInp") ?? []),
...(rowDataMap.get("adminAreaConnectionInp") ?? []),
];
default:
return rowDataMap.get(tabKey) ?? [];
}
};
const handleActiveTabChange = async (tabKey: string) => {
const prevTabKey = activeTabRef.current;
const latestRowsInPrevTab = getCurrentRowsForTab(prevTabKey);
if (latestRowsInPrevTab.length > 0 || rowDataMap.has(prevTabKey)) {
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(prevTabKey, latestRowsInPrevTab);
return next;
});
}
activeTabRef.current = tabKey;
setActiveTab(tabKey);
setStatsCd(tabStatsCdMap.get(tabKey) ?? "");
@@ -1735,7 +1792,7 @@ const SewagePipeInp = () => {
const maxYy = getManholeLastDataYear() % 100;
const saveList = currentData.map((d) => {
const yearlyStats = [];
const yearlyStats: SeweragePipeInputManholeStatusSaveYearlyStatItem[] = [];
for (let yy = minYy; yy <= maxYy; yy++) {
const year = 2000 + yy;
const wholKey = `yy${yy}WholMnhlCnt`;
@@ -1978,7 +2035,7 @@ const SewagePipeInp = () => {
return (
<TabsContent key={tabKey} value={tabKey}>
<DataGrid
key={(tabEnterableForGrid || isAdminInfo) ? "enterable" : "readonly"}
key={tabKey}
columnInfo={gridColumnInfo}
rowStartNum={info.rowStartNum}
excelExportFileSuffix={info.tabLabel || tabKey}
@@ -912,6 +912,16 @@ const SewagePipeMaintainInp = () => {
}, [searchFormRemountKey]);
const handleActiveTabChange = async (tabKey: string) => {
const prevTabKey = activeTabRef.current;
const latestRowsInPrevTab = getTabGridRows(prevTabKey);
if (latestRowsInPrevTab.length > 0 || rowDataMap.has(prevTabKey)) {
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(prevTabKey, latestRowsInPrevTab);
return next;
});
}
activeTabRef.current = tabKey;
setActiveTab(tabKey);
if (!hasSearched || !lastSearchedValues || !searchParamRef.current) return;
@@ -1110,11 +1120,7 @@ const SewagePipeMaintainInp = () => {
return (
<TabsContent key={tabKey} value={tabKey}>
<DataGrid
key={
tabEnterableForGrid || isAdminInfo
? `${tabKey}-enterable`
: `${tabKey}-readonly`
}
key={tabKey}
columnInfo={
columnInfoByTab.get(tabKey) ??
toGridColumnInfo(info)
@@ -4,10 +4,14 @@ import {
useRef,
useState,
type RefObject,
type Dispatch,
type SetStateAction,
} from "react";
import { flushSync } from "react-dom";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { ValueType } from "realgrid";
import type { RealGridReact } from "realgrid-react";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
@@ -25,7 +29,6 @@ import SearchForm from "@/components/module/SearchForm";
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import type {
CellEditedCallbackParams,
CellStyleCallbackParams,
CellTooltipCallbackParams,
ColumnConfig,
@@ -36,24 +39,33 @@ import { doList } from "@/components/util/column/input/DoList";
import { RainfallPumpStationInpColumn } from "@/components/util/column/input/sewageReservoirPump/RainfallPumpStationInpColumn";
import { SewageReservoirInpColumn } from "@/components/util/column/input/sewageReservoirPump/SewageReservoirInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { formatCompactYmdForGrid, normalizeOxYnValue } from "@/components/util/grid/ymdFormat";
import {
formatCompactYmdForGrid,
normalizeOxYnValue,
toCompactYmd,
} from "@/components/util/grid/ymdFormat";
import { statisticsInputSewageReservoirPumpApi } from "@/entities/statistics/api/input/statistics-input-sewage-reservoir-pump-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import {
SEWAGE_RESERVOIR_STATS_CD,
type SewageReservoirItem,
type SewageReservoirRequestParams,
type SewageReservoirSaveItem,
type RainfallPumpStationItem,
type SewageReservoirVerificationResult,
} from "@/entities/statistics/model/input/statistics-input-sewage-reservoir-pump-interface";
import type {
ApiResponse,
cancelSubmitRequest,
Sgg,
SewerageStatusInfo,
} from "@/entities/statistics/model/statistics-interface";
import { useAlertStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { useAdminStore } from "@/store/admin-store";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
const SewageReservoirPumpInpTabList = [
SewageReservoirInpColumn,
RainfallPumpStationInpColumn,
@@ -144,7 +156,7 @@ function buildSewageReservoirSearchParam(
};
}
/** 항목 추가 시 빈 행 (시도는 조회 시 선택한 시도명) */
/** 항목 추가 시 빈 행 (`ctpvNm` = 조회 시도명) */
function createEmptySewageReservoirRow(
ctpvNm: string,
sggNm = "",
@@ -248,6 +260,108 @@ function mapSewageReservoirListToGridRows(
});
}
function nullableTrimmedString(
value: string | number | null | undefined,
): string | null {
const s = String(value ?? "").trim();
return s === "" ? null : s;
}
function toGridNumber(value: string | number | null | undefined): number {
const n = Number(value);
return Number.isFinite(n) ? n : 0;
}
/** API 조회 항목 → 저장/삭제 API 형식 (조회 직후 스냅샷용) */
function buildSewageReservoirSaveItemFromApiItem(
item: SewageReservoirItem,
): SewageReservoirSaveItem {
return buildSewageReservoirSaveItem(
item as unknown as Record<string, string | number>,
);
}
function buildSewageReservoirDeleteList(
loadedSnapshot: SewageReservoirSaveItem[],
currentRows: Record<string, string | number>[],
): SewageReservoirSaveItem[] {
const currentFcltSnSet = new Set(
currentRows
.map((row) => String(row.fcltSn ?? "").trim())
.filter((sn) => sn !== ""),
);
return loadedSnapshot.filter((item) => {
const sn = String(item.fcltSn ?? "").trim();
return sn !== "" && !currentFcltSnSet.has(sn);
});
}
/** 시군구명(`sggNm`) 기준 행정구역 코드·명 */
function resolveSewageReservoirAdmdst(
sggNm: string,
sggOptions: Sgg[],
): { admdstCd: string; admdstNm: string } {
const trimmed = sggNm.trim();
const matched = sggOptions.find((s) => s.gugun === trimmed);
if (matched) {
return { admdstCd: matched.adstDstrCd, admdstNm: matched.gugun };
}
return { admdstCd: "", admdstNm: trimmed };
}
/** 그리드 행 → ST00040001 저장/삭제 API 항목 */
function buildSewageReservoirSaveItem(
row: Record<string, string | number>,
sggOptions: Sgg[] = [],
): SewageReservoirSaveItem {
const { admdstCd, admdstNm } = resolveSewageReservoirAdmdst(
String(row.sggNm ?? ""),
sggOptions,
);
return {
admdstCd,
admdstNm,
fcltSn: String(row.fcltSn ?? ""),
fcltNm: String(row.fcltNm ?? ""),
fcltPstnCn: String(row.fcltPstnCn ?? ""),
oprtngStrtYmd: toCompactYmd(row.oprtngStrtYmd),
planDesignFreqCnt: toGridNumber(row.planDesignFreqCnt),
fldngPrvtnYn: normalizeOxYnValue(row.fldngPrvtnYn),
wqPlltnMtgtYn: normalizeOxYnValue(row.wqPlltnMtgtYn),
rtlztnUsgYn: normalizeOxYnValue(row.rtlztnUsgYn),
fcltStrctCn: nullableTrimmedString(row.fcltStrctCn),
wtrStrgZoneArea: toGridNumber(row.wtrStrgZoneArea),
impdFcltCpct: toGridNumber(row.impdFcltCpct),
grssPpqty: toGridNumber(row.grssPpqty),
pumpFcltN1DmtrExpln: String(row.pumpFcltN1DmtrExpln ?? ""),
pumpFcltN1PerPpqty: toGridNumber(row.pumpFcltN1PerPpqty),
pumpFcltN1PumpCnt: toGridNumber(row.pumpFcltN1PumpCnt),
pumpFcltN1RsrvPumpCnt: toGridNumber(row.pumpFcltN1RsrvPumpCnt),
pumpFcltN2DmtrExpln: String(row.pumpFcltN2DmtrExpln ?? ""),
pumpFcltN2PerPpqty: toGridNumber(row.pumpFcltN2PerPpqty),
pumpFcltN2PumpCnt: toGridNumber(row.pumpFcltN2PumpCnt),
pumpFcltN2RsrvPumpCnt: toGridNumber(row.pumpFcltN2RsrvPumpCnt),
pumpFcltN3DmtrExpln: String(row.pumpFcltN3DmtrExpln ?? ""),
pumpFcltN3PerPpqty: toGridNumber(row.pumpFcltN3PerPpqty),
pumpFcltN3PumpCnt: toGridNumber(row.pumpFcltN3PumpCnt),
pumpFcltN3RsrvPumpCnt: toGridNumber(row.pumpFcltN3RsrvPumpCnt),
prcsgFcltYn: String(row.prcsgFcltYn ?? ""),
ddrztnEqpmYn: String(row.ddrztnEqpmYn ?? ""),
oslfEtgtYn: String(row.oslfEtgtYn ?? ""),
mltvMthCn: nullableTrimmedString(row.mltvMthCn),
impdMthCn: nullableTrimmedString(row.impdMthCn),
impdPrcsMthdCn: nullableTrimmedString(row.impdPrcsMthdCn),
dschgWtrsExpln: String(row.dschgWtrsExpln ?? ""),
prcptnLastDspsQnt: toGridNumber(row.prcptnLastDspsQnt),
ctmnLastDspsQnt: toGridNumber(row.ctmnLastDspsQnt),
prcptnLastDspsMthdExpln: String(row.prcptnLastDspsMthdExpln ?? ""),
ctmnLastDspsMthdExpln: String(row.ctmnLastDspsMthdExpln ?? ""),
oprtngDayCnt: toGridNumber(row.oprtngDayCnt),
grssMltvQnt: toGridNumber(row.grssMltvQnt),
rmrk: nullableTrimmedString(row.rmrk),
};
}
function mapRainfallPumpStationListToGridRows(
list: RainfallPumpStationItem[],
ctpvNm: string,
@@ -275,6 +389,7 @@ function mapRainfallPumpStationListToGridRows(
// 하수도 통계 관리 > 04. 하수저류 시설 및 빗물 펌프장
const SewageReservoirPumpInp = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const { confirm } = useConfirm();
const { isAdminInfo } = useAdminStore();
const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate();
@@ -282,24 +397,228 @@ const SewageReservoirPumpInp = () => {
console.log("submit");
};
const saveFunc = () => {
console.log("save");
};
/** 마지막 조회 시 서버에 있던 하수저류시설 목록 (deleteList 비교 기준) */
const loadedSewageReservoirSnapshotRef = useRef<SewageReservoirSaveItem[]>([]);
const cnacelSubmitFunc = () => {
console.log("cancel submit");
void (async () => {
try {
if (!(await confirm("제출된 내용을 취소하시겠습니까?"))) return;
if (!lastSearchedValues) {
alert("먼저 조회를 진행해주세요.");
return;
}
const tabKey = activeTabRef.current;
const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey;
const userInfo = await getUserInfo();
const requestData: cancelSubmitRequest = {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
statsYr: lastSearchedValues.year ?? "",
mdfrId: userInfo.userUnqId,
statsCd: statsCdForTab,
};
const response = await statisticsApi.cancelSubmit(requestData);
if (response.status === "success") {
setIsLoading(true);
await loadActiveTabData(tabKey, lastSearchedValues, false, false);
alert("제출 취소되었습니다.");
} else {
alert(response.message ?? "제출 취소 중 오류가 발생했습니다.");
}
} catch {
alert("제출 취소 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
})();
};
const isCheckedRow = (row: Record<string, string | number>): boolean => {
const v = row.__checked;
return v === 1 || v === "1" || v === "Y" || v === "y" || v === "true";
};
const getCheckedItemIndexesFromGrid = (
tabKey: string,
rowCount: number,
): number[] => {
const gridRef =
tabKey === SEWAGE_RESERVOIR_STATS_CD
? reservoirGridRef
: tabKey === rainfallPumpTabKey
? rainfallPumpGridRef
: null;
if (!gridRef?.current?.gridView) return [];
const gridView = gridRef.current.gridView as {
getCheckedItems?: () => number[];
getCheckedRows?: () => number[];
};
const checked =
(typeof gridView.getCheckedItems === "function"
? gridView.getCheckedItems()
: typeof gridView.getCheckedRows === "function"
? gridView.getCheckedRows()
: []) ?? [];
return checked.filter(
(idx) => Number.isInteger(idx) && idx >= 0 && idx < rowCount,
);
};
const getCurrentRowsForTab = (
tabKey: string,
): Record<string, string | number>[] => {
if (tabKey === SEWAGE_RESERVOIR_STATS_CD) {
return reservoirGridGetRowsRef.current?.() ?? rowDataMap.get(tabKey) ?? [];
}
if (tabKey === rainfallPumpTabKey) {
return rainfallPumpGridGetRowsRef.current?.() ?? rowDataMap.get(tabKey) ?? [];
}
return rowDataMap.get(tabKey) ?? [];
};
const commitGridForTab = (tabKey: string) => {
const gridRef =
tabKey === SEWAGE_RESERVOIR_STATS_CD
? reservoirGridRef
: tabKey === rainfallPumpTabKey
? rainfallPumpGridRef
: null;
const gridView = gridRef?.current?.gridView as
| {
commit?: (force?: boolean) => void;
cancel?: () => void;
}
| undefined;
try {
gridView?.commit?.(true);
} catch {
gridView?.cancel?.();
}
};
const deleteArticleFunc = () => {
console.log("delete article");
const tabKey = activeTabRef.current;
const currentRows = getCurrentRowsForTab(tabKey);
if (currentRows.length === 0) {
alert("삭제할 데이터가 없습니다.");
return;
}
const checkedIndexes = getCheckedItemIndexesFromGrid(
tabKey,
currentRows.length,
);
const checkedIndexSet =
checkedIndexes.length > 0
? new Set(checkedIndexes)
: new Set(
currentRows
.map((row, idx) => (isCheckedRow(row) ? idx : -1))
.filter((idx) => idx >= 0),
);
const nextRows = currentRows.filter((_, idx) => !checkedIndexSet.has(idx));
if (nextRows.length === currentRows.length) {
alert("삭제할 항목을 선택해주세요.");
return;
}
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(tabKey, nextRows);
return next;
});
};
const saveFunc = () => {
void (async () => {
const tabKey = activeTabRef.current;
if (tabKey !== SEWAGE_RESERVOIR_STATS_CD) {
alert("저장할 수 없는 탭입니다.");
return;
}
if (!lastSearchedValues) {
alert("먼저 조회를 진행해주세요.");
return;
}
if (!buildGugunOkForToolbar(lastSearchedValues)) {
alert("시군구를 선택해주세요.");
return;
}
if (!(await confirm("저장 하시겠습니까?"))) return;
commitGridForTab(tabKey);
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false;
const currentData = getCurrentRowsForTab(tabKey);
const saveList = noApplicable
? []
: currentData.map((row) =>
buildSewageReservoirSaveItem(row, sggOptions),
);
const deleteList = buildSewageReservoirDeleteList(
loadedSewageReservoirSnapshotRef.current,
currentData,
);
if (!noApplicable && saveList.length === 0 && deleteList.length === 0) {
alert("저장할 변경 사항이 없습니다.");
return;
}
try {
setIsLoading(true);
const userInfo = await getUserInfo();
const response =
await statisticsInputSewageReservoirPumpApi.saveSewageReservoir({
statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
userId: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
...(noApplicable ? { noApplicable: true } : {}),
saveList,
deleteList,
});
if (response.status === "success") {
await loadActiveTabData(tabKey, lastSearchedValues, false, false);
alert("저장되었습니다.");
} else {
alert(response.message ?? "저장 중 오류가 발생했습니다.");
}
} catch {
alert("저장 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
})();
};
const [activeTab, setActiveTab] = useState(firstTabKey);
const [rowDataMap, setRowDataMap] = useState<
Map<string, Record<string, string | number>[]>
>(new Map());
const [isNotApplicable, setIsNotApplicable] = useState(false);
const [showIsNotApplicable, setShowIsNotApplicable] = useState(false);
const [isNotApplicableByTab, setIsNotApplicableByTab] = useState<Map<string, boolean>>(
() => new Map(),
);
const [searchValues, setSearchValues] = useState<SearchFormValues | null>(null);
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(
null,
@@ -317,13 +636,40 @@ const SewageReservoirPumpInp = () => {
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
const [sggOptions, setSggOptions] = useState<Sgg[]>([]);
const reservoirGridRef = useRef<RealGridReact | null>(null);
const rainfallPumpGridRef = useRef<RealGridReact | null>(null);
const reservoirGridGetRowsRef =
useRef<(() => Record<string, string | number>[]) | null>(null);
const rainfallPumpGridGetRowsRef =
useRef<(() => Record<string, string | number>[]) | null>(null);
const sggOptionsSidoRef = useRef<string | null>(null);
const searchDoneRef = useRef(false);
const periodEnterableRef = useRef(true);
const activeTabRef = useRef(activeTab);
const loadedTabKeysRef = useRef<Set<string>>(new Set());
activeTabRef.current = activeTab;
const shouldShowNotApplicable = (
tabKey: string,
hasRows: boolean,
statusInfo: SewerageStatusInfo | null | undefined,
): boolean =>
(tabKey === SEWAGE_RESERVOIR_STATS_CD || tabKey === rainfallPumpTabKey) &&
hasSearched &&
!hasRows &&
computeTabEnterable(statusInfo, inputPeriodEnterable);
const setIsNotApplicableForTab = useCallback(
(tabKey: string, nextValue: boolean) => {
setIsNotApplicableByTab((prev) => {
const next = new Map(prev);
next.set(tabKey, nextValue);
return next;
});
},
[],
);
const commitToolbarSnapshotForLoadedTab = (
tabKey: string,
statusInfo: SewerageStatusInfo | null,
@@ -371,8 +717,8 @@ const SewageReservoirPumpInp = () => {
updateLastSearchedValues: boolean,
overrides?: { mode?: string },
): Promise<boolean> => {
// 서버 데이터로 새로 불러오는 시점이므로 "해당없음" 체크는 항상 초기화
setIsNotApplicable(false);
// 서버 데이터로 새로 불러오는 시점이므로 현재 탭의 "해당없음" 체크는 초기화
setIsNotApplicableForTab(tabKey, false);
if (tabKey === SEWAGE_RESERVOIR_STATS_CD) {
const response =
await statisticsInputSewageReservoirPumpApi.getSewageReservoirList(
@@ -386,15 +732,14 @@ const SewageReservoirPumpInp = () => {
const { commonInfo, statusInfo, sewageReservoirList } = response.data;
const list = sewageReservoirList ?? [];
loadedSewageReservoirSnapshotRef.current = list
.filter((item) => String(item.fcltSn ?? "").trim() !== "")
.map(buildSewageReservoirSaveItemFromApiItem);
const gridRows = mapSewageReservoirListToGridRows(
list,
commonInfo.ctpvNm,
);
if (activeTabRef.current === tabKey) {
setShowIsNotApplicable(list.length === 0);
}
applyRowMap(mergeIntoExistingMap, (m) => {
m.set(tabKey, gridRows);
});
@@ -409,6 +754,7 @@ const SewageReservoirPumpInp = () => {
gridRows.length > 0,
values,
);
loadedTabKeysRef.current.add(tabKey);
return true;
}
@@ -444,6 +790,7 @@ const SewageReservoirPumpInp = () => {
gridRows.length > 0,
values,
);
loadedTabKeysRef.current.add(tabKey);
return true;
}
@@ -490,6 +837,7 @@ const SewageReservoirPumpInp = () => {
}, []);
const handleReset = () => {
loadedSewageReservoirSnapshotRef.current = [];
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
@@ -499,10 +847,10 @@ const SewageReservoirPumpInp = () => {
sggOptionsSidoRef.current = null;
setHasSearched(false);
setInputPeriodEnterable(true);
setIsNotApplicable(false);
setShowIsNotApplicable(false);
setIsNotApplicableByTab(new Map());
searchDoneRef.current = false;
periodEnterableRef.current = true;
loadedTabKeysRef.current.clear();
setActiveTab(firstTabKey);
};
@@ -521,6 +869,7 @@ const SewageReservoirPumpInp = () => {
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setToolbarSnapshotByTab(new Map());
loadedTabKeysRef.current.clear();
setActiveTab(firstTabKey);
}
@@ -529,6 +878,12 @@ const SewageReservoirPumpInp = () => {
searchDoneRef.current = true;
setHasSearched(true);
setLastSearchedValues(values);
const loadedSgg = await loadSggOptions(values.sido);
sggOptionsSidoRef.current = values.sido;
if (loadedSgg.length === 0) {
alert("시군구 목록을 불러오지 못했습니다.");
return;
}
const tabKeyToLoad = yearChangedFromLastSearch ? firstTabKey : activeTabRef.current;
if (yearChangedFromLastSearch) {
@@ -551,15 +906,21 @@ const SewageReservoirPumpInp = () => {
};
const handleActiveTabChange = async (tabKey: string) => {
setActiveTab(tabKey);
if (tabKey === SEWAGE_RESERVOIR_STATS_CD) {
setShowIsNotApplicable(
hasSearched && !tabRowDataExists(rowDataMap, tabKey),
);
} else {
setShowIsNotApplicable(false);
const prevTabKey = activeTabRef.current;
commitGridForTab(prevTabKey);
const latestRowsInPrevTab = getCurrentRowsForTab(prevTabKey);
if (latestRowsInPrevTab.length > 0 || rowDataMap.has(prevTabKey)) {
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(prevTabKey, latestRowsInPrevTab);
return next;
});
}
activeTabRef.current = tabKey;
setActiveTab(tabKey);
if (!hasSearched || !lastSearchedValues) return;
if (loadedTabKeysRef.current.has(tabKey)) return;
if (tabRowDataExists(rowDataMap, tabKey)) return;
try {
@@ -602,6 +963,26 @@ const SewageReservoirPumpInp = () => {
activeTab === SEWAGE_RESERVOIR_STATS_CD || activeTab === rainfallPumpTabKey;
const showArticleToolbarCard =
hasSearched && isReservoirOrRainfallTab && toolbarGroupAllowed;
const isNotApplicableForActiveTab = isNotApplicableByTab.get(activeTab) ?? false;
const showNotApplicableForActiveTab =
loadedTabKeysRef.current.has(activeTab) &&
shouldShowNotApplicable(
activeTab,
tabRowDataExists(rowDataMap, activeTab),
tabStatusInfoMap.get(activeTab),
);
const setIsNotApplicableForActiveTab: Dispatch<SetStateAction<boolean>> = useCallback(
(next) => {
const tabKey = activeTabRef.current;
const current = isNotApplicableByTab.get(tabKey) ?? false;
const resolved =
typeof next === "function"
? (next as (prevState: boolean) => boolean)(current)
: next;
setIsNotApplicableForTab(tabKey, resolved);
},
[isNotApplicableByTab, setIsNotApplicableForTab],
);
const openVerificationHistoryDialog = () => {
const ctx = lastSearchedValues ?? searchValues;
@@ -673,8 +1054,6 @@ const SewageReservoirPumpInp = () => {
// 행 추가 직후 setColumnProperty를 강제 재적용하면 타이밍 이슈로
// `fieldIndex is out of bounds`가 발생할 수 있어 제거한다.
setShowIsNotApplicable(false);
const statusInfo = tabStatusInfoMap.get(tabKey) ?? null;
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, true, ctx);
};
@@ -690,6 +1069,13 @@ const SewageReservoirPumpInp = () => {
) || isAdminInfo;
const isReservoirTab = tabKey === SEWAGE_RESERVOIR_STATS_CD;
const columns = (tab.columns ?? []).map((col: ColumnConfig) => {
if (isReservoirTab && col.fieldName === "ctpvNm") {
return {
...col,
editable: false as const,
styleName: col.styleName ?? "rg-cell-readonly",
};
}
if (isReservoirTab && col.fieldName === "sggNm") {
return buildSggNmColumn(col, canEdit, sggOptions);
}
@@ -709,31 +1095,6 @@ const SewageReservoirPumpInp = () => {
return map;
}, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, sggOptions]);
const handleSewageReservoirCellEdited = useCallback(
(params: CellEditedCallbackParams) => {
if (params.fieldName !== "sggNm") return;
const selected = sggOptions.find((s) => s.gugun === params.newValue);
if (!selected) return;
const tabKey = SEWAGE_RESERVOIR_STATS_CD;
setRowDataMap((prev) => {
const next = new Map(prev);
const rows = [...(next.get(tabKey) ?? [])];
const row = rows[params.itemIndex];
if (!row) return prev;
rows[params.itemIndex] = {
...row,
sggNm: selected.gugun,
admdstCd: selected.adstDstrCd,
admdstNm: selected.gugun,
};
next.set(tabKey, rows);
return next;
});
},
[sggOptions],
);
const verificationCellStyle = ({
fieldName,
row,
@@ -817,10 +1178,10 @@ const SewageReservoirPumpInp = () => {
{showArticleToolbarCard && (
<div className="card !py-0">
<div className="m-[0.6rem] flex items-center justify-end gap-[0.6rem]">
{showIsNotApplicable && (
{showNotApplicableForActiveTab && (
<NotApplicableCheckButton
isCheck={isNotApplicable}
setIsCheck={setIsNotApplicable}
isCheck={isNotApplicableForActiveTab}
setIsCheck={setIsNotApplicableForActiveTab}
/>
)}
<AddArticleButton addArticleFunc={addArticleFunc} />
@@ -859,13 +1220,13 @@ const SewageReservoirPumpInp = () => {
return (
<TabsContent key={tabKey} value={tabKey}>
<DataGrid
key={
tabEnterableForGrid || isAdminInfo
? `${tabKey}-enterable`
: `${tabKey}-readonly`
}
key={tabKey}
getGridRef={
isReservoirTab ? reservoirGridRef : undefined
isReservoirTab
? reservoirGridRef
: tabKey === rainfallPumpTabKey
? rainfallPumpGridRef
: undefined
}
columnInfo={
columnInfoByTab.get(tabKey) ??
@@ -894,6 +1255,13 @@ const SewageReservoirPumpInp = () => {
return next;
});
}}
getGridDataRef={
tabKey === SEWAGE_RESERVOIR_STATS_CD
? reservoirGridGetRowsRef
: tabKey === rainfallPumpTabKey
? rainfallPumpGridGetRowsRef
: undefined
}
showHeaderCheckbox
statusInfo={tabStatusInfo}
isExcelImport={
@@ -903,13 +1271,6 @@ const SewageReservoirPumpInp = () => {
}
cellStyleCallback={verificationCellStyle}
cellTooltipCallback={verificationCellTooltip}
onCellEdited={
isReservoirTab &&
tabEnterableForGrid &&
sggOptions.length > 0
? handleSewageReservoirCellEdited
: undefined
}
/>
</TabsContent>
);
@@ -921,6 +1282,7 @@ const SewageReservoirPumpInp = () => {
onOpenChange={setVerificationHistoryOpen}
queryContext={verificationQueryContext}
/>
<ConfirmDialog isOpen={false} />
</>
);
};
@@ -196,6 +196,20 @@ const LogHistory = () => {
const handleSearch = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (startDate && endDate) {
const start = dayjs(startDate);
const end = dayjs(endDate);
if (start.isAfter(end, "day")) {
alert("시작일은 종료일보다 늦을 수 없습니다.");
return;
}
if (end.isBefore(start, "day")) {
alert("종료일은 시작일보다 빠를 수 없습니다.");
return;
}
}
await loadLogs(1);
};
@@ -213,6 +213,7 @@ function buildRowsFromStatusList(
statKeys: string[],
): Record<string, string | number>[] {
const rowMap = new Map<string, Record<string, string | number>>();
const rowOrder: string[] = [];
for (const item of statusList) {
const { sido, sgg } = splitSidoSggNm(item.sidoSggNm);
@@ -228,6 +229,7 @@ function buildRowsFromStatusList(
row[key] = "";
}
rowMap.set(rowKey, row);
rowOrder.push(rowKey);
}
const row = rowMap.get(rowKey)!;
@@ -236,11 +238,7 @@ function buildRowsFromStatusList(
}
}
return Array.from(rowMap.values()).sort((a, b) => {
const sidoCmp = String(a[SIDO_FIELD]).localeCompare(String(b[SIDO_FIELD]), "ko");
if (sidoCmp !== 0) return sidoCmp;
return String(a[SGG_FIELD]).localeCompare(String(b[SGG_FIELD]), "ko");
});
return rowOrder.map((key) => rowMap.get(key)!);
}
function buildUnSubmitDataRequest(
@@ -149,6 +149,14 @@ export type ComputedFieldConfig =
denominatorFields: string[];
thirdFields: string[];
type: "swrgInstlrtRate";
}
| {
pumpGroups: Array<{
pumpCntField: string;
rsrvPumpCntField: string;
perPpqtyField: string;
}>;
type: "pumpGrossPpqty";
};
export type ColumnConfig = {
@@ -8,69 +8,74 @@ import { ValueType } from "realgrid";
export const PumpingStationInpColumn = {
columns: [
// 행정구역
{ name: 'sido', fieldName: '시도', header: { text: '시도' }, editable: false },
{ name: 'sgg', fieldName: '시군구', header: { text: '시군구' }, footer: { text: '합계' } },
{ name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' },
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } },
// 기본 정보
{ name: 'treatmentFacilityName', fieldName: '처리장명', header: { text: '처리장명' } },
{ name: 'facilityName', fieldName: '시설명', header: { text: '시설명' } },
{ name: 'classification', fieldName: '구분(합류식 분류식)', header: { text: '구분(합류식 분류식)' } },
{ name: 'location', fieldName: '위치', header: { text: '위치' } },
{ name: 'operationDate', fieldName: '가동 년월일 yyyy-mm-dd', header: { text: '가동 년월일 yyyy-mm-dd' } },
{ name: 'planDrainageArea', fieldName: '계획배수면적(㎢)', header: { text: '계획배수면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
{ name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '처리장명' } },
{ name: 'fcltNm', fieldName: 'fcltNm', header: { text: '시설명' } },
{ name: 'pumpSeNm', fieldName: 'pumpSeNm', header: { text: '구분(합류식 분류식)' } },
{ name: 'fcltPstnCn', fieldName: 'fcltPstnCn', header: { text: '위치' } },
{ name: 'oprtngStrtYmd', fieldName: 'oprtngStrtYmd', header: { text: '가동 년월일 yyyy-mm-dd' } },
{ name: 'planMltvArea', fieldName: 'planMltvArea', header: { text: '계획배수면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
// 계획시간 대 최대배수량
{ name: 'fairWeatherFlow', fieldName: '청천시(A)', header: { text: '청천시(A)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'rainyWeatherFlow', fieldName: '우천시(B)', header: { text: '우천시(B)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'rainwaterFlow', fieldName: '우수량(B-A)', header: { text: '우수량(B-A)' }, footer: footerDecimal1, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'sndyArea', fieldName: 'sndyArea', header: { text: '청천시(A)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'rainArea', fieldName: 'rainArea', header: { text: '우천시(B)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'exlnQnt', fieldName: 'exlnQnt', header: { text: '우수량(B-A)' }, footer: footerDecimal1, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
// 배수능력 > 총 양수량 (오수)
{ name: 'sewageTotalPumpCapacity', fieldName: '총양수량(m³/min)', header: { text: '총양수량(m³/min)' }, footer: footerDecimal1, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'drtwPumpGrssPpqty', fieldName: 'drtwPumpGrssPpqty', header: { text: '총양수량(m³/min)' }, footer: footerDecimal1, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
// 배수능력 > 오수펌프(1)
{ name: 'sewagePump1Diameter', fieldName: '오수펌프(1) 구경(mm)', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'sewagePump1Capacity', fieldName: '오수펌프(1) 대당 양수량(m³/min·대)', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'sewagePump1Count', fieldName: '오수펌프(1) 펌프대수', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'sewagePump1Reserve', fieldName: '오수펌프(1) 예비펌프대수', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'drtwPumpN1DmtrArea', fieldName: 'drtwPumpN1DmtrArea', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'drtwPumpN1Ppqty', fieldName: 'drtwPumpN1Ppqty', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'drtwPumpN1Cnt', fieldName: 'drtwPumpN1Cnt', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'drtwPumpN1RsrvCnt', fieldName: 'drtwPumpN1RsrvCnt', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
// 배수능력 > 오수펌프(2)
{ name: 'sewagePump2Diameter', fieldName: '오수펌프(2) 구경(mm)', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'sewagePump2Capacity', fieldName: '오수펌프(2) 대당 양수량(m³/min·대)', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'sewagePump2Count', fieldName: '오수펌프(2) 펌프대수', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'sewagePump2Reserve', fieldName: '오수펌프(2) 예비펌프대수', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'drtwPumpN2DmtrArea', fieldName: 'drtwPumpN2DmtrArea', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'drtwPumpN2Ppqty', fieldName: 'drtwPumpN2Ppqty', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'drtwPumpN2Cnt', fieldName: 'drtwPumpN2Cnt', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'drtwPumpN2RsrvCnt', fieldName: 'drtwPumpN2RsrvCnt', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
// 배수능력 > 오수펌프(3)
{ name: 'sewagePump3Diameter', fieldName: '오수펌프(3) 구경(mm)', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'sewagePump3Capacity', fieldName: '오수펌프(3) 대당 양수량(m³/min·대)', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'sewagePump3Count', fieldName: '오수펌프(3) 펌프대수', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'sewagePump3Reserve', fieldName: '오수펌프(3) 예비펌프대수', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'drtwPumpN3DmtrArea', fieldName: 'drtwPumpN3DmtrArea', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'drtwPumpN3Ppqty', fieldName: 'drtwPumpN3Ppqty', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'drtwPumpN3Cnt', fieldName: 'drtwPumpN3Cnt', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'drtwPumpN3RsrvCnt', fieldName: 'drtwPumpN3RsrvCnt', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
// 배수능력 > 총 양수량 (우수)
{ name: 'rainwaterTotalPumpCapacity', fieldName: '총양수량(m³/min)', header: { text: '총양수량(m³/min)' }, footer: footerDecimal1, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'exlnPumpGrssPpqty', fieldName: 'exlnPumpGrssPpqty', header: { text: '총양수량(m³/min)' }, footer: footerDecimal1, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
// 배수능력 > 우수펌프(1)
{ name: 'rainwaterPump1Diameter', fieldName: '우수펌프(1) 구경(mm)', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'rainwaterPump1Capacity', fieldName: '우수펌프(1) 대당 양수량(m³/min·대)', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'rainwaterPump1Count', fieldName: '우수펌프(1) 펌프대수', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'rainwaterPump1Reserve', fieldName: '우수펌프(1) 예비펌프대수', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'exlnPumpN1DmtrArea', fieldName: 'exlnPumpN1DmtrArea', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'exlnPumpN1Ppqty', fieldName: 'exlnPumpN1Ppqty', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'exlnPumpN1Cnt', fieldName: 'exlnPumpN1Cnt', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'exlnPumpN1RsrvCnt', fieldName: 'exlnPumpN1RsrvCnt', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
// 배수능력 > 우수펌프(2)
{ name: 'rainwaterPump2Diameter', fieldName: '우수펌프(2) 구경(mm)', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'rainwaterPump2Capacity', fieldName: '우수펌프(2) 대당 양수량(m³/min·대)', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'rainwaterPump2Count', fieldName: '우수펌프(2) 펌프대수', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'rainwaterPump2Reserve', fieldName: '우수펌프(2) 예비펌프대수', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'exlnPumpN2DmtrArea', fieldName: 'exlnPumpN2DmtrArea', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'exlnPumpN2Ppqty', fieldName: 'exlnPumpN2Ppqty', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'exlnPumpN2Cnt', fieldName: 'exlnPumpN2Cnt', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'exlnPumpN2RsrvCnt', fieldName: 'exlnPumpN2RsrvCnt', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
// 배수능력 > 우수펌프(3)
{ name: 'rainwaterPump3Diameter', fieldName: '우수펌프(3) 구경(mm)', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'rainwaterPump3Capacity', fieldName: '우수펌프(3) 대당 양수량(m³/min·대)', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'rainwaterPump3Count', fieldName: '우수펌프(3) 펌프대수', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'rainwaterPump3Reserve', fieldName: '우수펌프(3) 예비펌프대수', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'exlnPumpN3DmtrArea', fieldName: 'exlnPumpN3DmtrArea', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'exlnPumpN3Ppqty', fieldName: 'exlnPumpN3Ppqty', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'exlnPumpN3Cnt', fieldName: 'exlnPumpN3Cnt', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'exlnPumpN3RsrvCnt', fieldName: 'exlnPumpN3RsrvCnt', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
// 연간전력 사용량
{ name: 'annualPowerUsage', fieldName: '연간전력사용량(kWh/년)', header: { text: '연간전력사용량(kWh/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'anlElpwrUseQnt', fieldName: 'anlElpwrUseQnt', header: { text: '연간전력사용량(kWh/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
// 비고
{ name: 'remarks', fieldName: '비고', header: { text: '비고' } },
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
// Hidden Columns : 시스템 관리를 위한 값
{ name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, },
{ name: 'fcltSn', fieldName: 'fcltSn', header: { text: '시설일련번호' }, visible: false, },
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false },
],
columnLayout: [
@@ -78,66 +83,69 @@ export const PumpingStationInpColumn = {
name: 'administrativeArea',
hideChildHeaders: false,
header: { text: '행정구역' },
items: ['sido', 'sgg'],
items: ['ctpvNm', 'sggNm'],
},
'treatmentFacilityName',
'facilityName',
'classification',
'location',
'operationDate',
'planDrainageArea',
'swtrmtFcltNm',
'fcltNm',
'pumpSeNm',
'fcltPstnCn',
'oprtngStrtYmd',
'planMltvArea',
{
name: 'planFlowGroup',
hideChildHeaders: false,
header: { text: '계획시간대최대배수량(m³/hr)' },
items: ['fairWeatherFlow', 'rainyWeatherFlow', 'rainwaterFlow'],
items: ['sndyArea', 'rainArea', 'exlnQnt'],
},
{
name: 'drainageCapability',
hideChildHeaders: false,
header: { text: '배수능력' },
items: [
'sewageTotalPumpCapacity',
'drtwPumpGrssPpqty',
{
name: 'sewagePump1Group',
hideChildHeaders: false,
header: { text: '오수펌프(1)' },
items: ['sewagePump1Diameter', 'sewagePump1Capacity', 'sewagePump1Count', 'sewagePump1Reserve'],
items: ['drtwPumpN1DmtrArea', 'drtwPumpN1Ppqty', 'drtwPumpN1Cnt', 'drtwPumpN1RsrvCnt'],
},
{
name: 'sewagePump2Group',
hideChildHeaders: false,
header: { text: '오수펌프(2)' },
items: ['sewagePump2Diameter', 'sewagePump2Capacity', 'sewagePump2Count', 'sewagePump2Reserve'],
items: ['drtwPumpN2DmtrArea', 'drtwPumpN2Ppqty', 'drtwPumpN2Cnt', 'drtwPumpN2RsrvCnt'],
},
{
name: 'sewagePump3Group',
hideChildHeaders: false,
header: { text: '오수펌프(3)' },
items: ['sewagePump3Diameter', 'sewagePump3Capacity', 'sewagePump3Count', 'sewagePump3Reserve'],
items: ['drtwPumpN3DmtrArea', 'drtwPumpN3Ppqty', 'drtwPumpN3Cnt', 'drtwPumpN3RsrvCnt'],
},
'rainwaterTotalPumpCapacity',
'exlnPumpGrssPpqty',
{
name: 'rainwaterPump1Group',
hideChildHeaders: false,
header: { text: '우수펌프(1)' },
items: ['rainwaterPump1Diameter', 'rainwaterPump1Capacity', 'rainwaterPump1Count', 'rainwaterPump1Reserve'],
items: ['exlnPumpN1DmtrArea', 'exlnPumpN1Ppqty', 'exlnPumpN1Cnt', 'exlnPumpN1RsrvCnt'],
},
{
name: 'rainwaterPump2Group',
hideChildHeaders: false,
header: { text: '우수펌프(2)' },
items: ['rainwaterPump2Diameter', 'rainwaterPump2Capacity', 'rainwaterPump2Count', 'rainwaterPump2Reserve'],
items: ['exlnPumpN2DmtrArea', 'exlnPumpN2Ppqty', 'exlnPumpN2Cnt', 'exlnPumpN2RsrvCnt'],
},
{
name: 'rainwaterPump3Group',
hideChildHeaders: false,
header: { text: '우수펌프(3)' },
items: ['rainwaterPump3Diameter', 'rainwaterPump3Capacity', 'rainwaterPump3Count', 'rainwaterPump3Reserve'],
items: ['exlnPumpN3DmtrArea', 'exlnPumpN3Ppqty', 'exlnPumpN3Cnt', 'exlnPumpN3RsrvCnt'],
},
],
},
'annualPowerUsage',
'remarks',
'anlElpwrUseQnt',
'rmrk',
'admdstCd',
'fcltSn',
'admdstNm'
]
};
@@ -1,6 +1,33 @@
import type { ColumnConfig } from "@/components/type/DataGrid.type";
import { footerInt, footerDecimal1, footerDecimal2, footerDecimal3 } from "@/components/util/grid/footerSummary";
import { ValueType } from "realgrid";
const RAINFALL_PUMP_FCLTY_KIND_VALUES = ["해당없음", "제1종", "제2종", "제3종"] as const;
const RAINFALL_PUMP_YN_VALUES = ["유", "무"] as const;
function dropdownColumn(
col: Pick<ColumnConfig, "name" | "fieldName" | "header"> &
Partial<ColumnConfig>,
options: readonly string[],
): ColumnConfig {
const values = [...options];
return {
...col,
values,
labels: values,
lookupDisplay: true,
editor: {
type: "dropdown",
domainOnly: true,
textReadOnly: true,
commitOnSelect: true,
dropDownWhenClick: true,
values,
labels: values,
},
};
}
// 하수도 통계 관리 > 04.하수저류시설 및 빗물펌프장 > tab 2: 빗물펌프장
export const RainfallPumpStationInpColumn = {
columns: [
@@ -10,7 +37,10 @@ export const RainfallPumpStationInpColumn = {
{ name: 'fcltNm', fieldName: 'fcltNm', header: { text: '시설명' } },
{ name: 'fcltPstnCn', fieldName: 'fcltPstnCn', header: { text: '위치' } },
{ name: 'fcltyKndCn', fieldName: 'fcltyKndCn', header: { text: '시설물종류' } },
dropdownColumn(
{ name: 'fcltyKndCn', fieldName: 'fcltyKndCn', header: { text: '시설물종류' } },
RAINFALL_PUMP_FCLTY_KIND_VALUES,
),
{
name: 'oprtngStrtYmd',
@@ -29,14 +59,46 @@ export const RainfallPumpStationInpColumn = {
{ name: 'wtrStrgZoneArea', fieldName: 'wtrStrgZoneArea', header: { text: '집수구역면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
// 유수지
{ name: 'rsvrYn', fieldName: 'rsvrYn', header: { text: '유수지유무(유,무)' } },
dropdownColumn(
{ name: 'rsvrYn', fieldName: 'rsvrYn', header: { text: '유수지유무(유,무)' } },
RAINFALL_PUMP_YN_VALUES,
),
{ name: 'rsvrImpndQnt', fieldName: 'rsvrImpndQnt', header: { text: '저류랑(m³)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
// 흡수정
{ name: 'abstnRgnImpdQnt', fieldName: 'abstnRgnImpdQnt', header: { text: '저류랑(m³)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
// 펌프시설 > 총양수량(m³/min)
{ name: 'grssPpqty', fieldName: 'grssPpqty', header: { text: '총양수량(m³/min)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{
name: 'grssPpqty',
fieldName: 'grssPpqty',
header: { text: '총양수량(m³/min)' },
editable: false,
footer: footerDecimal1,
valueType: ValueType.NUMBER,
dataFormat: '#,##0.0',
styleName: 'rg-cell-total',
computed: {
type: 'pumpGrossPpqty' as const,
pumpGroups: [
{
pumpCntField: 'pumpFcltN1PumpCnt',
rsrvPumpCntField: 'pumpFcltN1RsrvPumpCnt',
perPpqtyField: 'pumpFcltN1PerPpqty',
},
{
pumpCntField: 'pumpFcltN2PumpCnt',
rsrvPumpCntField: 'pumpFcltN2RsrvPumpCnt',
perPpqtyField: 'pumpFcltN2PerPpqty',
},
{
pumpCntField: 'pumpFcltN3PumpCnt',
rsrvPumpCntField: 'pumpFcltN3RsrvPumpCnt',
perPpqtyField: 'pumpFcltN3PerPpqty',
},
],
},
},
// 펌프시설1
{ name: 'pumpFcltN1DmtrExpln', fieldName: 'pumpFcltN1DmtrExpln', header: { text: '구경(mm)' } },
@@ -57,9 +119,18 @@ export const RainfallPumpStationInpColumn = {
{ name: 'pumpFcltN3RsrvPumpCnt', fieldName: 'pumpFcltN3RsrvPumpCnt', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
// 부대시설
{ name: 'prcsgFcltYn', fieldName: 'prcsgFcltYn', header: { text: '전처리시설유무(유/무)' } },
{ name: 'ddrztnEqpmYn', fieldName: 'ddrztnEqpmYn', header: { text: '탈취설비유무(유/무)' } },
{ name: 'oslfEtgtYn', fieldName: 'oslfEtgtYn', header: { text: '자가발전유무(유/무)' } },
dropdownColumn(
{ name: 'prcsgFcltYn', fieldName: 'prcsgFcltYn', header: { text: '전처리시설유무(유/무)' } },
RAINFALL_PUMP_YN_VALUES,
),
dropdownColumn(
{ name: 'ddrztnEqpmYn', fieldName: 'ddrztnEqpmYn', header: { text: '탈취설비유무(유/무)' } },
RAINFALL_PUMP_YN_VALUES,
),
dropdownColumn(
{ name: 'oslfEtgtYn', fieldName: 'oslfEtgtYn', header: { text: '자가발전유무(유/무)' } },
RAINFALL_PUMP_YN_VALUES,
),
// 방류수역
{ name: 'dschgWtrsExpln', fieldName: 'dschgWtrsExpln', header: { text: '방류수역' } },
@@ -11,6 +11,10 @@ export const SEWAGE_RESERVOIR_FCLT_STRCT_VALUES = [
"지하터널식",
"기타",
] as const;
export const SEWAGE_RESERVOIR_YN_VALUES = ["유", "무"] as const;
export const SEWAGE_RESERVOIR_MLTV_MTH_VALUES = ["자연배수", "강제배수"] as const;
export const SEWAGE_RESERVOIR_IMPD_MTH_VALUES = ["직렬", "병렬"] as const;
export const SEWAGE_RESERVOIR_IMPD_PRCS_MTHD_VALUES = ["자체처리", "연계처리"] as const;
function dropdownColumn(
col: Pick<ColumnConfig, "name" | "fieldName" | "header"> &
@@ -99,7 +103,36 @@ export const SewageReservoirInpColumn = {
{ name: 'impdFcltCpct', fieldName: 'impdFcltCpct', header: { text: '시설용량(m³)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
// 펌프시설 > 총양수량
{ name: 'grssPpqty', fieldName: 'grssPpqty', header: { text: '총양수량(m³/min)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{
name: 'grssPpqty',
fieldName: 'grssPpqty',
header: { text: '총양수량(m³/min)' },
editable: false,
footer: footerDecimal1,
valueType: ValueType.NUMBER,
dataFormat: '#,##0.0',
styleName: 'rg-cell-total',
computed: {
type: 'pumpGrossPpqty' as const,
pumpGroups: [
{
pumpCntField: 'pumpFcltN1PumpCnt',
rsrvPumpCntField: 'pumpFcltN1RsrvPumpCnt',
perPpqtyField: 'pumpFcltN1PerPpqty',
},
{
pumpCntField: 'pumpFcltN2PumpCnt',
rsrvPumpCntField: 'pumpFcltN2RsrvPumpCnt',
perPpqtyField: 'pumpFcltN2PerPpqty',
},
{
pumpCntField: 'pumpFcltN3PumpCnt',
rsrvPumpCntField: 'pumpFcltN3RsrvPumpCnt',
perPpqtyField: 'pumpFcltN3PerPpqty',
},
],
},
},
// 펌프시설 1
{ name: 'pumpFcltN1DmtrExpln', fieldName: 'pumpFcltN1DmtrExpln', header: { text: '구경(mm)' } },
@@ -120,18 +153,60 @@ export const SewageReservoirInpColumn = {
{ name: 'pumpFcltN3RsrvPumpCnt', fieldName: 'pumpFcltN3RsrvPumpCnt', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
// 부대시설
{ name: 'prcsgFcltYn', fieldName: 'prcsgFcltYn', header: { text: '전처리시설유무(유/무)' } },
{ name: 'ddrztnEqpmYn', fieldName: 'ddrztnEqpmYn', header: { text: '탈취설비유무(유/무)' } },
{ name: 'oslfEtgtYn', fieldName: 'oslfEtgtYn', header: { text: '자가발전유무(유/무)' } },
dropdownColumn(
{
name: "prcsgFcltYn",
fieldName: "prcsgFcltYn",
header: { text: "전처리시설유무(유/무)" },
},
SEWAGE_RESERVOIR_YN_VALUES,
),
dropdownColumn(
{
name: "ddrztnEqpmYn",
fieldName: "ddrztnEqpmYn",
header: { text: "탈취설비유무(유/무)" },
},
SEWAGE_RESERVOIR_YN_VALUES,
),
dropdownColumn(
{
name: "oslfEtgtYn",
fieldName: "oslfEtgtYn",
header: { text: "자가발전유무(유/무)" },
},
SEWAGE_RESERVOIR_YN_VALUES,
),
// 배수 방식
{ name: 'mltvMthCn', fieldName: 'mltvMthCn', header: { text: '배수방식(자연배수,강제배수)' } },
dropdownColumn(
{
name: "mltvMthCn",
fieldName: "mltvMthCn",
header: { text: "배수방식(자연배수,강제배수)" },
},
SEWAGE_RESERVOIR_MLTV_MTH_VALUES,
),
// 저류 방식
{ name: 'impdMthCn', fieldName: 'impdMthCn', header: { text: '저류방식(직렬,병렬)' } },
dropdownColumn(
{
name: "impdMthCn",
fieldName: "impdMthCn",
header: { text: "저류방식(직렬,병렬)" },
},
SEWAGE_RESERVOIR_IMPD_MTH_VALUES,
),
// 저류수 처리방법
{ name: 'impdPrcsMthdCn', fieldName: 'impdPrcsMthdCn', header: { text: '저류수처리방법(자체처리,연계처리)' } },
dropdownColumn(
{
name: "impdPrcsMthdCn",
fieldName: "impdPrcsMthdCn",
header: { text: "저류수처리방법(자체처리,연계처리)" },
},
SEWAGE_RESERVOIR_IMPD_PRCS_MTHD_VALUES,
),
// 방류 수역
{ name: 'dschgWtrsExpln', fieldName: 'dschgWtrsExpln', header: { text: '방류수역(또는 연계처리장명)' } },
@@ -179,6 +179,29 @@ export function createSwrgInstlrtRateValueCallback(
};
}
/** 총양수량: Σ(펌프대수 + 예비펌프대수) × 대당 양수량 */
export function createPumpGrossPpqtyValueCallback(
pumpGroups: Array<{
pumpCntField: string;
rsrvPumpCntField: string;
perPpqtyField: string;
}>,
): DataFieldValueCallback {
return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => {
let total = 0;
for (const group of pumpGroups) {
const pumpIdx = fieldNamesArr.indexOf(group.pumpCntField);
const rsrvIdx = fieldNamesArr.indexOf(group.rsrvPumpCntField);
const perIdx = fieldNamesArr.indexOf(group.perPpqtyField);
const pumpCnt = pumpIdx >= 0 ? toNum(values[pumpIdx]) : 0;
const rsrvCnt = rsrvIdx >= 0 ? toNum(values[rsrvIdx]) : 0;
const perPpqty = perIdx >= 0 ? toNum(values[perIdx]) : 0;
total += (pumpCnt + rsrvCnt) * perPpqty;
}
return total;
};
}
/** 연산 타입에 맞는 valueCallback 반환 */
export function getComputedValueCallback(config: ComputedFieldConfig): DataFieldValueCallback {
switch (config.type) {
@@ -209,5 +232,7 @@ export function getComputedValueCallback(config: ComputedFieldConfig): DataField
return createCoverageRateValueCallback(config.numeratorFields, config.denominatorFields);
case 'swrgInstlrtRate':
return createSwrgInstlrtRateValueCallback(config.numeratorFields, config.denominatorFields, config.thirdFields);
case 'pumpGrossPpqty':
return createPumpGrossPpqtyValueCallback(config.pumpGroups);
}
}
+55 -5
View File
@@ -5,11 +5,61 @@ export function formatCompactYmdForGrid(ymd: string | number | null | undefined)
return `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`;
}
/** 그리드 값 → API 8자리 일자(yyyyMMdd) */
export function toCompactYmd(value: string | number | null | undefined): string {
const s = String(value ?? "").replace(/\D/g, "");
if (s.length !== 8) return "";
return s;
function isValidCompactYmdDigits(ymd: string): boolean {
if (!/^\d{8}$/.test(ymd)) return false;
const y = Number(ymd.slice(0, 4));
const m = Number(ymd.slice(4, 6));
const d = Number(ymd.slice(6, 8));
if (y < 1000 || y > 9999 || m < 1 || m > 12 || d < 1 || d > 31) return false;
const dt = new Date(y, m - 1, d);
return dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d;
}
function formatDateToCompactYmd(date: Date): string {
if (Number.isNaN(date.getTime())) return "";
const y = date.getFullYear();
const m = date.getMonth() + 1;
const d = date.getDate();
if (y < 1000 || y > 9999) return "";
return `${y}${String(m).padStart(2, "0")}${String(d).padStart(2, "0")}`;
}
/**
* 그리드·API 값 → 8자리 일자(yyyyMMdd).
* RealGrid datetime은 Date 객체로 올 수 있어, 숫자만 이어붙이는 방식은 사용하지 않습니다.
*/
export function toCompactYmd(
value: string | number | Date | null | undefined,
): string {
if (value == null || value === "") return "";
if (value instanceof Date) {
return formatDateToCompactYmd(value);
}
if (typeof value === "number" && Number.isFinite(value)) {
if (value >= 19000101 && value <= 21001231) {
const compact = String(Math.trunc(value)).padStart(8, "0").slice(-8);
return isValidCompactYmdDigits(compact) ? compact : "";
}
if (value > 1e11) {
return formatDateToCompactYmd(new Date(value));
}
}
const trimmed = String(value).trim();
const isoLike = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})/);
if (isoLike) {
const compact = `${isoLike[1]}${isoLike[2]}${isoLike[3]}`;
return isValidCompactYmdDigits(compact) ? compact : "";
}
const digits = trimmed.replace(/\D/g, "");
if (digits.length === 8 && isValidCompactYmdDigits(digits)) {
return digits;
}
return "";
}
/** 설치목적 등 O/X 표기 → 대문자 O 또는 X (그 외는 원문 유지) */
+4 -20
View File
@@ -2,30 +2,14 @@ import { useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import type { RecentPostSearchParams } from "../model/post-interface";
import type { UserPostSearchParams } from "../model/post-interface";
import { postApi } from "./post-service";
export function useRecentPostList(params?: RecentPostSearchParams) {
export function useUserPostList(params?: UserPostSearchParams) {
return useQuery({
queryKey: createRecentPostListQueryKey(params),
queryFn: () => postApi.getRecentPostList(params),
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, params],
queryFn: () => postApi.getUserPostList(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 ?? "",
];
}
@@ -1,22 +1,22 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import { ENDPOINT, ST_DASHBOARD_BBS_ID } from "@/shared/config";
import type {
PostService,
RecentPostListResponse,
RecentPostSearchParams,
UserPostListResponse,
UserPostSearchParams,
} from "../model/post-interface";
async function getRecentPostList(
params?: RecentPostSearchParams,
): Promise<RecentPostListResponse> {
const response = await client.get<RecentPostListResponse>(
`${ENDPOINT.BOARD_SERVICE.POSTS}/recentPosts`,
async function getUserPostList(
params?: UserPostSearchParams,
): Promise<UserPostListResponse> {
const response = await client.get<UserPostListResponse>(
`${ENDPOINT.BOARD_SERVICE.POSTS}/user/${ST_DASHBOARD_BBS_ID}`,
{ params },
);
return response.data;
}
export const postApi: PostService = {
getRecentPostList,
getUserPostList,
};
+2 -7
View File
@@ -1,8 +1,3 @@
export { useRecentPostList } from "./api/post-queries";
export { useUserPostList } from "./api/post-queries";
export { postApi } from "./api/post-service";
export {
type PostService,
type PstRecentListResponse,
type RecentPostListResponse,
type RecentPostSearchParams,
} from "./model/post-interface";
export { type PostService, type UserPost } from "./model/post-interface";
@@ -1,15 +1,17 @@
import type { PageableList, PageableParams } from "@/shared/model";
export interface RecentPostSearchParams extends PageableParams {
export interface UserPostSearchParams extends PageableParams {
keywordType?: string;
keyword?: string;
bbsId?: number;
delYn?: string;
frDateYmd?: string;
toDateYmd?: string;
popupYn?: string;
upndFixYn?: string;
prdPstgYn?: string;
siteId?: string;
bbsTypeCd?: string;
ansUseYn?: string;
}
export interface PstRecentListResponse {
export interface UserPost {
rowNo: number;
bbsId: number;
pstNo: number;
@@ -29,14 +31,12 @@ export interface PstRecentListResponse {
regDt: string;
rgtrNm: string;
cmntCnt: number;
bbsTypeCd: string;
bbsNm: string;
}
export type RecentPostListResponse = PageableList<PstRecentListResponse>;
export type UserPostListResponse = PageableList<UserPost>;
export interface PostService {
getRecentPostList(
params?: RecentPostSearchParams,
): Promise<RecentPostListResponse>;
getUserPostList(
params?: UserPostSearchParams,
): Promise<UserPostListResponse>;
}
@@ -5,6 +5,8 @@ import {
type StatisticsInputSewageReservoirPumpService,
type SewageReservoirApiResponse,
type SewageReservoirRequestParams,
type SewageReservoirSaveRequest,
type SewageReservoirSaveResponse,
type RainfallPumpStationApiResponse,
} from "../../model/input/statistics-input-sewage-reservoir-pump-interface";
@@ -28,6 +30,17 @@ export class StatisticsInputSewageReservoirPumpApiService implements StatisticsI
);
return response.data;
}
/** 하수저류시설 저장 — ST00040001 */
public async saveSewageReservoir(
request: SewageReservoirSaveRequest,
): Promise<SewageReservoirSaveResponse> {
const response = await stClient.put<SewageReservoirSaveResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-reservoir-pump/sewage-reservoir/save`,
request,
);
return response.data;
}
}
export const statisticsInputSewageReservoirPumpApi = new StatisticsInputSewageReservoirPumpApiService();
@@ -293,7 +293,7 @@ export class StatisticsApiService implements StatisticsService {
...(statsCd && statsCd !== "ALL" ? { statsCd } : {}),
};
const response = await stClient.get<VerificationResultListResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/results`,
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/results/input`,
{ params },
);
return response.data;
@@ -0,0 +1,42 @@
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import { stClient } from "@/shared/api/client";
import { ENDPOINT } from "@/shared/config";
import type {
VerificationInputReportRequest,
VerificationInputReportResponse,
VerificationInputReportService,
} from "../model/verification-input-report-interface";
/** 이 API 전용 헤더 키 (백엔드 스펙: `token-id`) */
const TOKEN_ID_HEADER = "token-id";
export class VerificationInputReportApiService implements VerificationInputReportService {
public async getVerificationInputReport(
param: VerificationInputReportRequest,
): Promise<VerificationInputReportResponse> {
const userInfo = await getUserInfo();
const { statsCd, page, size, ...rest } = param;
const params: VerificationInputReportRequest = {
...rest,
...(statsCd && statsCd !== "ALL" ? { statsCd } : {}),
...(page != null ? { page } : {}),
...(size != null ? { size } : {}),
};
const response = await stClient.get<VerificationInputReportResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/results/input/report`,
{
params,
headers: {
[TOKEN_ID_HEADER]: userInfo.userUnqId,
},
},
);
return response.data;
}
}
export const verificationInputReportApi = new VerificationInputReportApiService();
@@ -28,7 +28,7 @@ export interface SewageReservoirCommonInfo {
/**
* 하수저류시설 조회 `data.sewageReservoirList[]` 한 행.
* `SewageReservoirInpColumn` fieldName과 동일 (ctpvNm은 commonInfo에서 그리드에 합침).
* `SewageReservoirInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명).
*/
export interface SewageReservoirItem {
admdstCd: string;
@@ -166,9 +166,38 @@ export interface SewageReservoirRequestParams {
fcltNm?: string;
}
/**
* 하수저류시설(ST00040001) 저장 — `saveList` / `deleteList` 항목.
* `sggNm`·검증결과는 저장 요청에서 제외.
*/
export type SewageReservoirSaveItem = Omit<
SewageReservoirItem,
"sggNm" | "verificationResult"
>;
/** 하수저류시설(ST00040001) 저장 API 요청 */
export interface SewageReservoirSaveRequest {
statsYr: string;
ctpvNm: string;
sggNm: string;
sewerBzmnCd: string;
userId: string;
superAdmin?: string;
noApplicable?: boolean;
saveList: SewageReservoirSaveItem[];
deleteList: SewageReservoirSaveItem[];
}
export interface SewageReservoirSaveResponse {
status: string;
message?: string;
data?: string;
}
export interface StatisticsInputSewageReservoirPumpService {
getSewageReservoirList(params: SewageReservoirRequestParams): Promise<SewageReservoirApiResponse>;
getRainfallPumpStationList(
params: SewageReservoirRequestParams,
): Promise<RainfallPumpStationApiResponse>;
saveSewageReservoir(request: SewageReservoirSaveRequest): Promise<SewageReservoirSaveResponse>;
}
@@ -159,10 +159,13 @@ export interface VerificationResultListRequest {
ctpvNm?: string;
sggNm?: string;
statsCd?: string;
/** 검증 방법 코드 (예: IMR) */
/** 검증 방법 코드 (예: NV, IMR, HB) */
vrfcMthdCd?: string;
/** 처리여부 코드 목록, 콤마 구분 */
/** 처리여부 코드 목록, 콤마 구분 (예: 0,2) */
prcsYnList?: string;
/** 반려상태 코드 목록, 콤마 구분 (예: 0,1,2) */
rejectCodeList?: string;
/** @deprecated 신규 API에서는 미사용 */
pbcpAprvYn?: string;
page?: number;
size?: number;
@@ -172,7 +175,6 @@ export interface VerificationResultListRequest {
export interface VerificationResultReportItem {
statsYr: string;
statsCd: string;
statsNm: string;
sewerBzmnCd: string;
ctpvNm: string;
sggNm: string;
@@ -181,18 +183,22 @@ export interface VerificationResultReportItem {
vrfcMthdCd: string;
vrfcMthdNm: string;
vrfcTrgtNm: string;
regDt: string;
rsnExpln: string;
nowVlCn: string;
compVlCn: string | null;
prcsYn: string;
prcsYnNm: string;
prcsOpnnExpln: string | null;
prcrId: string | null;
stVrfRjtComCd: string;
stVrfRjtComNm: string;
rjctOpnnCn: string | null;
pbcpAprvYn: string;
mdfrId: string | null;
regDt: string;
mdfcnDt: string;
/** 하위 호환: 일부 API/화면에서만 내려올 수 있음 */
statsNm?: string;
prcrId?: string | null;
pbcpAprvYn?: string;
mdfrId?: string | null;
}
export interface VerificationResultListPageInfo {
@@ -0,0 +1,58 @@
/** 이상치 선별 보고서 조회 요청 (`GET .../results/input/report`) */
export interface VerificationInputReportRequest {
statsYr: string;
vrfcMthdCd: string;
statsCd?: string;
page?: number;
size?: number;
}
export interface VerificationInputReportTypeCountingInfo {
nvCount: number;
imrCount: number;
hbCount: number;
}
/** 이상치 선별 보고서 목록 한 행 */
export interface VerificationInputReportItem {
statsYr: string;
statsCd: string;
/** 통계코드명 (타이틀 표시용) */
statsNm?: string;
sewerBzmnCd: string;
ctpvNm: string;
sggNm: string | null;
vrfcSn: number;
vrfcMthdCd: string;
vrfcMthdNm: string;
vrfcTrgtNm: string;
rsnExpln: string;
nowVlCn: string;
compVlCn: string | null;
vrfcStatusNm: string;
}
export interface VerificationInputReportPageInfo {
totalElements: number;
totalPages: number;
currentPage: number;
size: number;
}
export interface VerificationInputReportData {
verifyTypeCountingInfo: VerificationInputReportTypeCountingInfo;
verifyResultList: VerificationInputReportItem[];
pageInfo: VerificationInputReportPageInfo;
}
export interface VerificationInputReportResponse {
status: string;
message?: string;
data?: VerificationInputReportData;
}
export interface VerificationInputReportService {
getVerificationInputReport(
param: VerificationInputReportRequest,
): Promise<VerificationInputReportResponse>;
}
@@ -5,8 +5,9 @@ import { useRouter } from "next/navigation";
import { createColumnHelper, getCoreRowModel } from "@tanstack/react-table";
import dayjs from "dayjs";
import { ST_DASHBOARD_BBS_ID } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import { type PstRecentListResponse, useRecentPostList } from "@/entities/post";
import { type UserPost, useUserPostList } from "@/entities/post";
import { Button } from "@common/components";
import { useDragHandle } from "../sortable-panel";
@@ -15,25 +16,23 @@ export function NoticePanel() {
const { dragHandleProps, isDragging } = useDragHandle();
const router = useRouter();
const recentPostListQuery = useRecentPostList({
const userPostListQuery = useUserPostList({
page: 0,
size: 4,
keywordType: "bbsTypeCd",
keyword: "BBSNOTICE",
});
const handleRowClick = (post: PstRecentListResponse) => {
router.push(
`/cms/${post.bbsTypeCd.toLowerCase()}/${post.bbsId}/${post.pstNo}`,
);
const handleRowClick = (post: UserPost) => {
router.push(`/cms/bbsnotice/${ST_DASHBOARD_BBS_ID}/${post.pstNo}`);
};
const handleMoreClick = () => {
router.push("/cms/bbsnotice/7");
router.push(`/cms/bbsnotice/${ST_DASHBOARD_BBS_ID}`);
};
const { tableComponent } = useDataTable({
query: recentPostListQuery,
query: userPostListQuery,
tableOptions: {
columns: noticeColumns,
getCoreRowModel: getCoreRowModel(),
@@ -52,8 +51,11 @@ export function NoticePanel() {
title="헤더를 드래그하여 패널 순서 변경"
>
<h3 className="text-2xl font-semibold">공지사항</h3>
</div>
<div className="py-2 text-right">
<Button
size="sm"
color="white"
className="!text-xl"
onClick={handleMoreClick}
>
@@ -65,7 +67,7 @@ export function NoticePanel() {
);
}
const columnHelper = createColumnHelper<PstRecentListResponse>();
const columnHelper = createColumnHelper<UserPost>();
const noticeColumns = [
columnHelper.accessor("pstTtl", {
+6 -5
View File
@@ -1,4 +1,5 @@
import { Client } from "@common/network";
import { BASE_URL, ST_SERVICE_BASE_URL } from "../config/api";
import { SITE_ID } from "../config/env";
import { setCookie } from "../lib";
@@ -6,11 +7,11 @@ import { setCookie } from "../lib";
setCookie("site-id-key", SITE_ID);
export const client = Client.getInstance({
baseURL: BASE_URL,
siteId: SITE_ID,
baseURL: BASE_URL,
siteId: SITE_ID,
});
export const stClient = Client.getInstance({
baseURL: ST_SERVICE_BASE_URL,
siteId: SITE_ID,
});
baseURL: ST_SERVICE_BASE_URL,
siteId: SITE_ID,
});
+1
View File
@@ -14,3 +14,4 @@ export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
export const ST_SERVICE_URL =
process.env.NEXT_PUBLIC_ST_SERVICE_URL || "http://localhost:8080";
export const COOKIE_DOMAIN = process.env.NEXT_PUBLIC_COOKIE_DOMAIN;
export const ST_DASHBOARD_BBS_ID = process.env.NEXT_PUBLIC_ST_DASHBOARD_BBS_ID;