st: feat: 진행상황 정보 표시
This commit is contained in:
@@ -21,6 +21,18 @@
|
||||
@apply !bg-[#e3eaf2];
|
||||
}
|
||||
|
||||
/* 진행상황: 소프트 인라인 요약 (툴바와 자연스럽게 blend) */
|
||||
.data-grid-status-bubble {
|
||||
@apply rounded-lg border border-slate-200/80 bg-slate-50/90 px-4 py-2.5;
|
||||
box-shadow: none;
|
||||
}
|
||||
.data-grid-status-bubble span {
|
||||
@apply text-base text-slate-700;
|
||||
}
|
||||
.data-grid-status-bubble span + span {
|
||||
@apply ml-4 border-l border-slate-200/70 pl-4;
|
||||
}
|
||||
|
||||
[data-theme="black"] {
|
||||
.rg-grid .rg-header .rg-table .rg-header-group-cell {
|
||||
@apply !bg-[#3C4043] !text-[#fff];
|
||||
|
||||
@@ -86,110 +86,6 @@ function toNum(v: unknown): number {
|
||||
return parseFloat(String(v).replace(/,/g, "")) || NaN;
|
||||
}
|
||||
|
||||
/** 헤더에 정렬 화살표 표시 (CSS 방식) */
|
||||
function updateHeaderSortIndicator(
|
||||
gridView: {
|
||||
columnByIndex?: (
|
||||
i: number,
|
||||
) => { fieldName?: string; name?: string } | undefined;
|
||||
columnByName?: (
|
||||
name: string,
|
||||
) => { fieldName?: string; name?: string } | undefined;
|
||||
getColumn?: (
|
||||
i: number,
|
||||
) => { fieldName?: string; name?: string } | undefined;
|
||||
},
|
||||
columns: { fieldName?: string; name: string }[],
|
||||
sortDir: Record<string, "asc" | "desc" | "none">,
|
||||
) {
|
||||
try {
|
||||
columns.forEach((col) => {
|
||||
if (!col.fieldName && !col.name) return;
|
||||
|
||||
// 컬럼 찾기
|
||||
let gridCol: { fieldName?: string; name?: string } | undefined;
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const c =
|
||||
gridView.columnByIndex?.(i) ?? gridView.getColumn?.(i);
|
||||
if (!c) break;
|
||||
if (
|
||||
c.fieldName === (col.fieldName || col.name) ||
|
||||
c.name === (col.fieldName || col.name)
|
||||
) {
|
||||
gridCol = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!gridCol) {
|
||||
gridCol = gridView.columnByName?.(col.name);
|
||||
}
|
||||
|
||||
if (!gridCol) return;
|
||||
|
||||
const fieldName =
|
||||
gridCol.fieldName || gridCol.name || col.fieldName || col.name;
|
||||
const direction = sortDir[fieldName] || sortDir[col.name] || "none";
|
||||
|
||||
// DOM에서 헤더 요소 찾기
|
||||
const gridEl1 = document.querySelector(
|
||||
"[data-grid-id]",
|
||||
) as HTMLElement | null;
|
||||
const gridEl2 = document.querySelector(
|
||||
".rg-grid",
|
||||
) as HTMLElement | null;
|
||||
const gridEl = gridEl1 || gridEl2;
|
||||
if (!gridEl) return;
|
||||
|
||||
// 헤더 셀 찾기 (fieldName 또는 name으로)
|
||||
const headerCells = gridEl.querySelectorAll(
|
||||
'.rg-header-cell, [class*="header"]',
|
||||
);
|
||||
let targetCell: HTMLElement | null = null;
|
||||
|
||||
headerCells.forEach((cell) => {
|
||||
const el = cell as HTMLElement;
|
||||
const text = el.textContent?.trim();
|
||||
const colName = col.name;
|
||||
const colFieldName = col.fieldName;
|
||||
if (
|
||||
text &&
|
||||
(text.includes(colName) ||
|
||||
(colFieldName && text.includes(colFieldName)))
|
||||
) {
|
||||
targetCell = el;
|
||||
}
|
||||
});
|
||||
|
||||
if (!targetCell) return;
|
||||
|
||||
const headerCell = targetCell as HTMLElement;
|
||||
|
||||
// 기존 화살표 제거
|
||||
const existingIndicator = headerCell.querySelector(
|
||||
".rg-sort-indicator",
|
||||
) as HTMLElement | null;
|
||||
if (existingIndicator) {
|
||||
existingIndicator.remove();
|
||||
}
|
||||
|
||||
// 화살표 추가
|
||||
if (direction === "asc" || direction === "desc") {
|
||||
const indicator = document.createElement("span");
|
||||
indicator.className = "rg-sort-indicator";
|
||||
indicator.textContent = direction === "asc" ? "▲" : "▼";
|
||||
indicator.style.marginLeft = "4px";
|
||||
indicator.style.fontSize = "10px";
|
||||
indicator.style.color = "#666";
|
||||
indicator.style.display = "inline-block";
|
||||
indicator.style.verticalAlign = "middle";
|
||||
headerCell.appendChild(indicator);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn("updateHeaderSortIndicator error:", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** onSorting: mergeCells: true인 컬럼이 있으면 그룹 내 순서 유지 정렬, 없으면 RealGrid 기본 정렬 */
|
||||
function createOnSortingHandler(
|
||||
columns: {
|
||||
@@ -416,6 +312,7 @@ const DataGrid = ({
|
||||
getGridDataRef,
|
||||
cellStyleCallback,
|
||||
onRowClick,
|
||||
statusInfo,
|
||||
}: DataGridProps) => {
|
||||
const { columns, columnLayout } = columnInfo;
|
||||
const gridRef = useRef<RealGridReact>(null);
|
||||
@@ -624,10 +521,10 @@ const DataGrid = ({
|
||||
) {
|
||||
const cell =
|
||||
worksheet[
|
||||
XLSX.utils.encode_cell({
|
||||
r: rowIndex,
|
||||
c: col,
|
||||
})
|
||||
XLSX.utils.encode_cell({
|
||||
r: rowIndex,
|
||||
c: col,
|
||||
})
|
||||
];
|
||||
if (
|
||||
cell &&
|
||||
@@ -703,7 +600,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}`;
|
||||
@@ -999,7 +896,7 @@ const DataGrid = ({
|
||||
) {
|
||||
(
|
||||
column.footer[
|
||||
index
|
||||
index
|
||||
] as FooterItemWithCallback
|
||||
).valueCallback =
|
||||
footerItem.valueCallback;
|
||||
@@ -1050,11 +947,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();
|
||||
@@ -1126,6 +1023,22 @@ const DataGrid = ({
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-[0.6rem] px-[1.2rem]">
|
||||
{statusInfo &&
|
||||
<div className="data-grid-status-bubble flex flex-wrap items-center gap-x-4 gap-y-1">
|
||||
<span>진행상황: {
|
||||
statusInfo.locgStatus === null || statusInfo.locgStatus === '0' ?
|
||||
'미작성'
|
||||
:
|
||||
statusInfo.locgStatus === '1' ? '작성중' : statusInfo.locgStatus === '2' ? '제출'
|
||||
:
|
||||
statusInfo.locgStatus === '3' ? '승인' : '반려'
|
||||
}
|
||||
</span>
|
||||
<span>제출일: {statusInfo.locgDt ? statusInfo.locgDt : '미제출'}</span>
|
||||
<span>담당자: {statusInfo.locgCharger ? statusInfo.locgCharger : '-'}</span>
|
||||
<span>연락처: {statusInfo.locgCttpc ? statusInfo.locgCttpc : '-'}</span>
|
||||
</div>
|
||||
}
|
||||
{isExcelImport && (
|
||||
<Button
|
||||
className="h-[3.2rem] w-[96px] bg-gradient-green px-6 hover:bg-gradient-green-hover"
|
||||
@@ -1148,48 +1061,48 @@ const DataGrid = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="h-[65vh]">
|
||||
<RealGridReact
|
||||
ref={gridRef}
|
||||
autoGenerateField={true}
|
||||
layout={columnLayout}
|
||||
rows={rowData}
|
||||
onInitialized={(grid) => {
|
||||
if (!grid) return;
|
||||
(grid as { onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean }).onSorting = createOnSortingHandler(columns, setRowData, sortDirRef, originalRowsRef);
|
||||
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> }
|
||||
| undefined;
|
||||
if (dataProvider?.setFields) {
|
||||
dataProvider.setFields(buildFieldsFromColumns(columns));
|
||||
}
|
||||
if (dataProvider?.setRows && rowData?.length) {
|
||||
dataProvider.setRows(rowData);
|
||||
}
|
||||
const gridView = gridRef.current?.gridView ?? grid;
|
||||
if (!gridView?.columnByName) return;
|
||||
setTimeout(() => {
|
||||
const gv = gridRef.current?.gridView;
|
||||
if (!gv?.columnByName) return;
|
||||
columns.forEach((column) => {
|
||||
const baseStyleName = column.styleName;
|
||||
const gridCol = gv.columnByName(column.name) as { styleCallback?: (g: unknown, dataCell: { itemIndex?: number; index?: { itemIndex?: number }; dataField?: string; fieldName?: string }) => string } | null;
|
||||
if (gridCol) {
|
||||
gridCol.styleCallback = (_g, dataCell) => {
|
||||
const itemIndex = dataCell?.index?.itemIndex ?? dataCell?.itemIndex ?? -1;
|
||||
const fieldName = dataCell?.dataField ?? dataCell?.fieldName ?? column.fieldName;
|
||||
const data = rowDataRef.current;
|
||||
if (itemIndex < 0 || itemIndex >= data.length) return baseStyleName ?? '';
|
||||
const row = data[itemIndex] as Record<string, string | number>;
|
||||
const extra = cellStyleCallbackRef.current?.({ rowIndex: itemIndex, fieldName, row });
|
||||
if (!extra) return baseStyleName ?? '';
|
||||
return baseStyleName ? `${extra} ${baseStyleName}` : extra;
|
||||
};
|
||||
}
|
||||
});
|
||||
gv.refresh();
|
||||
}, 0);
|
||||
}}
|
||||
>
|
||||
<RealGridReact
|
||||
ref={gridRef}
|
||||
autoGenerateField={true}
|
||||
layout={columnLayout}
|
||||
rows={rowData}
|
||||
onInitialized={(grid) => {
|
||||
if (!grid) return;
|
||||
(grid as { onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean }).onSorting = createOnSortingHandler(columns, setRowData, sortDirRef, originalRowsRef);
|
||||
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> }
|
||||
| undefined;
|
||||
if (dataProvider?.setFields) {
|
||||
dataProvider.setFields(buildFieldsFromColumns(columns));
|
||||
}
|
||||
if (dataProvider?.setRows && rowData?.length) {
|
||||
dataProvider.setRows(rowData);
|
||||
}
|
||||
const gridView = gridRef.current?.gridView ?? grid;
|
||||
if (!gridView?.columnByName) return;
|
||||
setTimeout(() => {
|
||||
const gv = gridRef.current?.gridView;
|
||||
if (!gv?.columnByName) return;
|
||||
columns.forEach((column) => {
|
||||
const baseStyleName = column.styleName;
|
||||
const gridCol = gv.columnByName(column.name) as { styleCallback?: (g: unknown, dataCell: { itemIndex?: number; index?: { itemIndex?: number }; dataField?: string; fieldName?: string }) => string } | null;
|
||||
if (gridCol) {
|
||||
gridCol.styleCallback = (_g, dataCell) => {
|
||||
const itemIndex = dataCell?.index?.itemIndex ?? dataCell?.itemIndex ?? -1;
|
||||
const fieldName = dataCell?.dataField ?? dataCell?.fieldName ?? column.fieldName;
|
||||
const data = rowDataRef.current;
|
||||
if (itemIndex < 0 || itemIndex >= data.length) return baseStyleName ?? '';
|
||||
const row = data[itemIndex] as Record<string, string | number>;
|
||||
const extra = cellStyleCallbackRef.current?.({ rowIndex: itemIndex, fieldName, row });
|
||||
if (!extra) return baseStyleName ?? '';
|
||||
return baseStyleName ? `${extra} ${baseStyleName}` : extra;
|
||||
};
|
||||
}
|
||||
});
|
||||
gv.refresh();
|
||||
}, 0);
|
||||
}}
|
||||
>
|
||||
{columns.map((column) => {
|
||||
const baseStyleName = column.styleName;
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ import type {
|
||||
SewerageSupplyRateRequestParams,
|
||||
SewerageSupplyRateSaveItem,
|
||||
SewerageSupplyRateVerificationResult,
|
||||
SewerageSupplyRateSaveRequest
|
||||
SewerageSupplyRateSaveRequest,
|
||||
SewerageSupplyRateStatusInfo,
|
||||
} from "@/entities/statistics/model/statistics-input-interface";
|
||||
import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input/SeweragePenetrationRateInpColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
@@ -26,6 +27,10 @@ import { getFooterSummary } from "@/components/util/grid/footerSummary";
|
||||
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
|
||||
type GridRowWithVerification = Record<string, string | number> & {
|
||||
verificationResult?: SewerageSupplyRateVerificationResult;
|
||||
};
|
||||
|
||||
function getSewerageSupplyRateParams(
|
||||
values: SearchFormValues,
|
||||
overrides?: { yr?: string; demoYr?: string; mode?: string }
|
||||
@@ -47,6 +52,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
const [searchValues, setSearchValues] = useState<SearchFormValues | null>(null);
|
||||
const [isEnterable, setIsEnterable] = useState<boolean>(true);
|
||||
const [lastSearchedYear, setLastSearchedYear] = useState<string | null>(null);
|
||||
const [statusInfo, setStatusInfo] = useState<SewerageSupplyRateStatusInfo | null>(null);
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
const [footerFromApi, setFooterFromApi] = useState<{ totalPopl?: number; poplSm?: number; frgnrPoplSm?: number } | null>(null);
|
||||
@@ -77,6 +83,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
|
||||
const handleReset = () => {
|
||||
setRowData([]);
|
||||
setStatusInfo(null);
|
||||
setLastSearchedYear(null);
|
||||
setFooterFromApi(null);
|
||||
};
|
||||
@@ -95,12 +102,14 @@ const SeweragePenetrationRateInp = () => {
|
||||
}));
|
||||
setRowData(
|
||||
convertedData.map((d) => {
|
||||
const row = toGridRows([d])[0] as Record<string, string | number> & { verificationResult?: SewerageSupplyRateVerificationResult };
|
||||
const row = toGridRows([d])[0] as GridRowWithVerification;
|
||||
if (d.verificationResult) row.verificationResult = d.verificationResult;
|
||||
return row;
|
||||
})
|
||||
);
|
||||
|
||||
setStatusInfo(response.data.statusInfo);
|
||||
|
||||
setFooterFromApi(
|
||||
commonInfo
|
||||
? {
|
||||
@@ -116,6 +125,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
setRowData([]);
|
||||
setStatusInfo(null);
|
||||
setFooterFromApi(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -164,7 +174,12 @@ const SeweragePenetrationRateInp = () => {
|
||||
};
|
||||
|
||||
const submitFunc = () => {
|
||||
// 제출 로직
|
||||
const isError = rowData.filter((d) => d.verificationResult);
|
||||
|
||||
if (isError.length > 0) {
|
||||
alert('검증 오류 데이터가 있습니다.');
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const saveFunc = async () => {
|
||||
@@ -241,8 +256,13 @@ const SeweragePenetrationRateInp = () => {
|
||||
}
|
||||
|
||||
const response = await statisticsInputAPi.saveSewerageSupplyRate(requestData);
|
||||
console.log(convertedData)
|
||||
console.log(response);
|
||||
|
||||
if (response.status === 'success') {
|
||||
fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}));
|
||||
alert('저장되었습니다.');
|
||||
} else {
|
||||
alert('데이터 저장 중 오류가 발생했습니다.');
|
||||
}
|
||||
} else {
|
||||
// 저장 취소
|
||||
}
|
||||
@@ -283,9 +303,10 @@ const SeweragePenetrationRateInp = () => {
|
||||
fixedColumns={1}
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
statusInfo={statusInfo}
|
||||
getGridDataRef={getGridDataRef}
|
||||
cellStyleCallback={({ row, fieldName }) => {
|
||||
const verification = (row as any).verificationResult as SewerageSupplyRateVerificationResult | undefined;
|
||||
const verification = (row as GridRowWithVerification).verificationResult;
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { ValueType } from "realgrid";
|
||||
import { type SewerageSupplyRateStatusInfo } from "@/entities/statistics/model/statistics-input-interface";
|
||||
|
||||
// RealGrid 타입 정의
|
||||
/** setColumns에 전달하는 컬럼 정보 (footer 배열/단일 객체 포함) */
|
||||
@@ -230,6 +231,7 @@ export type DataGridProps = {
|
||||
current: (() => Record<string, string | number>[]) | null;
|
||||
};
|
||||
onRowClick?: (rowData: Record<string, string | number>) => void; // 행 클릭 이벤트
|
||||
statusInfo?: SewerageSupplyRateStatusInfo | null;
|
||||
};
|
||||
|
||||
export type TabDataGridProps = {
|
||||
|
||||
Reference in New Issue
Block a user