[st] - 그리드 style 적용 로직 수정, 로그 조회 일정 선택 로직 수정(최대 1년 제한)

This commit is contained in:
2026-07-03 16:19:27 +09:00
parent 4c7666169c
commit f1fcfa9ee4
4 changed files with 164 additions and 29 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",
@@ -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>
@@ -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;