Merge remote-tracking branch 'origin/iipbs/st/dev'

This commit is contained in:
2026-07-05 10:58:48 +09:00
12 changed files with 242 additions and 84 deletions
+24 -1
View File
@@ -61,6 +61,25 @@ type GridColumnWithStyleCallback = {
) => CellStyleCallbackResult;
};
function mergeGridRowsWithSourceMetadata(
rows: Record<string, string | number>[],
sourceRows: Record<string, string | number>[],
columnFieldNames: string[],
): Record<string, string | number>[] {
const columnFieldSet = new Set(columnFieldNames);
return rows.map((row, index) => {
const source = sourceRows[index];
if (!source) return row;
const merged: Record<string, string | number> = { ...row };
for (const [key, value] of Object.entries(source)) {
if (columnFieldSet.has(key) || value === undefined) continue;
merged[key] = value as string | number;
}
return merged;
});
}
function applyCellStyleCallbacksToColumns(
gridView: unknown,
columns: ColumnConfig[],
@@ -1620,7 +1639,11 @@ useEffect(() => {
r[f] = ds.getValue(i, f) as string | number;
rows.push(r);
}
return rows;
return mergeGridRowsWithSourceMetadata(
rows,
rowDataRef.current,
fieldNames,
);
};
}
@@ -18,8 +18,6 @@ const StatisticsReviewHistory = () => {
{ name: "statsYr", fieldName: "statsYr", header: { text: "연도" }, editable: false },
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
{ name: "sewerBzmnCd", fieldName: "sewerBzmnCd", header: { text: "하수사업자코드" }, editable: false },
{ name: "statsCd", fieldName: "statsCd", header: { text: "통계코드" }, editable: false },
{ name: "menuNm", fieldName: "menuNm", header: { text: "통계항목" }, editable: false },
{ name: "chckerOpinionSm", fieldName: "chckerOpinionSm", header: { text: "검토자의견" }, editable: false },
{ name: "wrterOpinionSm", fieldName: "wrterOpinionSm", header: { text: "처리내역" }, editable: false },
@@ -29,8 +27,6 @@ const StatisticsReviewHistory = () => {
"statsYr",
"ctpvNm",
"sggNm",
"sewerBzmnCd",
"statsCd",
"menuNm",
"chckerOpinionSm",
"wrterOpinionSm",
@@ -55,7 +55,16 @@ const AdmdstRenewalTab = ({ tab, statsYr }: Props) => {
추가된 행정구역 자료
</span>
</div>
<div className="flex justify-end pr-[1rem] mb-[0.4rem]">
<div className="flex justify-end pr-[1rem] mb-[0.4rem] gap-4">
<Button
type="button"
color="red"
disabled={tab.isLoading}
onClick={() => void tab.handleRefresh()}
className="mt-[0.5rem] px-[4rem] py-[0.8rem] text-[1.4rem]"
>
갱신
</Button>
<Button
type="button"
color="primary"
@@ -3,6 +3,7 @@
import { useCallback, useEffect, useState } from "react";
import dayjs from "dayjs";
import { CalendarIcon } from "lucide-react";
import useSWR from "swr";
import { useAlertStore } from "@/shared/lib";
@@ -17,9 +18,13 @@ import type {
} from "@/entities/statistics/model/statistics-interface";
import {
Button,
InputDatePicker,
Calendar,
Input,
Label,
LoadingWithAnimation,
Popover,
PopoverContent,
PopoverTrigger,
Select,
SelectContent,
SelectItem,
@@ -34,6 +39,91 @@ import { LogHistoryColumn } from "@/components/util/column/management/LogHistory
import { toGridRows } from "@/components/util/grid/gridFunction";
const PAGE_SIZE = 100;
const MAX_DATE_RANGE_YEARS = 1;
const isWithinMaxDateRange = (start: Date, end: Date) => {
const startDay = dayjs(start).startOf("day");
const endDay = dayjs(end).startOf("day");
return (
!endDay.isBefore(startDay, "day") &&
!endDay.isAfter(startDay.add(MAX_DATE_RANGE_YEARS, "year"), "day")
);
};
type LogHistoryDateFieldProps = {
value: Date;
onChange: (date: Date) => void;
className?: string;
};
/** 로그 이력 전용: 날짜를 비울 수 없는 date input */
const LogHistoryDateField = ({
value,
onChange,
className,
}: LogHistoryDateFieldProps) => {
const [open, setOpen] = useState(false);
const [month, setMonth] = useState(value);
const dateValue = dayjs(value).format("YYYY-MM-DD");
useEffect(() => {
setMonth(value);
}, [value]);
return (
<div className="relative flex gap-2">
<Input
type="date"
value={dateValue}
className={`input-date-picker bg-background pr-10 ${className ?? ""}`}
onChange={(e) => {
if (!e.target.value) return;
const next = dayjs(e.target.value);
if (!next.isValid()) return;
onChange(next.toDate());
}}
onKeyDown={(e) => {
if (e.key === "ArrowDown") {
e.preventDefault();
setOpen(true);
}
}}
/>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
type="button"
variant="ghost"
color="white"
className="absolute right-2 top-1/2 size-7 -translate-y-1/2 rounded-[0.4rem] border-none p-0"
>
<CalendarIcon className="!size-full" />
<span className="sr-only">날짜 선택</span>
</Button>
</PopoverTrigger>
<PopoverContent
className="w-auto overflow-hidden p-0"
align="end"
alignOffset={-8}
sideOffset={10}
>
<Calendar
mode="single"
selected={value}
captionLayout="dropdown"
month={month}
onMonthChange={setMonth}
onSelect={(newDate) => {
if (!newDate) return;
onChange(newDate);
setOpen(false);
}}
/>
</PopoverContent>
</Popover>
</div>
);
};
const getSidoListFetcher = async (): Promise<ApiResponse<SearchCondition>> => {
return statisticsApi.getSidoList();
@@ -63,13 +153,45 @@ const LogHistory = () => {
string,
string | number
> | null>(null);
const [startDate, setStartDate] = useState<Date | undefined>(() =>
const [startDate, setStartDate] = useState<Date>(() =>
dayjs().subtract(1, "month").startOf("day").toDate(),
);
const [endDate, setEndDate] = useState<Date | undefined>(() =>
const [endDate, setEndDate] = useState<Date>(() =>
dayjs().endOf("day").toDate(),
);
const handleStartDateChange = (date: Date) => {
const next = dayjs(date).startOf("day");
const end = dayjs(endDate).startOf("day");
if (next.isAfter(end, "day")) {
alert("시작일은 종료일보다 늦을 수 없습니다.");
return;
}
if (next.isBefore(end.subtract(MAX_DATE_RANGE_YEARS, "year"), "day")) {
alert("조회 기간은 최대 1년까지 설정할 수 있습니다.");
return;
}
setStartDate(next.toDate());
};
const handleEndDateChange = (date: Date) => {
const next = dayjs(date).startOf("day");
const start = dayjs(startDate).startOf("day");
if (next.isBefore(start, "day")) {
alert("종료일은 시작일보다 빠를 수 없습니다.");
return;
}
if (next.isAfter(start.add(MAX_DATE_RANGE_YEARS, "year"), "day")) {
alert("조회 기간은 최대 1년까지 설정할 수 있습니다.");
return;
}
setEndDate(next.toDate());
};
const { data: sidoList } = useSWR<ApiResponse<SearchCondition>>(
"log-history-sido-list",
getSidoListFetcher,
@@ -141,10 +263,8 @@ const LogHistory = () => {
const param: StatisticsLogRequest = {
page: page1Based,
size: PAGE_SIZE,
startDate: startDate
? dayjs(startDate).format("YYYY-MM-DD")
: "",
endDate: endDate ? dayjs(endDate).format("YYYY-MM-DD") : "",
startDate: dayjs(startDate).format("YYYY-MM-DD"),
endDate: dayjs(endDate).format("YYYY-MM-DD"),
...(!(statsCd === "" || statsCd === "ALL") && { statsCd }),
...(sido && sido !== "ALL" && { ctpvNm: sido }),
...(gugun && gugun !== "ALL" && { sggNm: gugun }),
@@ -199,17 +319,15 @@ 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;
}
const start = dayjs(startDate);
const end = dayjs(endDate);
if (end.isBefore(start, "day")) {
alert("종료일은 시작일보다 빠를 수 없습니다.");
return;
}
if (!isWithinMaxDateRange(startDate, endDate)) {
alert("조회 기간은 최대 1년까지 설정할 수 있습니다.");
return;
}
await loadLogs(1);
@@ -310,15 +428,15 @@ const LogHistory = () => {
</div>
<div className="flex items-center gap-2">
<Label className="mx-2 text-label">시작일</Label>
<InputDatePicker
<LogHistoryDateField
value={startDate}
onChange={setStartDate}
onChange={handleStartDateChange}
className="w-[14rem]"
/>
<Label className="mx-2 text-label">종료일</Label>
<InputDatePicker
<LogHistoryDateField
value={endDate}
onChange={setEndDate}
onChange={handleEndDateChange}
className="w-[14rem]"
/>
</div>
@@ -60,6 +60,7 @@ export function useAdmdstRenewalTab() {
return;
}
await run(async () => {
const userInfo = await getUserInfo();
const addedList: AdmdstRenewalAddedSaveItem[] = addedRowData
@@ -79,13 +80,12 @@ const addedList: AdmdstRenewalAddedSaveItem[] = addedRowData
userId: userInfo.userUnqId,
}));
const reducedList: AdmdstRenewalReducedSaveItem[] = reducedRowData.map((row) => ({
adstCd: String(row.adstCd ?? ""),
}));
console.log("reducedList:", reducedList);
console.log("addedList:", addedList);
console.log("reducedList 이상한 것:", reducedList.filter(r => r.adstCd.length !== 10));
const res = await statisticsPpltnApi.saveChangedArea(reducedList, addedList);
if (res.status === "success") {
await loadAll(statsYr);
@@ -110,8 +110,6 @@ console.log("reducedList 이상한 것:", reducedList.filter(r => r.adstCd.lengt
if (res.status === "success" && Array.isArray(res.data) && res.data.length > 0) {
setCandidates(res.data);
const rowIndex = addedRowData.findIndex((r) => r.adstCd === rowData.adstCd);
console.log("rowData.adstCd:", rowData.adstCd); // ← 여기
console.log("rowIndex:", rowIndex); // ← 여기
selectedRowIndexRef.current = rowIndex;
setDialogOpen(true);
} else {
@@ -131,9 +129,20 @@ console.log("reducedList 이상한 것:", reducedList.filter(r => r.adstCd.lengt
: row,
),
);
selectedRowIndexRef.current = null;
};
const handleRefresh = async () => {
await run(async () => {
const res = await statisticsPpltnApi.refreshMaterializedViews();
if (res.status === "success") {
alert("뷰 갱신이 완료되었습니다.");
} else {
alert("뷰 갱신 중 오류가 발생했습니다.");
}
});
};
const reset = () => {
setReducedRowData([]);
setAddedRowData([]);
@@ -156,6 +165,7 @@ console.log("reducedList 이상한 것:", reducedList.filter(r => r.adstCd.lengt
handleFinal,
handleRowClick,
handleCodeSelect,
handleRefresh,
reset,
};
}
@@ -813,8 +813,6 @@ const rawSewagePipeTotList = [
{ name: "yy25Step3MnhlCnt", fieldName: "yy25Step3MnhlCnt", header: { text: "콘크리트" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy25Step4MnhlCnt", fieldName: "yy25Step4MnhlCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy25Step5MnhlCnt", fieldName: "yy25Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "rmrk", fieldName: "rmrk", header: { text: "비고" }, editable: false },
],
columnLayout: [
{ name: "addrGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm"] },
@@ -922,8 +920,7 @@ const rawSewagePipeTotList = [
{ name: "yy25MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy25WholMnhlCnt", "yy25Step1MnhlCnt", "yy25Step2MnhlCnt", "yy25Step3MnhlCnt", "yy25Step4MnhlCnt"] },
"yy25Step5MnhlCnt",
],
},
"rmrk",
}
],
},
@@ -135,10 +135,7 @@ export const SeweragePentrationRateTotColumn = {
valueType: ValueType.NUMBER,
dataFormat: '#,##0.0',
excludeFromExcelExport: true,
},
// 비고
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
}
].map((col) => ({ ...col, editable: false })),
columnLayout: [
@@ -200,7 +197,6 @@ export const SeweragePentrationRateTotColumn = {
'staaPsplyRt',
'altdPrcsPsplyRt',
'sewerInstlRt',
'rurPsplyRt',
'rmrk',
'rurPsplyRt'
]
};
@@ -4,7 +4,7 @@ export const PrivateSewageTab2ColumnConfig = {
tabKey: "ST00080002",
tabLabel: "정화조현황",
rowStartNum: 4,
fixedColumns: 3,
fixedColumns: 2,
columns: [
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
@@ -9,34 +9,36 @@ export const DigestionTankTab2ColumnConfig = {
{ 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: "fcltNm", fieldName: "fcltNm", header: { text: "하수\n처리장명" }, editable: false },
{ name: "digesterYn", fieldName: "digesterYn", header: { text: "소화조 유/무\n(Y/N)선택" }, editable: false },
{ name: "dsgnCpct", fieldName: "dsgnCpct", header: { text: "설계\n용량(㎥)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "actlOperCpct", fieldName: "actlOperCpct", header: { text: "실제운전\n용량(㎥)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "inputQnt", fieldName: "inputQnt", header: { text: "투입량\n(㎥/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
{ name: "tsConc", fieldName: "tsConc", header: { text: "투입 TS 농도\n(%)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
{ name: "oprtngDays", fieldName: "oprtngDays", header: { text: "가동일수\n(연간)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "gnrtGasQnt", fieldName: "gnrtGasQnt", header: { text: "발생가스량\n(㎥/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
{ name: "swtrmtFcltNm", fieldName: "swtrmtFcltNm", header: { text: "하수\n처리장명" }, editable: false },
{ name: "digstnEnYn", fieldName: "digstnEnYn", header: { text: "소화조 유/무\n(Y/N)선택" }, editable: false },
{ name: "digstnCpct", fieldName: "digstnCpct", header: { text: "설계\n용량(㎥)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "actlDrvnCpct", fieldName: "actlDrvnCpct", header: { text: "실제운전\n용량(㎥)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "digstnGasInputQnt", fieldName: "digstnGasInputQnt", header: { text: "투입량\n(㎥/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
{ name: "digstnGasInputSldDnst", fieldName: "digstnGasInputSldDnst", header: { text: "투입 TS 농도\n(%)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
{ name: "digstnTnkOprtngDayCnt", fieldName: "digstnTnkOprtngDayCnt", header: { text: "가동일수\n(연간)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "ocrnGasQnt", fieldName: "ocrnGasQnt", header: { text: "발생가스량\n(㎥/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
// 소화가스 이용현황(하위 항목)
{ name: "gasTotal", fieldName: "gasTotal", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
{ name: "gasSale", fieldName: "gasSale", header: { text: "판매" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
{ name: "gasGen", fieldName: "gasGen", header: { text: "발전" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
{ name: "gasSelf", fieldName: "gasSelf", header: { text: "자체활용" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
{ name: "gasEtc", fieldName: "gasEtc", header: { text: "기타(CNG등)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
{ name: "surplusGas", fieldName: "surplusGas", header: { text: "잉여가스량\n(㎥/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }
{ name: "digstnGasUtztnWholPrst", fieldName: "digstnGasUtztnWholPrst", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
{ name: "digstnGasUtztnNtslQnt", fieldName: "digstnGasUtztnNtslQnt", header: { text: "판매" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
{ name: "digstnGasUtztnEtgtQnt", fieldName: "digstnGasUtztnEtgtQnt", header: { text: "발전" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
{ name: "digstnGasUtztnItslfQnt", fieldName: "digstnGasUtztnItslfQnt", header: { text: "자체활용" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
{ name: "digstnGasUtztnEtcQnt", fieldName: "digstnGasUtztnEtcQnt", header: { text: "기타(CNG등)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
{ name: "surplsGasQnt", fieldName: "surplsGasQnt", header: { text: "잉여가스량\n(㎥/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }
],
columnLayout: [
{ name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] },
"fcltNm",
{ name: "operGroup", header: { text: "소화조 운영현황" }, items: ["digesterYn", "dsgnCpct", "actlOperCpct", "inputQnt", "tsConc", "oprtngDays", "gnrtGasQnt",
{
name: "gasUsageGroup",
header: { text: "소화가스 이용현황(㎥/일)" },
items: ["gasTotal", "gasSale", "gasGen", "gasSelf", "gasEtc"]
},"surplusGas"
"swtrmtFcltNm",
{ name: "operGroup", header: { text: "소화조 운영현황" }, items: [
"digstnEnYn", "digstnCpct", "actlDrvnCpct", "digstnGasInputQnt", "digstnGasInputSldDnst", "digstnTnkOprtngDayCnt", "ocrnGasQnt",
{
name: "gasUsageGroup",
header: { text: "소화가스 이용현황(㎥/일)" },
items: ["digstnGasUtztnWholPrst", "digstnGasUtztnNtslQnt", "digstnGasUtztnEtgtQnt", "digstnGasUtztnItslfQnt", "digstnGasUtztnEtcQnt"]
},
"surplsGasQnt"
] },
]
};
@@ -235,6 +235,14 @@ public async getLawCdsByName(
);
return response.data;
}
/** 행정동 갱신 — MATERIALIZED VIEW 갱신 */
public async refreshMaterializedViews(): Promise<{ status: string; message?: string }> {
const response = await stClient.post<{ status: string; message?: string }>(
`${ENDPOINT.ST_SERVICE.PPLTN}/admdst-renewal/refresh`,
);
return response.data;
}
}
@@ -463,8 +463,6 @@ export interface StatiscicsManagementReviewHistoryItem {
statsYr: string;
ctpvNm: string;
sggNm: string;
sewerBzmnCd: string;
statsCd: string;
menuNm: string;
wrterOpinionSm: string | null;
chckerOpinionSm: string | null;
@@ -101,21 +101,22 @@ export interface DigestionTankOperStatusTotalItem {
ctpvNm: string;
sggNm: string;
admdstNm: string;
admdstCd: string;
rowType: string;
fcltNm: string;
digesterYn: string;
dsgnCpct: number;
actlOperCpct: number;
inputQnt: number;
tsConc: number;
oprtngDays: number;
gnrtGasQnt: number;
gasTotal: number;
gasSale: number;
gasGen: number;
gasSelf: number;
gasEtc: number;
surplusGas: number;
swtrmtFcltNm: string;
digstnEnYn: string;
digstnCpct: number;
actlDrvnCpct: number;
digstnGasInputQnt: number;
digstnGasInputSldDnst: number;
digstnTnkOprtngDayCnt: number;
ocrnGasQnt: number;
digstnGasUtztnWholPrst: number;
digstnGasUtztnNtslQnt: number;
digstnGasUtztnEtgtQnt: number;
digstnGasUtztnItslfQnt: number;
digstnGasUtztnEtcQnt: number;
surplsGasQnt: number;
}
export interface DigestionTankOperTab2Response { status: string; message?: string; data: DigestionTankOperStatusTotalItem[]; }