This commit is contained in:
2026-04-20 17:46:03 +09:00
47 changed files with 3601 additions and 937 deletions
+111 -52
View File
@@ -311,6 +311,7 @@ function buildFieldsFromColumns(
const DataGrid = ({
columnInfo,
rowStartNum,
excelExportFileSuffix,
isExcelImport = true,
isExcelExport = true,
fixedColumns,
@@ -321,10 +322,14 @@ const DataGrid = ({
cellStyleCallback,
cellTooltipCallback,
onRowClick,
onItemChecked,
onItemAllChecked,
onScrollToBottom,
statusInfo,
highlightedRowId,
idFieldName = "id",
enableSort = false,
gridHeight = 65, // vh 단위
}: DataGridProps) => {
const { columns, columnLayout } = columnInfo;
const gridRef = useRef<RealGridReact>(null);
@@ -340,9 +345,18 @@ const DataGrid = ({
const cellTooltipCallbackRef = useRef<CellTooltipCallback | undefined>(
cellTooltipCallback,
);
const onScrollToBottomRef = useRef<(() => void) | undefined>(
onScrollToBottom,
);
const onItemCheckedRef = useRef<typeof onItemChecked>(onItemChecked);
const onItemAllCheckedRef =
useRef<typeof onItemAllChecked>(onItemAllChecked);
rowDataRef.current = rowData;
cellStyleCallbackRef.current = cellStyleCallback;
cellTooltipCallbackRef.current = cellTooltipCallback;
onScrollToBottomRef.current = onScrollToBottom;
onItemCheckedRef.current = onItemChecked;
onItemAllCheckedRef.current = onItemAllChecked;
// highlightedRowId가 변경될 때 해당 행으로 스크롤 및 선택
useEffect(() => {
@@ -470,10 +484,14 @@ const DataGrid = ({
gridView.setColumnLayout?.(normalizedLayout);
}
const exportBaseName = excelExportFileSuffix
? `${menuName}_${excelExportFileSuffix}`
: menuName;
gridView.exportGrid({
type: "excel",
target: "local",
fileName: `${menuName}.xlsx`,
fileName: `${exportBaseName}.xlsx`,
footer: "hidden",
allColumns: true, // 숨김(visible: false) 컬럼도 엑셀에 포함
done: () => {
@@ -558,10 +576,10 @@ const DataGrid = ({
) {
const cell =
worksheet[
XLSX.utils.encode_cell({
r: rowIndex,
c: col,
})
XLSX.utils.encode_cell({
r: rowIndex,
c: col,
})
];
if (
cell &&
@@ -637,7 +655,7 @@ const DataGrid = ({
const col = range.s.c + colIndex;
const cell =
worksheet[
XLSX.utils.encode_cell({ r: row, c: col })
XLSX.utils.encode_cell({ r: row, c: col })
];
const headerName =
headers[colIndex] || `Column${colIndex + 1}`;
@@ -761,13 +779,13 @@ const DataGrid = ({
onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean;
}
).onSorting = enableSort
? createOnSortingHandler(
columns,
setRowData,
sortDirRef,
originalRowsRef,
)
: () => false;
? createOnSortingHandler(
columns,
setRowData,
sortDirRef,
originalRowsRef,
)
: () => false;
(gridView.displayOptions as DisplayOptions).fitStyle =
GridFitStyle.EVEN;
@@ -937,7 +955,7 @@ const DataGrid = ({
) {
(
column.footer[
index
index
] as FooterItemWithCallback
).valueCallback =
footerItem.valueCallback;
@@ -990,11 +1008,11 @@ const DataGrid = ({
getGridDataRef.current = () => {
const gv = gridRef.current?.gridView as
| {
getDataSource?: () => {
getRowCount(): number;
getValue(r: number, f: string): unknown;
};
}
getDataSource?: () => {
getRowCount(): number;
getValue(r: number, f: string): unknown;
};
}
| undefined;
if (!gv?.getDataSource) return [];
const ds = gv.getDataSource();
@@ -1090,6 +1108,47 @@ const DataGrid = ({
};
}, [onRowClick, cellTooltipCallback, columns]);
useEffect(() => {
const gridView = gridRef.current?.gridView;
if (!gridView || !onScrollToBottom) return;
gridView.onScrollToBottom = () => {
onScrollToBottomRef.current?.();
};
return () => {
if (gridView.onScrollToBottom)
gridView.onScrollToBottom = undefined;
};
}, [onScrollToBottom]);
useEffect(() => {
const gridView = gridRef.current?.gridView;
if (!gridView) return;
if (onItemChecked) {
gridView.onItemChecked = (_grid, itemIndex, checked) => {
onItemCheckedRef.current?.(itemIndex, checked);
};
} else {
gridView.onItemChecked = undefined;
}
if (onItemAllChecked) {
gridView.onItemAllChecked = (_grid, checked) => {
onItemAllCheckedRef.current?.(checked);
};
} else {
gridView.onItemAllChecked = undefined;
}
return () => {
if (gridView.onItemChecked) gridView.onItemChecked = undefined;
if (gridView.onItemAllChecked)
gridView.onItemAllChecked = undefined;
};
}, [onItemChecked, onItemAllChecked]);
return (
<div className="mx-[0.6rem] flex flex-col gap-[0.6rem]">
<input
@@ -1105,23 +1164,23 @@ const DataGrid = ({
<span>
진행상황:{" "}
{statusInfo.laeSbmsnSttsCd === null ||
statusInfo.laeSbmsnSttsCd === "0"
statusInfo.laeSbmsnSttsCd === "0"
? "미작성"
: statusInfo.laeSbmsnSttsCd === "1"
? "작성중"
: statusInfo.laeSbmsnSttsCd === "2"
? "제출"
: statusInfo.laeSbmsnSttsCd === "3"
? "승인"
: statusInfo.laeSbmsnSttsCd === "4"
? "반려"
: "검증중"}
? "작성중"
: statusInfo.laeSbmsnSttsCd === "2"
? "제출"
: statusInfo.laeSbmsnSttsCd === "3"
? "승인"
: statusInfo.laeSbmsnSttsCd === "4"
? "반려"
: "검증중"}
</span>
<span>
제출일:{" "}
{statusInfo.laeSbmsnYmd
? statusInfo.laeSbmsnYmd.length === 8 &&
/^\d{8}$/.test(statusInfo.laeSbmsnYmd)
/^\d{8}$/.test(statusInfo.laeSbmsnYmd)
? `${statusInfo.laeSbmsnYmd.slice(0, 4)}-${statusInfo.laeSbmsnYmd.slice(4, 6)}-${statusInfo.laeSbmsnYmd.slice(6, 8)}`
: statusInfo.laeSbmsnYmd
: "미제출"}
@@ -1159,7 +1218,7 @@ const DataGrid = ({
</Button>
)}
</div>
<div className="h-[65vh]">
<div style={{ height: `${gridHeight}vh` }}>
<RealGridReact
ref={gridRef}
autoGenerateField={true}
@@ -1176,24 +1235,24 @@ const DataGrid = ({
) => boolean;
}
).onSorting = enableSort
? createOnSortingHandler(
columns,
setRowData,
sortDirRef,
originalRowsRef,
)
: () => false;
? createOnSortingHandler(
columns,
setRowData,
sortDirRef,
originalRowsRef,
)
: () => false;
const dataProvider = gridRef.current
?.dataProvider as unknown as
| {
setFields?(f: unknown[]): void;
setRows?(
r: Record<string, string | number>[],
): void;
getJsonRow?(
r: number,
): Record<string, unknown>;
}
setFields?(f: unknown[]): void;
setRows?(
r: Record<string, string | number>[],
): void;
getJsonRow?(
r: number,
): Record<string, unknown>;
}
| undefined;
if (dataProvider?.setFields) {
dataProvider.setFields(
@@ -1346,14 +1405,14 @@ const DataGrid = ({
})}
footer={
column.footer &&
!Array.isArray(column.footer)
!Array.isArray(column.footer)
? {
text: column.footer.text,
expression:
column.footer.expression,
numberFormat:
column.footer.numberFormat,
}
text: column.footer.text,
expression:
column.footer.expression,
numberFormat:
column.footer.numberFormat,
}
: undefined
}
mergeRule={
+49 -29
View File
@@ -1,52 +1,66 @@
"use client";
import { useState, useEffect } from "react";
import { useEffect, useState } from "react";
import useSWR from "swr";
import {
Button,
Input,
Label,
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
Checkbox,
} from "@common/components";
import { useAlertStore } from "@/shared/lib";
import {
type SearchFormProps,
type SearchFormValues,
defaultSearchFormProps,
} from "../type/SearchForm.type";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsAPi } from "@/entities/statistics/api/statistics-service";
import type {
ApiResponse,
SearchCondition,
Sgg,
} from "@/entities/statistics/model/statistics-interface";
import {
Button,
Checkbox,
Input,
Label,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@common/components";
const getSidoListFetcher = async (): Promise<ApiResponse<SearchCondition>> => {
return statisticsAPi.getSidoList();
import {
defaultSearchFormProps,
type SearchFormProps,
type SearchFormValues,
} from "../type/SearchForm.type";
import { doList } from "../util/column/input/DoList";
const getSidoListFetcher = async (
key: [string, string],
): Promise<ApiResponse<SearchCondition>> => {
return statisticsAPi.getSidoList({ instSeCd: key[1] });
};
const getSggListFetcher = async (
key: [string, string],
key: [string, string, string, string],
): Promise<ApiResponse<Sgg[]>> => {
return statisticsAPi.getSggList(key[1]);
const gugunSkipYn = doList.includes(key[3]) ? "N" : "Y";
return statisticsAPi.getSggAllList(key[1], {
instSeCd: key[2],
gugunSkipYn,
});
};
const SearchForm = (props: SearchFormProps = {}) => {
const { setMessage: alert } = useAlertStore((state) => state);
const userInfo = useUserStore((state) => state.userInfo);
const mergedProps = { ...defaultSearchFormProps, ...props };
const instSeCd = userInfo?.instSeCd ?? "";
const sidoSelectText =
props.sidoSelectText && props.sidoSelectText.trim() !== ""
? props.sidoSelectText
: "선택";
const currentYear = new Date().getFullYear();
const defaultYear = currentYear - 1;
const { data: sidoList } = useSWR<ApiResponse<SearchCondition>>(
"sido-list",
["sido-list", instSeCd],
getSidoListFetcher,
{
revalidateOnFocus: false,
@@ -77,8 +91,10 @@ const SearchForm = (props: SearchFormProps = {}) => {
isDetail: false,
});
const sggKey: [string, string] | null =
values.sido && values.sido !== "ALL" ? ["sgg-list", values.sido] : null;
const sggKey: [string, string, string, string] | null =
values.sido && values.sido !== "ALL"
? ["sgg-list", values.sido, instSeCd, values.bsnmCd || ""]
: null;
const { data: sggList } = useSWR<ApiResponse<Sgg[]>>(
sggKey,
getSggListFetcher,
@@ -102,7 +118,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
);
const sggOptions = sggList?.data ?? [];
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
props.onSubmit?.(values);
};
@@ -163,10 +179,14 @@ const SearchForm = (props: SearchFormProps = {}) => {
onValueChange={handleSidoChange}
>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder="선택"></SelectValue>
<SelectValue
placeholder={sidoSelectText}
></SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL">선택</SelectItem>
<SelectItem value="ALL">
{sidoSelectText}
</SelectItem>
{sidoList?.data?.sidoList?.map((sido) => (
<SelectItem
key={sido.adstDstrCd}