[ST] - fix: 입력: 새로고침 시 열려있는 모든 텝의 공통 api가 실행되는 현상 수정, 하수도보급률 저장&제출 request 수정(외국인총인구 제거, 총면적 추가), 검증 툴팁 max-width 추가, 지난 검증 결과 및 제출 결과 데이터 마우스 툴팁 추가

This commit is contained in:
2026-08-10 17:58:05 +09:00
parent 678af2f06e
commit 181730bcde
26 changed files with 905 additions and 499 deletions
+196 -54
View File
@@ -86,6 +86,40 @@ function mergeGridRowsWithSourceMetadata(
});
}
/** row.verificationResult 필드별 오류 배열이 하나라도 있으면 true */
function rowHasVerificationError(
row: Record<string, string | number>,
): boolean {
const verification = (
row as { verificationResult?: Record<string, unknown> }
).verificationResult;
if (!verification || typeof verification !== "object") return false;
for (const value of Object.values(verification)) {
if (Array.isArray(value) && value.length > 0) return true;
}
return false;
}
function rowsHaveVerificationErrors(
rows: Record<string, string | number>[],
): boolean {
return rows.some(rowHasVerificationError);
}
function sortRowsVerifyErrorsFirst(
rows: Record<string, string | number>[],
): Record<string, string | number>[] {
return rows
.map((row, index) => ({ row, index }))
.sort((a, b) => {
const aErr = rowHasVerificationError(a.row) ? 0 : 1;
const bErr = rowHasVerificationError(b.row) ? 0 : 1;
if (aErr !== bErr) return aErr - bErr;
return a.index - b.index;
})
.map(({ row }) => row);
}
function applyCellStyleCallbacksToColumns(
gridView: unknown,
columns: ColumnConfig[],
@@ -804,6 +838,12 @@ const DataGrid = ({
const emptyCellHoverRafRef = useRef<number | null>(null);
const [emptyCellHoverTooltip, setEmptyCellHoverTooltip] =
useState<EmptyCellHoverTooltip | null>(null);
/** 검증 오류 행 우선보기 (오류 있는 데이터일 때만 UI 노출) */
const [preferVerifyErrors, setPreferVerifyErrors] = useState(false);
const verifyPriorityBaselineRef = useRef<
Record<string, string | number>[] | null
>(null);
const skipVerifyPriorityResetRef = useRef(false);
columnsRef.current = columns;
rowDataRef.current = rowData;
cellStyleCallbackRef.current = cellStyleCallback;
@@ -814,6 +854,90 @@ const DataGrid = ({
onCellEditedRef.current = onCellEdited;
onDataCellClickRef.current = onDataCellClick;
const hasVerificationErrors = rowsHaveVerificationErrors(rowData);
const showVerifyPriorityToggle = hasVerificationErrors;
// 조회 등으로 rowData가 교체되면 우선보기 해제 (자체 setRowData는 스킵)
useEffect(() => {
if (skipVerifyPriorityResetRef.current) {
skipVerifyPriorityResetRef.current = false;
return;
}
verifyPriorityBaselineRef.current = null;
setPreferVerifyErrors(false);
}, [rowData]);
// 오류가 없어지면 체크 해제 + 원본 순서 복원
useEffect(() => {
if (hasVerificationErrors || !preferVerifyErrors) return;
const baseline = verifyPriorityBaselineRef.current;
verifyPriorityBaselineRef.current = null;
setPreferVerifyErrors(false);
if (baseline) {
skipVerifyPriorityResetRef.current = true;
setRowData(baseline);
}
}, [hasVerificationErrors, preferVerifyErrors, setRowData]);
const collectCurrentRowsWithMetadata = () => {
const fieldNames = columns
.map((c) => c.fieldName)
.filter(Boolean) as string[];
const gv = gridRef.current?.gridView as
| {
getDataSource?: () => {
getRowCount(): number;
getValue(r: number, f: string): unknown;
};
commit?: (force?: boolean) => boolean;
}
| undefined;
if (!gv?.getDataSource) {
return rowDataRef.current.map((r) => ({ ...r }));
}
try {
gv.commit?.();
} catch {
/* ignore */
}
const ds = gv.getDataSource();
if (!ds) return rowDataRef.current.map((r) => ({ ...r }));
const cnt = ds.getRowCount();
const rows: Record<string, string | number>[] = [];
for (let i = 0; i < cnt; i++) {
const r: Record<string, string | number> = {};
for (const f of fieldNames)
r[f] = ds.getValue(i, f) as string | number;
rows.push(r);
}
return mergeGridRowsWithSourceMetadata(
rows,
rowDataRef.current,
fieldNames,
);
};
const handlePreferVerifyErrorsChange = (checked: boolean) => {
if (checked) {
const current = collectCurrentRowsWithMetadata();
if (!rowsHaveVerificationErrors(current)) return;
verifyPriorityBaselineRef.current = current.map((r) => ({ ...r }));
sortDirRef.current = {};
originalRowsRef.current = {};
skipVerifyPriorityResetRef.current = true;
setPreferVerifyErrors(true);
setRowData(sortRowsVerifyErrorsFirst(current));
return;
}
const baseline = verifyPriorityBaselineRef.current;
verifyPriorityBaselineRef.current = null;
skipVerifyPriorityResetRef.current = true;
setPreferVerifyErrors(false);
if (baseline) {
setRowData(baseline);
}
};
// rowData 변경 시 편집 중인 셀을 commit한 뒤 수동으로 setRows (RealGridReact에 rows prop을 넘기지 않음)
const rowDataMountedRef = useRef(false);
useEffect(() => {
@@ -2086,60 +2210,79 @@ useEffect(() => {
onChange={handleFileChange}
/>
<div
className={`flex items-center ${statusInfo ? "justify-between" : "justify-end"} gap-[0.6rem] pr-[1.2rem]`}
className={`flex items-center ${statusInfo || showVerifyPriorityToggle ? "justify-between" : "justify-end"} gap-[0.6rem] pr-[1.2rem]`}
>
{statusInfo && (
<div className="data-grid-status-bubble flex flex-wrap items-center gap-x-4 gap-y-1">
<span>
진행상황:{" "}
{statusInfo.laeSbmsnSttsCd === null ||
statusInfo.laeSbmsnSttsCd === "0" ? (
"미작성"
) : statusInfo.laeSbmsnSttsCd === "1" ? (
<span className="animate-pulse">작성중</span>
) : statusInfo.laeSbmsnSttsCd === "2" ? (
<span className="!text-[#2094fa]">제출</span>
) : statusInfo.laeSbmsnSttsCd === "3" ? (
<span className="!text-[#1fd078]">승인</span>
) : statusInfo.laeSbmsnSttsCd === "4" ? (
<span className="relative inline-flex pr-3 !text-[#ff6c64]">
반려
<span className="absolute -right-0.5 -top-0.5 inline-flex size-[0.6125rem]">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[#ff6c64] opacity-75" />
<span className="relative inline-flex size-[0.6125rem] rounded-full bg-[#ff6c64]" />
<div className="flex min-w-0 flex-wrap items-center gap-3">
{statusInfo && (
<div className="data-grid-status-bubble flex flex-wrap items-center gap-x-4 gap-y-1">
<span>
진행상황:{" "}
{statusInfo.laeSbmsnSttsCd === null ||
statusInfo.laeSbmsnSttsCd === "0" ? (
"미작성"
) : statusInfo.laeSbmsnSttsCd === "1" ? (
<span className="animate-pulse">작성중</span>
) : statusInfo.laeSbmsnSttsCd === "2" ? (
<span className="!text-[#2094fa]">제출</span>
) : statusInfo.laeSbmsnSttsCd === "3" ? (
<span className="!text-[#1fd078]">승인</span>
) : statusInfo.laeSbmsnSttsCd === "4" ? (
<span className="relative inline-flex pr-3 !text-[#ff6c64]">
반려
<span className="absolute -right-0.5 -top-0.5 inline-flex size-[0.6125rem]">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[#ff6c64] opacity-75" />
<span className="relative inline-flex size-[0.6125rem] rounded-full bg-[#ff6c64]" />
</span>
</span>
</span>
) : (
<span className="animate-pulse">검증중</span>
)}
</span>
<span>
반려사유:{" "}
{statusInfo.rjctOpnnCn
? statusInfo.rjctOpnnCn
: "-"}
</span>
<span>
제출일:{" "}
{statusInfo.laeSbmsnYmd
? statusInfo.laeSbmsnYmd.length === 8 &&
/^\d{8}$/.test(statusInfo.laeSbmsnYmd)
? `${statusInfo.laeSbmsnYmd.slice(0, 4)}-${statusInfo.laeSbmsnYmd.slice(4, 6)}-${statusInfo.laeSbmsnYmd.slice(6, 8)}`
: statusInfo.laeSbmsnYmd
: "미제출"}
</span>
<span>
담당자:{" "}
{statusInfo.laePicNm ? statusInfo.laePicNm : "-"}
</span>
<span>
연락처:{" "}
{statusInfo.laePicTelNo
? statusInfo.laePicTelNo
: "-"}
</span>
</div>
)}
) : (
<span className="animate-pulse">검증중</span>
)}
</span>
<span>
반려사유:{" "}
{statusInfo.rjctOpnnCn
? statusInfo.rjctOpnnCn
: "-"}
</span>
<span>
제출일:{" "}
{statusInfo.laeSbmsnYmd
? statusInfo.laeSbmsnYmd.length === 8 &&
/^\d{8}$/.test(statusInfo.laeSbmsnYmd)
? `${statusInfo.laeSbmsnYmd.slice(0, 4)}-${statusInfo.laeSbmsnYmd.slice(4, 6)}-${statusInfo.laeSbmsnYmd.slice(6, 8)}`
: statusInfo.laeSbmsnYmd
: "미제출"}
</span>
<span>
담당자:{" "}
{statusInfo.laePicNm ? statusInfo.laePicNm : "-"}
</span>
<span>
연락처:{" "}
{statusInfo.laePicTelNo
? statusInfo.laePicTelNo
: "-"}
</span>
</div>
)}
{showVerifyPriorityToggle && (
<label className="inline-flex shrink-0 cursor-pointer items-center gap-1.5 text-label data-grid-status-bubble">
<input
type="checkbox"
className="m-0 shrink-0 cursor-pointer accent-primary"
checked={preferVerifyErrors}
onChange={(e) =>
handlePreferVerifyErrorsChange(
e.target.checked,
)
}
/>
<span className="whitespace-nowrap leading-none">
검증 오류 항목 우선보기
</span>
</label>
)}
</div>
<div className="flex items-center gap-[0.6rem]">
{isExcelImport && (
<Button
@@ -2467,11 +2610,10 @@ useEffect(() => {
{emptyCellHoverTooltip &&
createPortal(
<div
className="rg-tooltip pointer-events-none !visible fixed z-[9999]"
className="rg-tooltip pointer-events-none !visible fixed z-[9999] w-max=[60rem]"
style={{
left: emptyCellHoverTooltip.x,
top: emptyCellHoverTooltip.y,
whiteSpace: "pre-wrap",
}}
role="tooltip"
>
@@ -22,6 +22,7 @@ import {
LoadingWithAnimation,
} from "@common/components";
import DataGrid from "@/components/module/DataGrid";
import { cellContentTooltip } from "@/components/module/verificationResultCellTooltip";
import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination";
import type {
CellEditedCallbackParams,
@@ -413,6 +414,7 @@ export function VerificationResultDialog({
getGridDataRef={getGridDataRef}
onCellEdited={onPrcsOpnnExplnEdited}
cellStyleCallback={verificationMthdCellStyle}
cellTooltipCallback={cellContentTooltip}
onItemChecked={(itemIndex, checked) => {
setCheckedIndices((prev) => {
const next = new Set(prev);
@@ -31,6 +31,7 @@ import {
} from "@common/components";
import DataGrid from "@/components/module/DataGrid";
import { cellContentTooltip } from "@/components/module/verificationResultCellTooltip";
import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination";
import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type";
import { useAlertStore } from "@/shared/lib";
@@ -500,6 +501,7 @@ export function VerificationResultHistoryDialog({
enableSort={false}
getGridRef={gridRef}
cellStyleCallback={verificationPrcsYnNmCellStyle}
cellTooltipCallback={cellContentTooltip}
onItemChecked={(itemIndex, checked) => {
const entry = entries[itemIndex];
if (!entry) return;
@@ -0,0 +1,11 @@
import type { CellTooltipCallbackParams } from "@/components/type/DataGrid.type";
/** RealGrid 셀 호버 시 title처럼 전체 내용 표시 */
export function cellContentTooltip({
row,
fieldName,
}: CellTooltipCallbackParams): string | undefined {
const value = row[fieldName];
if (value == null || value === "") return undefined;
return String(value);
}