[st] - 그리드 style 적용 로직 수정, 로그 조회 일정 선택 로직 수정(최대 1년 제한)
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user