[ST] - fix: 입력: 새로고침 시 열려있는 모든 텝의 공통 api가 실행되는 현상 수정, 하수도보급률 저장&제출 request 수정(외국인총인구 제거, 총면적 추가), 검증 툴팁 max-width 추가, 지난 검증 결과 및 제출 결과 데이터 마우스 툴팁 추가
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
@tailwind components;
|
||||
|
||||
/* RealGrid 기본은 white-space: nowrap → max-width만 주면 잘림. 너비 초과 시에만 줄바꿈 */
|
||||
.rg-tooltip {
|
||||
max-width: 60rem;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell {
|
||||
@apply !bg-white;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -62,6 +62,7 @@ import type {
|
||||
} from "@/entities/statistics/model/statistics-interface";
|
||||
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
|
||||
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
@@ -356,7 +357,10 @@ const ExcretaTreatmentFacilityInp = () => {
|
||||
const sggOptionsSidoRef = useRef<string | null>(null);
|
||||
const [searchLoaded, setSearchLoaded] = useState(false);
|
||||
|
||||
const activeTabId = useTabStore((s) => s.activeTabId);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTabId !== STATISTICS_INPUT_URL_PATH.EXCRETA_TREATMENT_FACILITY) return;
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
try {
|
||||
@@ -382,7 +386,7 @@ const ExcretaTreatmentFacilityInp = () => {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
}, [activeTabId]);
|
||||
|
||||
const loadSggOptions = useCallback(async (sido: string): Promise<Sgg[]> => {
|
||||
if (!sido || sido === "ALL") {
|
||||
|
||||
@@ -56,6 +56,8 @@ import { PrivateSewageDesigStatusInpColumn } from "@/components/util/column/inpu
|
||||
import { SepticTankStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/SepticTankStatusInpColumn";
|
||||
import { SewageTreatFactStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/SewageTreatFactStatusInpColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
|
||||
@@ -74,10 +76,6 @@ const PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY: Record<string, s
|
||||
[PrivateSewageDesigStatusInpColumn.tabKey]: "ST00080003",
|
||||
};
|
||||
|
||||
const PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CDS = Array.from(
|
||||
new Set(Object.values(PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY)),
|
||||
);
|
||||
|
||||
type GridRowWithVerification = Record<string, string | number> & {
|
||||
verificationResult?: PrivateSewageFacilityVerificationResult;
|
||||
};
|
||||
@@ -362,42 +360,30 @@ const PrivateSewageFacilityInp = () => {
|
||||
const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
|
||||
activeTabRef.current = activeTab;
|
||||
|
||||
const activeTabId = useTabStore((s) => s.activeTabId);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTabId !== STATISTICS_INPUT_URL_PATH.PRIVATE_SEWAGE_FACILITY) return;
|
||||
const tabKey = activeTab;
|
||||
if (!tabKey) return;
|
||||
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
|
||||
const statsCd =
|
||||
PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY[tabKey] ?? tabKey;
|
||||
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const entries = await Promise.all(
|
||||
PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CDS.map(async (statsCd) => {
|
||||
try {
|
||||
const res = await statisticsApi.getColumnGuides(statsCd);
|
||||
if (res.status !== "success" || !Array.isArray(res.data)) {
|
||||
return [statsCd, {}] as const;
|
||||
}
|
||||
const next: Record<string, string> = {};
|
||||
for (const item of res.data) {
|
||||
const key = item.engColNm?.trim();
|
||||
const guide = item.columnGuide?.trim();
|
||||
if (key && guide) next[key] = guide;
|
||||
}
|
||||
return [statsCd, next] as const;
|
||||
} catch {
|
||||
return [statsCd, {}] as const;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const next = await fetchColumnGuidesRecord(statsCd);
|
||||
if (cancelled) return;
|
||||
const guidesByStatsCd = Object.fromEntries(entries);
|
||||
setColumnGuidesByTab(
|
||||
Object.fromEntries(
|
||||
Object.entries(PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY).map(
|
||||
([tabKey, statsCd]) => [tabKey, guidesByStatsCd[statsCd] ?? {}],
|
||||
),
|
||||
),
|
||||
setColumnGuidesByTab((prev) =>
|
||||
Object.prototype.hasOwnProperty.call(prev, tabKey)
|
||||
? prev
|
||||
: { ...prev, [tabKey]: next },
|
||||
);
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
}, [activeTabId, activeTab, columnGuidesByTab]);
|
||||
|
||||
const commitToolbarSnapshotForLoadedTab = (
|
||||
tabKey: string,
|
||||
|
||||
@@ -49,6 +49,8 @@ import type { SearchFormValues } from '@/components/type/SearchForm.type';
|
||||
import { doList } from '@/components/util/column/input/DoList';
|
||||
import { PublicSewageFactInpColumnList } from '@/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactInpColumn';
|
||||
import { toGridRows } from '@/components/util/grid/gridFunction';
|
||||
import { fetchColumnGuidesRecord } from '@/components/util/grid/columnGuides';
|
||||
import { useTabStore } from '@/store/tab-store';
|
||||
import { patchVerificationFailedByTab } from '@/components/util/input/verification-failed-flag';
|
||||
import { useAdminStore } from '@/store/admin-store';
|
||||
|
||||
@@ -144,10 +146,6 @@ type MainFacilityMappingSaveDraft = {
|
||||
initialPrxyCtrtCn: string;
|
||||
};
|
||||
|
||||
const PUBLIC_SEWAGE_FACILITY_COLUMN_GUIDE_TAB_KEYS = Array.from(
|
||||
new Set(PublicSewageFactInpColumnList.map((tab) => tab.tabKey).filter(Boolean)),
|
||||
);
|
||||
|
||||
/** 위탁계약기간: `yyyymmdd~yyyymmdd` (빈 값 허용). 하이픈 형식은 정규화 */
|
||||
function normalizePrxyCtrtCn(raw: unknown): string | null {
|
||||
if (raw === null || raw === undefined) return null;
|
||||
@@ -644,35 +642,28 @@ const PublicSewageFacilityInp = () => {
|
||||
const firstTabKey = PublicSewageFactInpColumnList[0]?.tabKey ?? 'ST00060001';
|
||||
const searchParamRef = useRef<PublicSewageFacilityRequestParams | null>(null);
|
||||
|
||||
const activeTabId = useTabStore((s) => s.activeTabId);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTabId !== STATISTICS_INPUT_URL_PATH.PUBLIC_SEWAGE_FACILITY) return;
|
||||
const tabKey = activeTab;
|
||||
if (!tabKey) return;
|
||||
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
|
||||
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const entries = await Promise.all(
|
||||
PUBLIC_SEWAGE_FACILITY_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => {
|
||||
try {
|
||||
const res = await statisticsApi.getColumnGuides(tabKey);
|
||||
if (res.status !== 'success' || !Array.isArray(res.data)) {
|
||||
return [tabKey, {}] as const;
|
||||
}
|
||||
const next: Record<string, string> = {};
|
||||
for (const item of res.data) {
|
||||
const key = item.engColNm?.trim();
|
||||
const guide = item.columnGuide?.trim();
|
||||
if (key && guide) next[key] = guide;
|
||||
}
|
||||
return [tabKey, next] as const;
|
||||
} catch {
|
||||
return [tabKey, {}] as const;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const next = await fetchColumnGuidesRecord(tabKey);
|
||||
if (cancelled) return;
|
||||
setColumnGuidesByTab(Object.fromEntries(entries));
|
||||
setColumnGuidesByTab((prev) =>
|
||||
Object.prototype.hasOwnProperty.call(prev, tabKey)
|
||||
? prev
|
||||
: { ...prev, [tabKey]: next },
|
||||
);
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
}, [activeTabId, activeTab, columnGuidesByTab]);
|
||||
|
||||
const commitToolbarSnapshotForLoadedTab = (
|
||||
tabKey: string,
|
||||
|
||||
@@ -61,6 +61,7 @@ import type {
|
||||
} from "@/entities/statistics/model/statistics-interface";
|
||||
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
|
||||
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
@@ -353,7 +354,10 @@ const PumpingStationInp = () => {
|
||||
const sggOptionsSidoRef = useRef<string | null>(null);
|
||||
const [searchLoaded, setSearchLoaded] = useState(false);
|
||||
|
||||
const activeTabId = useTabStore((s) => s.activeTabId);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTabId !== STATISTICS_INPUT_URL_PATH.PUMPING_STATION) return;
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
try {
|
||||
@@ -377,7 +381,7 @@ const PumpingStationInp = () => {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
}, [activeTabId]);
|
||||
|
||||
const loadSggOptions = useCallback(async (sido: string): Promise<Sgg[]> => {
|
||||
if (!sido || sido === "ALL") {
|
||||
|
||||
@@ -36,6 +36,8 @@ import { PrivateSewageTreatRelatedIndustryInpColumn } from "@/components/util/co
|
||||
import { PublicInstitutionExcretaIndustryInpColumn } from "@/components/util/column/input/sewageExcretaRelated/PublicInstitutionExcretaIndustryInpColumn";
|
||||
import { PublicInstitutionSewerIndustryInpColumn } from "@/components/util/column/input/sewageExcretaRelated/PublicInstitutionSewerIndustryInpColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
|
||||
import { statisticsInputSewageExcretaRelatedApi } from "@/entities/statistics/api/input/statistics-input-sewage-excreta-related-service";
|
||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||
@@ -645,36 +647,29 @@ const SewageExcretaRelatedInp = () => {
|
||||
const sggOptionsSidoRef = useRef<string | null>(null);
|
||||
activeTabRef.current = activeTab;
|
||||
|
||||
const activeTabId = useTabStore((s) => s.activeTabId);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_EXCRETA_RELATED) return;
|
||||
const tabKey = activeTab;
|
||||
if (!tabKey) return;
|
||||
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
|
||||
const statsCd = tabKey;
|
||||
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const entries = await Promise.all(
|
||||
SewageExcretaRelatedInpTabList.map(async (tab) => {
|
||||
const tabKey = tab.tabKey;
|
||||
try {
|
||||
const res = await statisticsApi.getColumnGuides(tabKey);
|
||||
if (res.status !== "success" || !Array.isArray(res.data)) {
|
||||
return [tabKey, {}] as const;
|
||||
}
|
||||
const next: Record<string, string> = {};
|
||||
for (const item of res.data) {
|
||||
const key = item.engColNm?.trim();
|
||||
const guide = item.columnGuide?.trim();
|
||||
if (key && guide) next[key] = guide;
|
||||
}
|
||||
return [tabKey, next] as const;
|
||||
} catch {
|
||||
return [tabKey, {}] as const;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const next = await fetchColumnGuidesRecord(statsCd);
|
||||
if (cancelled) return;
|
||||
setColumnGuidesByTab(Object.fromEntries(entries));
|
||||
setColumnGuidesByTab((prev) =>
|
||||
Object.prototype.hasOwnProperty.call(prev, tabKey)
|
||||
? prev
|
||||
: { ...prev, [tabKey]: next },
|
||||
);
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
}, [activeTabId, activeTab, columnGuidesByTab]);
|
||||
|
||||
const shouldShowNotApplicable = (
|
||||
tabKey: string,
|
||||
|
||||
@@ -50,6 +50,8 @@ import { SewageSlurgeProcessingFacStatusInpColumn } from "@/components/util/colu
|
||||
import { SlurgeProcessingFacStatusInpColumn } from "@/components/util/column/input/sewageExcretaSlurge/SlurgeProcessingFacStatusInpColumn";
|
||||
import { doList } from "@/components/util/column/input/DoList";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
|
||||
import { statisticsInputSewageExcretaSlurgeApi } from "@/entities/statistics/api/input/statistics-input-sewage-excreta-slurge-service";
|
||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||
@@ -700,36 +702,29 @@ const SewageExcretaSlurgeInp = () => {
|
||||
const preYearArticleFcltSnRef = useRef<ArticleFcltSnSet>(new Set());
|
||||
activeTabRef.current = activeTab;
|
||||
|
||||
const activeTabId = useTabStore((s) => s.activeTabId);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_EXCRETA_SLURGE) return;
|
||||
const tabKey = activeTab;
|
||||
if (!tabKey) return;
|
||||
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
|
||||
const statsCd = tabKey;
|
||||
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const entries = await Promise.all(
|
||||
SewageExcretaSlurgeInpTabList.map(async (tab) => {
|
||||
const tabKey = tab.tabKey;
|
||||
try {
|
||||
const res = await statisticsApi.getColumnGuides(tabKey);
|
||||
if (res.status !== "success" || !Array.isArray(res.data)) {
|
||||
return [tabKey, {}] as const;
|
||||
}
|
||||
const next: Record<string, string> = {};
|
||||
for (const item of res.data) {
|
||||
const key = item.engColNm?.trim();
|
||||
const guide = item.columnGuide?.trim();
|
||||
if (key && guide) next[key] = guide;
|
||||
}
|
||||
return [tabKey, next] as const;
|
||||
} catch {
|
||||
return [tabKey, {}] as const;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const next = await fetchColumnGuidesRecord(statsCd);
|
||||
if (cancelled) return;
|
||||
setColumnGuidesByTab(Object.fromEntries(entries));
|
||||
setColumnGuidesByTab((prev) =>
|
||||
Object.prototype.hasOwnProperty.call(prev, tabKey)
|
||||
? prev
|
||||
: { ...prev, [tabKey]: next },
|
||||
);
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
}, [activeTabId, activeTab, columnGuidesByTab]);
|
||||
|
||||
const shouldShowNotApplicable = (
|
||||
hasRows: boolean,
|
||||
|
||||
@@ -70,6 +70,7 @@ import { PipeTypeStatusInpColumn } from "@/components/util/column/input/sewagePi
|
||||
import { RegionalFactStatusInpColumn } from "@/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn";
|
||||
import { SewagePipeInpColumnInfoList } from "@/components/util/column/input/sewagePipe/SewagePipeInpColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
|
||||
import {
|
||||
patchVerificationFailedByTab,
|
||||
} from "@/components/util/input/verification-failed-flag";
|
||||
@@ -297,9 +298,6 @@ type GridRowWithVerification = Record<string, string | number> & {
|
||||
};
|
||||
|
||||
const REGIONAL_FACT_TAB_KEY = "ST00020001";
|
||||
const SEWAGE_PIPE_COLUMN_GUIDE_TAB_KEYS = Array.from(
|
||||
new Set(SewagePipeInpColumnInfoList.map((tab) => tab.tabKey).filter(Boolean)),
|
||||
);
|
||||
|
||||
// function hasRegionalFactVerificationErrors(verification?: SeweragePipeInputVerificationResult | null): boolean {
|
||||
// if (!verification) return false;
|
||||
@@ -538,34 +536,24 @@ const SewagePipeInp = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE) return;
|
||||
const tabKey = activeTab;
|
||||
if (!tabKey) return;
|
||||
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
|
||||
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const entries = await Promise.all(
|
||||
SEWAGE_PIPE_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => {
|
||||
try {
|
||||
const res = await statisticsApi.getColumnGuides(tabKey);
|
||||
if (res.status !== "success" || !Array.isArray(res.data)) {
|
||||
return [tabKey, {}] as const;
|
||||
}
|
||||
const next: Record<string, string> = {};
|
||||
for (const item of res.data) {
|
||||
const key = item.engColNm?.trim();
|
||||
const guide = item.columnGuide?.trim();
|
||||
if (key && guide) next[key] = guide;
|
||||
}
|
||||
return [tabKey, next] as const;
|
||||
} catch {
|
||||
return [tabKey, {}] as const;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const next = await fetchColumnGuidesRecord(tabKey);
|
||||
if (cancelled) return;
|
||||
setColumnGuidesByTab(Object.fromEntries(entries));
|
||||
setColumnGuidesByTab((prev) =>
|
||||
Object.prototype.hasOwnProperty.call(prev, tabKey)
|
||||
? prev
|
||||
: { ...prev, [tabKey]: next },
|
||||
);
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [activeTabId]);
|
||||
}, [activeTabId, activeTab, columnGuidesByTab]);
|
||||
|
||||
const syncTopStatusIfStillOnTab = (loadedTabKey: string, nextStatsCd: string) => {
|
||||
if (loadedTabKey === activeTabRef.current) {
|
||||
|
||||
@@ -57,6 +57,7 @@ import { doList } from "@/components/util/column/input/DoList";
|
||||
import { PipeDredgingStatusInpColumn } from "@/components/util/column/input/sewagePipeMaintain/PipeDredgingStatusInpColumn";
|
||||
import { RepairStatusInpColumn } from "@/components/util/column/input/sewagePipeMaintain/RepairStatusInpColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
|
||||
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
|
||||
@@ -82,12 +83,6 @@ const SewagePipeMaintainInpTabList = [
|
||||
|
||||
const firstTabKey = RepairStatusInpColumn.tabKey;
|
||||
|
||||
const SEWAGE_PIPE_MAINTAIN_COLUMN_GUIDE_TAB_KEYS = Array.from(
|
||||
new Set(
|
||||
SewagePipeMaintainInpTabList.map((tab) => tab.tabKey).filter(Boolean),
|
||||
),
|
||||
);
|
||||
|
||||
const toGridColumnInfo = (tab: (typeof SewagePipeMaintainInpTabList)[number]): ColumnInfo => ({
|
||||
columns: tab.columns ?? [],
|
||||
columnLayout: tab.columnLayout ?? [],
|
||||
@@ -344,34 +339,25 @@ const SewagePipeMaintainInp = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE_MAINTAIN) return;
|
||||
const tabKey = activeTab;
|
||||
if (!tabKey) return;
|
||||
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
|
||||
const statsCd = tabKey;
|
||||
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const entries = await Promise.all(
|
||||
SEWAGE_PIPE_MAINTAIN_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => {
|
||||
try {
|
||||
const res = await statisticsApi.getColumnGuides(tabKey);
|
||||
if (res.status !== "success" || !Array.isArray(res.data)) {
|
||||
return [tabKey, {}] as const;
|
||||
}
|
||||
const next: Record<string, string> = {};
|
||||
for (const item of res.data) {
|
||||
const key = item.engColNm?.trim();
|
||||
const guide = item.columnGuide?.trim();
|
||||
if (key && guide) next[key] = guide;
|
||||
}
|
||||
return [tabKey, next] as const;
|
||||
} catch {
|
||||
return [tabKey, {}] as const;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const next = await fetchColumnGuidesRecord(statsCd);
|
||||
if (cancelled) return;
|
||||
setColumnGuidesByTab(Object.fromEntries(entries));
|
||||
setColumnGuidesByTab((prev) =>
|
||||
Object.prototype.hasOwnProperty.call(prev, tabKey)
|
||||
? prev
|
||||
: { ...prev, [tabKey]: next },
|
||||
);
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [activeTabId]);
|
||||
}, [activeTabId, activeTab, columnGuidesByTab]);
|
||||
|
||||
const commitToolbarSnapshotForLoadedTab = (
|
||||
tabKey: string,
|
||||
|
||||
@@ -43,6 +43,8 @@ import { doList } from "@/components/util/column/input/DoList";
|
||||
import { RainfallPumpStationInpColumn } from "@/components/util/column/input/sewageReservoirPump/RainfallPumpStationInpColumn";
|
||||
import { SewageReservoirInpColumn } from "@/components/util/column/input/sewageReservoirPump/SewageReservoirInpColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
|
||||
import {
|
||||
formatCompactYmdForGrid,
|
||||
@@ -92,12 +94,6 @@ const firstTabKey = SewageReservoirPumpInpTabList[0]?.tabKey ?? SEWAGE_RESERVOIR
|
||||
const rainfallPumpTabKey =
|
||||
RainfallPumpStationInpColumn.tabKey ?? "rainfallPumpStationInp";
|
||||
|
||||
const SEWAGE_RESERVOIR_PUMP_COLUMN_GUIDE_TAB_KEYS = Array.from(
|
||||
new Set(
|
||||
SewageReservoirPumpInpTabList.map((tab) => tab.tabKey).filter(Boolean),
|
||||
),
|
||||
);
|
||||
|
||||
const SEWAGE_RESERVOIR_FACILITY_NAME_GUIDE_MESSAGE =
|
||||
"하수저류시설 등록시 하수저류시설명이 동일할 경우 같은 데이터로 분류됩니다.\n\n" +
|
||||
"하수저류시설명이 동일 할 경우 구분을 위해 하수저류시설 1, 하수저류시설 2 형태로 구분하여 주시길 바랍니다.";
|
||||
@@ -957,35 +953,29 @@ const SewageReservoirPumpInp = () => {
|
||||
useRef<(() => Record<string, string | number>[]) | null>(null);
|
||||
const sggOptionsSidoRef = useRef<string | null>(null);
|
||||
|
||||
const activeTabId = useTabStore((s) => s.activeTabId);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_RESERVOIR_PUMP) return;
|
||||
const tabKey = activeTab;
|
||||
if (!tabKey) return;
|
||||
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
|
||||
const statsCd = tabKey;
|
||||
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const entries = await Promise.all(
|
||||
SEWAGE_RESERVOIR_PUMP_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => {
|
||||
try {
|
||||
const res = await statisticsApi.getColumnGuides(tabKey);
|
||||
if (res.status !== "success" || !Array.isArray(res.data)) {
|
||||
return [tabKey, {}] as const;
|
||||
}
|
||||
const next: Record<string, string> = {};
|
||||
for (const item of res.data) {
|
||||
const key = item.engColNm?.trim();
|
||||
const guide = item.columnGuide?.trim();
|
||||
if (key && guide) next[key] = guide;
|
||||
}
|
||||
return [tabKey, next] as const;
|
||||
} catch {
|
||||
return [tabKey, {}] as const;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const next = await fetchColumnGuidesRecord(statsCd);
|
||||
if (cancelled) return;
|
||||
setColumnGuidesByTab(Object.fromEntries(entries));
|
||||
setColumnGuidesByTab((prev) =>
|
||||
Object.prototype.hasOwnProperty.call(prev, tabKey)
|
||||
? prev
|
||||
: { ...prev, [tabKey]: next },
|
||||
);
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
}, [activeTabId, activeTab, columnGuidesByTab]);
|
||||
|
||||
const searchDoneRef = useRef(false);
|
||||
const periodEnterableRef = useRef(true);
|
||||
|
||||
@@ -61,6 +61,8 @@ import { SewerageChargeInpColumn } from "@/components/util/column/input/sewerage
|
||||
import { SewerageFinanceSettleInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageFinanceSettleInpColumn";
|
||||
import { SewerageUsageFeeInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
|
||||
@@ -73,10 +75,6 @@ const SewerageFinanceInpTabList = [
|
||||
|
||||
const firstTabKey = SewerageFinanceSettleInpColumn.tabKey;
|
||||
|
||||
const SEWERAGE_FINANCE_COLUMN_GUIDE_TAB_KEYS = Array.from(
|
||||
new Set(SewerageFinanceInpTabList.map((tab) => tab.tabKey).filter(Boolean)),
|
||||
);
|
||||
|
||||
type GridRowWithVerification = Record<string, string | number> & {
|
||||
verificationResult?: SewerageFinanceVerificationResult;
|
||||
};
|
||||
@@ -391,35 +389,29 @@ const SewerageFinanceInp = () => {
|
||||
const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
|
||||
activeTabRef.current = activeTab;
|
||||
|
||||
const activeTabId = useTabStore((s) => s.activeTabId);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWERAGE_FINANCE) return;
|
||||
const tabKey = activeTab;
|
||||
if (!tabKey) return;
|
||||
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
|
||||
const statsCd = tabKey;
|
||||
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const entries = await Promise.all(
|
||||
SEWERAGE_FINANCE_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => {
|
||||
try {
|
||||
const res = await statisticsApi.getColumnGuides(tabKey);
|
||||
if (res.status !== "success" || !Array.isArray(res.data)) {
|
||||
return [tabKey, {}] as const;
|
||||
}
|
||||
const next: Record<string, string> = {};
|
||||
for (const item of res.data) {
|
||||
const key = item.engColNm?.trim();
|
||||
const guide = item.columnGuide?.trim();
|
||||
if (key && guide) next[key] = guide;
|
||||
}
|
||||
return [tabKey, next] as const;
|
||||
} catch {
|
||||
return [tabKey, {}] as const;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const next = await fetchColumnGuidesRecord(statsCd);
|
||||
if (cancelled) return;
|
||||
setColumnGuidesByTab(Object.fromEntries(entries));
|
||||
setColumnGuidesByTab((prev) =>
|
||||
Object.prototype.hasOwnProperty.call(prev, tabKey)
|
||||
? prev
|
||||
: { ...prev, [tabKey]: next },
|
||||
);
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
}, [activeTabId, activeTab, columnGuidesByTab]);
|
||||
|
||||
const commitToolbarSnapshotForLoadedTab = (
|
||||
tabKey: string,
|
||||
|
||||
@@ -490,7 +490,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
statsYr: searchValues?.year ?? '',
|
||||
userId: userInfo.userUnqId,
|
||||
submitList: convertedData,
|
||||
sewerBzmnFrgnrWholPpltnCnt: footerFromApi?.sewerBzmnFrgnrWholPpltnCnt,
|
||||
sewerBzmnArea: footerFromApi?.sewerBzmnArea,
|
||||
laePicNm: submitInfo.laePicNm,
|
||||
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
|
||||
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
|
||||
@@ -680,7 +680,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
statsYr: searchValues?.year ?? '', // 통계 연도
|
||||
userId: userInfo.userUnqId, // 사용자
|
||||
saveList: convertedData, // 저장 데이터
|
||||
sewerBzmnFrgnrWholPpltnCnt: footerFromApi?.sewerBzmnFrgnrWholPpltnCnt // 행정참고자료 외국인 합계
|
||||
sewerBzmnArea: footerFromApi?.sewerBzmnArea, // 행정참고자료면적
|
||||
};
|
||||
|
||||
if (isAdminInfo) {
|
||||
|
||||
@@ -24,6 +24,8 @@ import { doList } from "@/components/util/column/input/DoList";
|
||||
import { ComplaintInpColumn } from "@/components/util/column/input/sewerageServiceLevel/ComplaintInpColumn";
|
||||
import { ResidentFriendlyFactInpColumn } from "@/components/util/column/input/sewerageServiceLevel/ResidentFriendlyFactInpColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
|
||||
import { statisticsInputSewerageServiceLevelApi } from "@/entities/statistics/api/input/statistics-input-sewerage-service-level-service";
|
||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||
@@ -60,11 +62,6 @@ const SewerageServiceLevelInpTabList = [
|
||||
|
||||
const firstTabKey = ComplaintInpColumn.tabKey;
|
||||
|
||||
const SEWERAGE_SERVICE_LEVEL_COLUMN_GUIDE_STATS_CDS = [
|
||||
SEWERAGE_SERVICE_LEVEL_COMPLAINT_STATS_CD,
|
||||
SEWERAGE_SERVICE_LEVEL_RESIDENT_FRIENDLY_FACT_STATS_CD,
|
||||
] as const;
|
||||
|
||||
const SEWERAGE_SERVICE_LEVEL_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY: Record<string, string> = {
|
||||
[ComplaintInpColumn.tabKey]: SEWERAGE_SERVICE_LEVEL_COMPLAINT_STATS_CD,
|
||||
[ResidentFriendlyFactInpColumn.tabKey]: SEWERAGE_SERVICE_LEVEL_RESIDENT_FRIENDLY_FACT_STATS_CD,
|
||||
@@ -326,42 +323,30 @@ const SewerageServiceLevelInp = () => {
|
||||
const publicSewageFacilitiesAvailableRef = useRef(false);
|
||||
activeTabRef.current = activeTab;
|
||||
|
||||
const activeTabId = useTabStore((s) => s.activeTabId);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWERAGE_SERVICE_LEVEL) return;
|
||||
const tabKey = activeTab;
|
||||
if (!tabKey) return;
|
||||
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
|
||||
const statsCd =
|
||||
SEWERAGE_SERVICE_LEVEL_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY[tabKey] ?? tabKey;
|
||||
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const entries = await Promise.all(
|
||||
SEWERAGE_SERVICE_LEVEL_COLUMN_GUIDE_STATS_CDS.map(async (statsCd) => {
|
||||
try {
|
||||
const res = await statisticsApi.getColumnGuides(statsCd);
|
||||
if (res.status !== "success" || !Array.isArray(res.data)) {
|
||||
return [statsCd, {}] as const;
|
||||
}
|
||||
const next: Record<string, string> = {};
|
||||
for (const item of res.data) {
|
||||
const key = item.engColNm?.trim();
|
||||
const guide = item.columnGuide?.trim();
|
||||
if (key && guide) next[key] = guide;
|
||||
}
|
||||
return [statsCd, next] as const;
|
||||
} catch {
|
||||
return [statsCd, {}] as const;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const next = await fetchColumnGuidesRecord(statsCd);
|
||||
if (cancelled) return;
|
||||
const guidesByStatsCd = Object.fromEntries(entries);
|
||||
setColumnGuidesByTab(
|
||||
Object.fromEntries(
|
||||
Object.entries(SEWERAGE_SERVICE_LEVEL_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY).map(
|
||||
([tabKey, statsCd]) => [tabKey, guidesByStatsCd[statsCd] ?? {}],
|
||||
),
|
||||
),
|
||||
setColumnGuidesByTab((prev) =>
|
||||
Object.prototype.hasOwnProperty.call(prev, tabKey)
|
||||
? prev
|
||||
: { ...prev, [tabKey]: next },
|
||||
);
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
}, [activeTabId, activeTab, columnGuidesByTab]);
|
||||
|
||||
const commitToolbarSnapshotForLoadedTab = (
|
||||
tabKey: string,
|
||||
|
||||
@@ -39,6 +39,8 @@ import { RecycledWaterStatusInpColumn } from "@/components/util/column/input/wat
|
||||
import { TreatWaterReuseBusinessApprStatusInpColumn } from "@/components/util/column/input/waterReuse/TreatWaterReuseBusinessApprStatusInpColumn";
|
||||
import { TreatWaterReuseDesignConstStatusInpColumn } from "@/components/util/column/input/waterReuse/TreatWaterReuseDesignConstStatusInpColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
import {
|
||||
patchVerificationFailedByTab,
|
||||
} from "@/components/util/input/verification-failed-flag";
|
||||
@@ -94,14 +96,6 @@ const WaterReuseInpTabList = [
|
||||
|
||||
const firstTabKey = PublicSewageTreatWaterReuseInpColumn.tabKey;
|
||||
|
||||
const WATER_REUSE_COLUMN_GUIDE_STATS_CDS = [
|
||||
WATER_REUSE_PUBLIC_SEWAGE_TREAT_STATS_CD,
|
||||
WATER_REUSE_RECYCLED_WATER_STATS_CD,
|
||||
WATER_REUSE_RAINFALL_UTIL_FACT_STATS_CD,
|
||||
WATER_REUSE_DESIGN_CONST_STATS_CD,
|
||||
WATER_REUSE_BUSINESS_APPR_STATS_CD,
|
||||
] as const;
|
||||
|
||||
const WATER_REUSE_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY: Record<string, string> = {
|
||||
[PublicSewageTreatWaterReuseInpColumn.tabKey]: WATER_REUSE_PUBLIC_SEWAGE_TREAT_STATS_CD,
|
||||
[RecycledWaterStatusInpColumn.tabKey]: WATER_REUSE_RECYCLED_WATER_STATS_CD,
|
||||
@@ -569,42 +563,29 @@ const WaterReuseInp = () => {
|
||||
const loadedTabKeysRef = useRef<Set<string>>(new Set());
|
||||
activeTabRef.current = activeTab;
|
||||
|
||||
const activeTabId = useTabStore((s) => s.activeTabId);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTabId !== STATISTICS_INPUT_URL_PATH.WATER_REUSE) return;
|
||||
const tabKey = activeTab;
|
||||
if (!tabKey) return;
|
||||
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
|
||||
const statsCd = WATER_REUSE_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY[tabKey] ?? tabKey;
|
||||
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const entries = await Promise.all(
|
||||
WATER_REUSE_COLUMN_GUIDE_STATS_CDS.map(async (statsCd) => {
|
||||
try {
|
||||
const res = await statisticsApi.getColumnGuides(statsCd);
|
||||
if (res.status !== "success" || !Array.isArray(res.data)) {
|
||||
return [statsCd, {}] as const;
|
||||
}
|
||||
const next: Record<string, string> = {};
|
||||
for (const item of res.data) {
|
||||
const key = item.engColNm?.trim();
|
||||
const guide = item.columnGuide?.trim();
|
||||
if (key && guide) next[key] = guide;
|
||||
}
|
||||
return [statsCd, next] as const;
|
||||
} catch {
|
||||
return [statsCd, {}] as const;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const next = await fetchColumnGuidesRecord(statsCd);
|
||||
if (cancelled) return;
|
||||
const guidesByStatsCd = Object.fromEntries(entries);
|
||||
setColumnGuidesByTab(
|
||||
Object.fromEntries(
|
||||
Object.entries(WATER_REUSE_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY).map(
|
||||
([tabKey, statsCd]) => [tabKey, guidesByStatsCd[statsCd] ?? {}],
|
||||
),
|
||||
),
|
||||
setColumnGuidesByTab((prev) =>
|
||||
Object.prototype.hasOwnProperty.call(prev, tabKey)
|
||||
? prev
|
||||
: { ...prev, [tabKey]: next },
|
||||
);
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
}, [activeTabId, activeTab, columnGuidesByTab]);
|
||||
|
||||
const loadSggOptions = useCallback(async (sido: string): Promise<Sgg[]> => {
|
||||
if (!sido || sido === "ALL") {
|
||||
|
||||
@@ -156,11 +156,11 @@ const GeneralVerificationManagement = () => {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체</SelectItem>
|
||||
<SelectItem value="DO_SI">
|
||||
도단위 검증
|
||||
<SelectItem value="TOTAL">
|
||||
총계 검증
|
||||
</SelectItem>
|
||||
<SelectItem value="SI_GU">
|
||||
시단위 검증
|
||||
<SelectItem value="ROW">
|
||||
행 검증
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
+411
-144
@@ -1,55 +1,93 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { useConfirm } from '@frontend-opensource/use-react-hooks';
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
|
||||
import { useAlertStore } from '@/shared/lib';
|
||||
import { NoData } from '@/shared/ui/no-data';
|
||||
import { generalverificationApiService } from '@/entities/statistics/api/general-verification-service';
|
||||
import type { VerifyDetailData, VerifyDetailResponseDto } from '@/entities/statistics/model/general-verification-interface';
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { NoData } from "@/shared/ui/no-data";
|
||||
import { generalverificationApiService } from "@/entities/statistics/api/general-verification-service";
|
||||
import type {
|
||||
VerifyDetailData,
|
||||
VerifyDetailResponseDto,
|
||||
} from "@/entities/statistics/model/general-verification-interface";
|
||||
import {
|
||||
Button,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { LoadingWithAnimation } from '@common/components';
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
import { getUserInfo } from '../../../../../../../packages/common/components/src/api';
|
||||
import { getUserInfo } from "../../../../../../../packages/common/components/src/api";
|
||||
|
||||
import FomulaDialog from './FomulaDialog';
|
||||
import FormulaComponents from './FormulaComponents';
|
||||
import FomulaDialog from "./FomulaDialog";
|
||||
import FormulaComponents from "./FormulaComponents";
|
||||
|
||||
type ConnectorValue = "and" | "or" | "cond";
|
||||
type ClauseZone = "" | "zone-condition" | "zone-result";
|
||||
|
||||
const CONNECTOR_OPTIONS: { value: ConnectorValue; label: string }[] = [
|
||||
{ value: "and", label: "그리고(AND)" },
|
||||
{ value: "or", label: "또는(OR)" },
|
||||
{ value: "cond", label: "조건연산자" },
|
||||
];
|
||||
|
||||
interface DetailComponentProps {
|
||||
selectedVrfcRow: number | null;
|
||||
}
|
||||
|
||||
const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
|
||||
|
||||
const [detailInfo, setDetailInfo] = useState<VerifyDetailData | null>(null);
|
||||
const [formulaList, setFormulaList] = useState<(VerifyDetailResponseDto | null)[]>([]);
|
||||
const [hasPersistedDetails, setHasPersistedDetails] = useState<boolean>(false);
|
||||
const [formulaList, setFormulaList] = useState<
|
||||
(VerifyDetailResponseDto | null)[]
|
||||
>([]);
|
||||
/** UI 전용 — 수식 행 사이 연결 연산자 (and / or / 조건연산자) */
|
||||
const [connectors, setConnectors] = useState<ConnectorValue[]>([]);
|
||||
/** UI 전용 — 수식 행별 group by */
|
||||
const [groupByFlags, setGroupByFlags] = useState<boolean[]>([]);
|
||||
const [hasPersistedDetails, setHasPersistedDetails] =
|
||||
useState<boolean>(false);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [showFomulaDialog, setShowFomulaDialog] = useState<boolean>(false);
|
||||
const [selectedFormula, setSelectedFormula] = useState<VerifyDetailResponseDto | null>(null);
|
||||
const [selectedFormula, setSelectedFormula] =
|
||||
useState<VerifyDetailResponseDto | null>(null);
|
||||
const [selectedFormulaIndex, setSelectedFormulaIndex] = useState<number>(0);
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const persistedFormulaSignatureRef = useRef<string>('');
|
||||
const persistedFormulaSignatureRef = useRef<string>("");
|
||||
|
||||
const normalizeFormulaListForCompare = (list: (VerifyDetailResponseDto | null)[]) => {
|
||||
const syncUiMetaForListLength = (length: number) => {
|
||||
setConnectors(Array.from({ length: Math.max(0, length - 1) }, () => "and"));
|
||||
setGroupByFlags(Array.from({ length }, () => false));
|
||||
};
|
||||
|
||||
const conditionBoundary = connectors.findIndex((c) => c === "cond");
|
||||
|
||||
const zoneFor = (index: number): ClauseZone => {
|
||||
if (conditionBoundary === -1) return "";
|
||||
return index <= conditionBoundary ? "zone-condition" : "zone-result";
|
||||
};
|
||||
|
||||
const normalizeFormulaListForCompare = (
|
||||
list: (VerifyDetailResponseDto | null)[],
|
||||
) => {
|
||||
return list.map((row) =>
|
||||
row
|
||||
? {
|
||||
vrfcDtlSn: row.vrfcDtlSn ?? null,
|
||||
refStatsCd: row.refStatsCd ?? null,
|
||||
kornArtopnEqtnCn: row.kornArtopnEqtnCn ?? '',
|
||||
engArtopnEqtnCn: row.engArtopnEqtnCn ?? '',
|
||||
cmprOpert: row.cmprOpert ?? null,
|
||||
}
|
||||
vrfcDtlSn: row.vrfcDtlSn ?? null,
|
||||
refStatsCd: row.refStatsCd ?? null,
|
||||
kornArtopnEqtnCn: row.kornArtopnEqtnCn ?? "",
|
||||
engArtopnEqtnCn: row.engArtopnEqtnCn ?? "",
|
||||
cmprOpert: row.cmprOpert ?? null,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
};
|
||||
@@ -57,61 +95,76 @@ const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
|
||||
const handleSaveClick = async () => {
|
||||
const trgtStatsCd = detailInfo?.commonInfo.trgtStatsCd;
|
||||
if (!trgtStatsCd) {
|
||||
alert('대상 통계항목(trgtStatsCd) 정보가 없습니다.');
|
||||
alert("대상 통계항목(trgtStatsCd) 정보가 없습니다.");
|
||||
return;
|
||||
}
|
||||
const rows = formulaList.map((row, idx) => ({
|
||||
eqtnCombSeq: idx + 1,
|
||||
trgtStatsCd,
|
||||
refStatsCd: row?.refStatsCd ?? null,
|
||||
kornArtopnEqtnCn: row?.kornArtopnEqtnCn ?? '',
|
||||
engArtopnEqtnCn: row?.engArtopnEqtnCn ?? '',
|
||||
kornArtopnEqtnCn: row?.kornArtopnEqtnCn ?? "",
|
||||
engArtopnEqtnCn: row?.engArtopnEqtnCn ?? "",
|
||||
cmprOpert: row?.cmprOpert ?? null,
|
||||
}));
|
||||
|
||||
const UNARY_TERMINAL_OPERATORS = new Set<string>(['is_null', '!is_null', 'floor']);
|
||||
const UNARY_TERMINAL_OPERATORS = new Set<string>([
|
||||
"is_null",
|
||||
"!is_null",
|
||||
"floor",
|
||||
]);
|
||||
const lastOperator = rows.at(-1)?.cmprOpert ?? null;
|
||||
const lastIsUnary = rows.length > 0 && lastOperator != null && UNARY_TERMINAL_OPERATORS.has(lastOperator);
|
||||
const lastIsUnary =
|
||||
rows.length > 0 &&
|
||||
lastOperator != null &&
|
||||
UNARY_TERMINAL_OPERATORS.has(lastOperator);
|
||||
const isSingleUnaryFormula = rows.length === 1 && lastIsUnary;
|
||||
|
||||
// 단항 연산자일 때만 row 1개 허용
|
||||
if (rows.length === 1 && !isSingleUnaryFormula) {
|
||||
alert('해당 연산자는 최소 2개 항목이 필요합니다.');
|
||||
alert("해당 연산자는 최소 2개 항목이 필요합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 단항 연산자(is_null, !is_null, floor)는 반드시 맨 마지막 항목에만 허용
|
||||
if (rows.length > 1) {
|
||||
const unaryAtWrongIdx = rows.findIndex(
|
||||
(r, idx) => idx < rows.length - 1 && r.cmprOpert != null && UNARY_TERMINAL_OPERATORS.has(r.cmprOpert),
|
||||
(r, idx) =>
|
||||
idx < rows.length - 1 &&
|
||||
r.cmprOpert != null &&
|
||||
UNARY_TERMINAL_OPERATORS.has(r.cmprOpert),
|
||||
);
|
||||
if (unaryAtWrongIdx !== -1) {
|
||||
alert(`단항 연산자(is_null, !is_null, floor)는 맨 마지막 항목에만 사용할 수 있습니다.\n(순번: ${unaryAtWrongIdx + 1})`);
|
||||
alert(
|
||||
`단항 연산자(is_null, !is_null, floor)는 맨 마지막 항목에만 사용할 수 있습니다.\n(순번: ${unaryAtWrongIdx + 1})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const invalidIdx = rows.findIndex(
|
||||
(r) => r.kornArtopnEqtnCn.trim().length === 0 || r.engArtopnEqtnCn.trim().length === 0,
|
||||
(r) =>
|
||||
r.kornArtopnEqtnCn.trim().length === 0 ||
|
||||
r.engArtopnEqtnCn.trim().length === 0,
|
||||
);
|
||||
if (invalidIdx !== -1) {
|
||||
alert(`수식이 비어있는 항목이 있습니다.\n(순번: ${invalidIdx + 1})`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 마지막이 단항 연산자가 아닐 때만: 마지막을 제외한 항목들은 연산자(cmprOpert) 필수
|
||||
if (rows.length > 1 && !lastIsUnary) {
|
||||
const invalidOpertIdx = rows.slice(0, -1).findIndex((r) => r.cmprOpert == null);
|
||||
const invalidOpertIdx = rows
|
||||
.slice(0, -1)
|
||||
.findIndex((r) => r.cmprOpert == null);
|
||||
if (invalidOpertIdx !== -1) {
|
||||
alert(`연산자가 선택되지 않은 항목이 있습니다.\n(순번: ${invalidOpertIdx + 1})`);
|
||||
alert(
|
||||
`연산자가 선택되지 않은 항목이 있습니다.\n(순번: ${invalidOpertIdx + 1})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 여러 항목일 때 마지막이 단항 연산자면:
|
||||
// 이전 항목들의 연산자는 '?'일 때만 등록 허용 (자동 null 보정하지 않음)
|
||||
if (rows.length > 1 && lastIsUnary) {
|
||||
const invalidUnaryPrevIdx = rows.slice(0, -1).findIndex((r) => r.cmprOpert !== '?');
|
||||
const invalidUnaryPrevIdx = rows
|
||||
.slice(0, -1)
|
||||
.findIndex((r) => r.cmprOpert !== "?");
|
||||
if (invalidUnaryPrevIdx !== -1) {
|
||||
alert(
|
||||
`마지막 항목이 단항 연산자(is_null, !is_null, floor)일 때는 이전 항목의 연산자를 '?'로 선택해야 합니다.\n(순번: ${invalidUnaryPrevIdx + 1})`,
|
||||
@@ -120,7 +173,6 @@ const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 마지막 항목의 연산자는 (단항 연산자 케이스 제외) 무조건 null 처리
|
||||
if (rows.length > 1 && !lastIsUnary) {
|
||||
const lastRow = rows[rows.length - 1];
|
||||
if (lastRow) lastRow.cmprOpert = null;
|
||||
@@ -135,25 +187,38 @@ const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (await confirm('등록하시겠습니까?')) {
|
||||
if (await confirm("등록하시겠습니까?")) {
|
||||
try {
|
||||
const userInfo = await getUserInfo();
|
||||
let response;
|
||||
|
||||
if (!hasPersistedDetails) {
|
||||
response = await generalverificationApiService.RegistVrfcDetail({ vrfcSn: detailInfo?.commonInfo.vrfcSn ?? null, detailList: rows, rgtrId: userInfo.userUnqId });
|
||||
response =
|
||||
await generalverificationApiService.RegistVrfcDetail({
|
||||
vrfcSn: detailInfo?.commonInfo.vrfcSn ?? null,
|
||||
detailList: rows,
|
||||
rgtrId: userInfo.userUnqId,
|
||||
});
|
||||
} else {
|
||||
response = await generalverificationApiService.UpdateVrfcDetail({ vrfcSn: detailInfo?.commonInfo.vrfcSn ?? null, detailList: rows, mdfrId: userInfo.userUnqId });
|
||||
response =
|
||||
await generalverificationApiService.UpdateVrfcDetail({
|
||||
vrfcSn: detailInfo?.commonInfo.vrfcSn ?? null,
|
||||
detailList: rows,
|
||||
mdfrId: userInfo.userUnqId,
|
||||
});
|
||||
}
|
||||
|
||||
if (response?.status === 'success') {
|
||||
alert('등록되었습니다.');
|
||||
if (response?.status === "success") {
|
||||
alert("등록되었습니다.");
|
||||
getVrfcDetail();
|
||||
} else {
|
||||
alert(response?.message || '데이터 등록 중 오류가 발생했습니다.');
|
||||
alert(
|
||||
response?.message ||
|
||||
"데이터 등록 중 오류가 발생했습니다.",
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
alert('데이터 등록 중 오류가 발생했습니다.');
|
||||
alert("데이터 등록 중 오류가 발생했습니다.");
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -162,34 +227,45 @@ const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const response = await generalverificationApiService.getVrfcDetail(selectedVrfcRow!);
|
||||
const response =
|
||||
await generalverificationApiService.getVrfcDetail(
|
||||
selectedVrfcRow!,
|
||||
);
|
||||
|
||||
if (response.status === 'success') {
|
||||
if (response.status === "success") {
|
||||
setDetailInfo(response.data);
|
||||
const list = response.data.verifyDetailResponseDtoList;
|
||||
setFormulaList(list?.length ? [...list] : [null]);
|
||||
const nextList = list?.length ? [...list] : [null];
|
||||
setFormulaList(nextList);
|
||||
syncUiMetaForListLength(nextList.length);
|
||||
setHasPersistedDetails((list?.length ?? 0) > 0);
|
||||
// "기존 데이터" 스냅샷(서버 응답 기준)을 시그니처로 저장
|
||||
persistedFormulaSignatureRef.current = JSON.stringify(
|
||||
normalizeFormulaListForCompare(list?.length ? [...list] : [null]),
|
||||
normalizeFormulaListForCompare(nextList),
|
||||
);
|
||||
} else {
|
||||
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
|
||||
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickVrfcDetail = (formula?: VerifyDetailResponseDto | null, index?: number) => {
|
||||
const handleClickVrfcDetail = (
|
||||
formula?: VerifyDetailResponseDto | null,
|
||||
index?: number,
|
||||
) => {
|
||||
setSelectedFormula(formula ?? null);
|
||||
setSelectedFormulaIndex(index ?? 0);
|
||||
setShowFomulaDialog(true);
|
||||
};
|
||||
|
||||
const handleFormulaConfirm = (payload: { kornArtopnEqtnCn: string; engArtopnEqtnCn: string; refStatsCd: string | null }) => {
|
||||
const handleFormulaConfirm = (payload: {
|
||||
kornArtopnEqtnCn: string;
|
||||
engArtopnEqtnCn: string;
|
||||
refStatsCd: string | null;
|
||||
}) => {
|
||||
setFormulaList((prev) => {
|
||||
const next = [...prev];
|
||||
const i = selectedFormulaIndex;
|
||||
@@ -197,19 +273,19 @@ const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
|
||||
const item = next[i];
|
||||
next[i] = item
|
||||
? {
|
||||
...item,
|
||||
kornArtopnEqtnCn: payload.kornArtopnEqtnCn,
|
||||
engArtopnEqtnCn: payload.engArtopnEqtnCn,
|
||||
refStatsCd: payload.refStatsCd,
|
||||
}
|
||||
...item,
|
||||
kornArtopnEqtnCn: payload.kornArtopnEqtnCn,
|
||||
engArtopnEqtnCn: payload.engArtopnEqtnCn,
|
||||
refStatsCd: payload.refStatsCd,
|
||||
}
|
||||
: {
|
||||
vrfcDtlSn: null,
|
||||
refStatsCd: payload.refStatsCd,
|
||||
refStatsNm: null,
|
||||
kornArtopnEqtnCn: payload.kornArtopnEqtnCn,
|
||||
engArtopnEqtnCn: payload.engArtopnEqtnCn,
|
||||
cmprOpert: null,
|
||||
};
|
||||
vrfcDtlSn: null,
|
||||
refStatsCd: payload.refStatsCd,
|
||||
refStatsNm: null,
|
||||
kornArtopnEqtnCn: payload.kornArtopnEqtnCn,
|
||||
engArtopnEqtnCn: payload.engArtopnEqtnCn,
|
||||
cmprOpert: null,
|
||||
};
|
||||
return next;
|
||||
});
|
||||
setShowFomulaDialog(false);
|
||||
@@ -217,17 +293,50 @@ const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
|
||||
};
|
||||
|
||||
const handleResetClick = async () => {
|
||||
const currentSignature = JSON.stringify(normalizeFormulaListForCompare(formulaList));
|
||||
const currentSignature = JSON.stringify(
|
||||
normalizeFormulaListForCompare(formulaList),
|
||||
);
|
||||
if (currentSignature === persistedFormulaSignatureRef.current) {
|
||||
alert('변경된 내용이 없습니다.');
|
||||
alert("변경된 내용이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (await confirm(`변경 전 데이터로 리셋하시겠습니까?\n등록 완료된 데이터는 등록 전 데이터를 불러올 수 없습니다.`)) {
|
||||
if (
|
||||
await confirm(
|
||||
`변경 전 데이터로 리셋하시겠습니까?\n등록 완료된 데이터는 등록 전 데이터를 불러올 수 없습니다.`,
|
||||
)
|
||||
) {
|
||||
getVrfcDetail();
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddClause = () => {
|
||||
setFormulaList((prev) => [...prev, null]);
|
||||
setConnectors((prev) => [...prev, "and"]);
|
||||
setGroupByFlags((prev) => [...prev, false]);
|
||||
};
|
||||
|
||||
const handleRemoveClause = (index: number) => {
|
||||
setFormulaList((prev) => {
|
||||
const next = prev.filter((_, i) => i !== index);
|
||||
return next.length > 0 ? next : [null];
|
||||
});
|
||||
setConnectors((prev) => {
|
||||
if (prev.length === 0) return prev;
|
||||
if (index < prev.length) {
|
||||
return prev.filter((_, i) => i !== index);
|
||||
}
|
||||
if (index - 1 >= 0) {
|
||||
return prev.filter((_, i) => i !== index - 1);
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
setGroupByFlags((prev) => {
|
||||
const next = prev.filter((_, i) => i !== index);
|
||||
return next.length > 0 ? next : [false];
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedVrfcRow != null) {
|
||||
getVrfcDetail();
|
||||
@@ -236,20 +345,23 @@ const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='card card-border w-1/2 !mx-0 flex flex-col'>
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">검증 상세 정보</h3>
|
||||
<div className="card card-border !mx-0 flex h-full min-h-0 w-1/2 flex-col">
|
||||
<div className="card-header shrink-0">
|
||||
<h3 className="text-2xl font-medium text-label">
|
||||
검증 상세 정보
|
||||
</h3>
|
||||
</div>
|
||||
<div className="card !m-4 h-full !p-0">
|
||||
{selectedVrfcRow ?
|
||||
isLoading ?
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="card !m-4 flex min-h-0 flex-1 flex-col !p-0">
|
||||
{selectedVrfcRow ? (
|
||||
isLoading ? (
|
||||
<div className="absolute inset-0 z-50 flex items-center justify-center rounded-lg p-10 [data-theme='black']:bg-transparent">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div className='flex flex-col gap-12 m-2 h-full'>
|
||||
) : (
|
||||
<div className="m-2 flex min-h-0 flex-1 flex-col gap-4">
|
||||
<div className="shrink-0">
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="25%" />
|
||||
@@ -260,80 +372,230 @@ const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>검증명</TableHead>
|
||||
<TableCell className='text-center'>{detailInfo?.commonInfo.vrfcNm ?? "-"}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{detailInfo?.commonInfo.vrfcNm ??
|
||||
"-"}
|
||||
</TableCell>
|
||||
<TableHead>통계항목</TableHead>
|
||||
<TableCell className='text-center'>{detailInfo?.commonInfo.trgtStatsNm ?? "-"}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{detailInfo?.commonInfo
|
||||
.trgtStatsNm ?? "-"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>검증분류</TableHead>
|
||||
<TableCell className='text-center'>{detailInfo?.commonInfo.vrfcClsfNm ?? "-"}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{detailInfo?.commonInfo
|
||||
.vrfcClsfNm ?? "-"}
|
||||
</TableCell>
|
||||
<TableHead>소수점처리방식</TableHead>
|
||||
<TableCell className='text-center'>
|
||||
{detailInfo?.commonInfo.dcptDigtCnt === 0 ?
|
||||
'소수점 생략'
|
||||
:
|
||||
`${detailInfo?.commonInfo.dcptDigtCnt}자리에서 ${detailInfo?.commonInfo.dcptPrcsMthNm}`
|
||||
}
|
||||
<TableCell className="text-center">
|
||||
{detailInfo?.commonInfo
|
||||
.dcptDigtCnt === 0
|
||||
? "소수점 생략"
|
||||
: `${detailInfo?.commonInfo.dcptDigtCnt}자리에서 ${detailInfo?.commonInfo.dcptPrcsMthNm}`}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className='flex flex-col gap-4 overflow-auto'>
|
||||
<h3 className="font-bold text-title">수식 정보</h3>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-auto px-1">
|
||||
<div className="mb-1 font-bold text-[#33373f]">
|
||||
수식 정보
|
||||
</div>
|
||||
<div className="mb-3.5 flex gap-4 text-[#6b7280]">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="inline-block size-3 rounded-sm bg-[#dbe8ff]" />
|
||||
기준 대상
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="inline-block size-3 rounded-sm bg-[#ffe9d6]" />
|
||||
비교대상
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{formulaList.map((data, index) => (
|
||||
<FormulaComponents
|
||||
key={data?.vrfcDtlSn ?? `empty-${index}`}
|
||||
data={data}
|
||||
index={index}
|
||||
onOperatorChange={(v) => {
|
||||
setFormulaList((prev) => {
|
||||
const next = [...prev];
|
||||
if (index < 0 || index >= next.length) return prev;
|
||||
const current = next[index];
|
||||
const cmprOpert = v === 'none' ? null : v;
|
||||
next[index] = current
|
||||
? { ...current, cmprOpert }
|
||||
: {
|
||||
vrfcDtlSn: null,
|
||||
refStatsCd: null,
|
||||
refStatsNm: null,
|
||||
kornArtopnEqtnCn: '',
|
||||
engArtopnEqtnCn: '',
|
||||
cmprOpert,
|
||||
};
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
onDelete={() => {
|
||||
setFormulaList((prev) => {
|
||||
const next = prev.filter((_, i) => i !== index);
|
||||
return next.length > 0 ? next : [null];
|
||||
});
|
||||
}}
|
||||
handleFomulaDialog={(show, formula, idx) => {
|
||||
if (show) handleClickVrfcDetail(formula, idx ?? index);
|
||||
else setShowFomulaDialog(false);
|
||||
}}
|
||||
/>
|
||||
<div key={data?.vrfcDtlSn ?? `empty-${index}`}>
|
||||
<FormulaComponents
|
||||
data={data}
|
||||
index={index}
|
||||
zone={zoneFor(index)}
|
||||
groupBy={
|
||||
groupByFlags[index] ?? false
|
||||
}
|
||||
onGroupByChange={(checked) => {
|
||||
setGroupByFlags((prev) => {
|
||||
const next = [...prev];
|
||||
while (
|
||||
next.length <=
|
||||
index
|
||||
) {
|
||||
next.push(false);
|
||||
}
|
||||
next[index] = checked;
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
onOperatorChange={(v) => {
|
||||
setFormulaList((prev) => {
|
||||
const next = [...prev];
|
||||
if (
|
||||
index < 0 ||
|
||||
index >= next.length
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
const current =
|
||||
next[index];
|
||||
const cmprOpert =
|
||||
v === "none"
|
||||
? null
|
||||
: v;
|
||||
next[index] = current
|
||||
? {
|
||||
...current,
|
||||
cmprOpert,
|
||||
}
|
||||
: {
|
||||
vrfcDtlSn:
|
||||
null,
|
||||
refStatsCd:
|
||||
null,
|
||||
refStatsNm:
|
||||
null,
|
||||
kornArtopnEqtnCn:
|
||||
"",
|
||||
engArtopnEqtnCn:
|
||||
"",
|
||||
cmprOpert,
|
||||
};
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
onDelete={() =>
|
||||
handleRemoveClause(index)
|
||||
}
|
||||
handleFomulaDialog={(
|
||||
show,
|
||||
formula,
|
||||
idx,
|
||||
) => {
|
||||
if (show) {
|
||||
handleClickVrfcDetail(
|
||||
formula,
|
||||
idx ?? index,
|
||||
);
|
||||
} else {
|
||||
setShowFomulaDialog(
|
||||
false,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{index < connectors.length && (
|
||||
<div className="flex items-center justify-center gap-2 pt-1 pb-2">
|
||||
<div className="h-px flex-1 bg-[#e2e5ea]" />
|
||||
<Select
|
||||
value={
|
||||
connectors[index]
|
||||
}
|
||||
onValueChange={(v) => {
|
||||
setConnectors(
|
||||
(prev) => {
|
||||
const next =
|
||||
[
|
||||
...prev,
|
||||
];
|
||||
next[
|
||||
index
|
||||
] =
|
||||
v as ConnectorValue;
|
||||
return next;
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={`h-[3.2rem] w-[150px] rounded-full border text-center font-semibold focus:ring-0 ${
|
||||
connectors[
|
||||
index
|
||||
] === "cond"
|
||||
? "border-[#3a5bc7] bg-[#eef3ff] text-[#3a5bc7]"
|
||||
: "border-[#d8dce3] bg-white text-[#33373f]"
|
||||
}`}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-[150px]">
|
||||
{CONNECTOR_OPTIONS.map(
|
||||
(opt) => {
|
||||
const disabled =
|
||||
opt.value ===
|
||||
"cond" &&
|
||||
conditionBoundary !==
|
||||
-1 &&
|
||||
conditionBoundary !==
|
||||
index;
|
||||
return (
|
||||
<SelectItem
|
||||
key={
|
||||
opt.value
|
||||
}
|
||||
value={
|
||||
opt.value
|
||||
}
|
||||
disabled={
|
||||
disabled
|
||||
}
|
||||
>
|
||||
{
|
||||
opt.label
|
||||
}
|
||||
</SelectItem>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="h-px flex-1 bg-[#e2e5ea]" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="mt-2.5 flex justify-center">
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
className="size-[4rem] rounded-full"
|
||||
onClick={handleAddClause}
|
||||
>
|
||||
<i className="ri-play-list-add-fill text-[16px]" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 border-t border-[#eef0f3] px-1 py-3 text-[#8a8f98]">
|
||||
조건연산자는 규칙 전체에서 1개만 선택할
|
||||
수 있습니다. 조건연산자를 기준으로
|
||||
앞쪽은 기준 대상, 뒤쪽은 비교대상으로
|
||||
구분됩니다.
|
||||
{conditionBoundary === -1 && (
|
||||
<span className="font-semibold block text-[#f24e5b]">
|
||||
{" "}
|
||||
현재 조건연산자가 선택되지 않아
|
||||
전체가 비교대상과 동일하게
|
||||
처리됩니다.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex justify-center mb-4'>
|
||||
|
||||
<div className="flex shrink-0 items-center justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
className="size-[4rem] rounded-full"
|
||||
onClick={() => {
|
||||
setFormulaList((prev) => [...prev, null]);
|
||||
}}
|
||||
>
|
||||
<i className="ri-play-list-add-fill text-[16px]"></i>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 flex gap-2 justify-end items-end">
|
||||
<Button
|
||||
type="button"
|
||||
color='red'
|
||||
title='리셋'
|
||||
color="red"
|
||||
title="리셋"
|
||||
onClick={handleResetClick}
|
||||
>
|
||||
<i className="diveicon di-arrow-reset" />
|
||||
@@ -346,15 +608,20 @@ const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
)
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<NoData />
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<FomulaDialog
|
||||
key={showFomulaDialog ? (selectedFormula?.vrfcDtlSn ?? 'new') : 'closed'}
|
||||
key={
|
||||
showFomulaDialog
|
||||
? (selectedFormula?.vrfcDtlSn ?? "new")
|
||||
: "closed"
|
||||
}
|
||||
open={showFomulaDialog}
|
||||
onOpenChange={(open) => {
|
||||
setShowFomulaDialog(open);
|
||||
@@ -369,4 +636,4 @@ const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailComponent;
|
||||
export default DetailComponent;
|
||||
|
||||
+57
-15
@@ -1,41 +1,83 @@
|
||||
import { Button, Input } from "@common/components";;
|
||||
import OperatorsComponents from "./OperatorsComponents";
|
||||
import { Button, Input } from "@common/components";
|
||||
|
||||
import type { VerifyDetailResponseDto } from "@/entities/statistics/model/general-verification-interface";
|
||||
|
||||
import OperatorsComponents from "./OperatorsComponents";
|
||||
|
||||
type ClauseZone = "" | "zone-condition" | "zone-result";
|
||||
|
||||
interface FormulaComponentsProps {
|
||||
data?: VerifyDetailResponseDto | null;
|
||||
index?: number;
|
||||
zone?: ClauseZone;
|
||||
groupBy?: boolean;
|
||||
onGroupByChange?: (checked: boolean) => void;
|
||||
onDelete?: () => void;
|
||||
onOperatorChange?: (value: string) => void;
|
||||
handleFomulaDialog?: (show: boolean, data?: VerifyDetailResponseDto | null, index?: number) => void;
|
||||
handleFomulaDialog?: (
|
||||
show: boolean,
|
||||
data?: VerifyDetailResponseDto | null,
|
||||
index?: number,
|
||||
) => void;
|
||||
}
|
||||
|
||||
const FormulaComponents = ({ data, index, onDelete, onOperatorChange, handleFomulaDialog }: FormulaComponentsProps) => {
|
||||
const zoneClassName: Record<ClauseZone, string> = {
|
||||
"": "bg-[#f9fafb]",
|
||||
"zone-condition": "bg-[#eef4ff]",
|
||||
"zone-result": "bg-[#fff4ea]",
|
||||
};
|
||||
|
||||
const FormulaComponents = ({
|
||||
data,
|
||||
index,
|
||||
zone = "",
|
||||
groupBy = false,
|
||||
onGroupByChange,
|
||||
onDelete,
|
||||
onOperatorChange,
|
||||
handleFomulaDialog,
|
||||
}: FormulaComponentsProps) => {
|
||||
const cmprOpert = data?.cmprOpert;
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-x-4 mb-2 p-2 justify-around rounded has-[.formulaDeleteBtn:hover]:bg-gray-100'>
|
||||
<div
|
||||
className={`mb-1.5 flex items-center gap-2.5 rounded-md px-2.5 py-2 ${zoneClassName[zone]}`}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
className="size-[3rem] shrink-0 rounded-full formulaDeleteBtn"
|
||||
color='red'
|
||||
className="formulaDeleteBtn size-[2.6rem] shrink-0 rounded-full !bg-[#f24e5b] hover:!bg-[#d63c48]"
|
||||
color="red"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<i className="ri-subtract-fill text-[16px]"></i>
|
||||
<i className="ri-subtract-fill text-[16px]" />
|
||||
</Button>
|
||||
<Input
|
||||
className='h-[4rem] !cursor-pointer'
|
||||
placeholder='클릭하여 수식을 등록해주세요.'
|
||||
id='formulaInput'
|
||||
value={data ? data.kornArtopnEqtnCn : ''}
|
||||
title={data ? data.kornArtopnEqtnCn : ''}
|
||||
className="h-[3.6rem] flex-1 !cursor-pointer rounded-md border-[#d8dce3] text-[13px]"
|
||||
placeholder="클릭하여 수식을 등록해주세요."
|
||||
id={`formulaInput-${index ?? 0}`}
|
||||
value={data ? data.kornArtopnEqtnCn : ""}
|
||||
title={data ? data.kornArtopnEqtnCn : ""}
|
||||
onClick={() => handleFomulaDialog?.(true, data, index)}
|
||||
readOnly
|
||||
/>
|
||||
<OperatorsComponents cmprOpert={cmprOpert} onValueChange={onOperatorChange} />
|
||||
<div className="w-[200px] shrink-0 [&_button]:!h-[3.6rem] [&_button]:!w-[200px] [&_button]:min-w-[200px] [&_button]:max-w-[200px]">
|
||||
<OperatorsComponents
|
||||
cmprOpert={cmprOpert}
|
||||
onValueChange={onOperatorChange}
|
||||
/>
|
||||
</div>
|
||||
<label className="flex w-[96px] shrink-0 items-center justify-end gap-1.5 text-[#4b5563] cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="cursor-pointer accent-[#3a6df0]"
|
||||
checked={groupBy}
|
||||
onChange={(e) => onGroupByChange?.(e.target.checked)}
|
||||
/>
|
||||
group by
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormulaComponents;
|
||||
export default FormulaComponents;
|
||||
|
||||
@@ -384,9 +384,8 @@ const RegistDialog = memo(
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">공통 검증</SelectItem>
|
||||
<SelectItem value="DO_SI">도단위 검증</SelectItem>
|
||||
<SelectItem value="SI_GU">시단위 검증</SelectItem>
|
||||
<SelectItem value="TOTAL">총계 검증</SelectItem>
|
||||
<SelectItem value="ROW">행 검증</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
+3
-3
@@ -50,9 +50,9 @@ export const PublicSewageFactColumn = {
|
||||
{ name: 'prcsQnt', fieldName: 'prcsQnt', header: { text: '방류량(500m³/일 이상/미만)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
|
||||
// 방류량 - (m³/일)
|
||||
{ name: 'prcsPhysCpct', fieldName: 'prcsPhysCpct', header: { text: '물리적' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'prcsBoilCpct', fieldName: 'prcsBoilCpct', header: { text: '생물학적' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'prcsAltdCpct', fieldName: 'prcsAltdCpct', header: { text: '고도' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'prcsPhysCpct', fieldName: 'prcsPhysCpct', header: { text: '물리적' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'prcsBoilCpct', fieldName: 'prcsBoilCpct', header: { text: '생물학적' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'prcsAltdCpct', fieldName: 'prcsAltdCpct', header: { text: '고도' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 수질
|
||||
{ name: 'inflwBodVl', fieldName: 'inflwBodVl', header: { text: '유입BOD' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
|
||||
/** column-guides API 응답 → fieldName(engColNm) → guide 텍스트 */
|
||||
export function mapColumnGuideListToRecord(
|
||||
data: { engColNm?: string; columnGuide?: string }[],
|
||||
): Record<string, string> {
|
||||
const next: Record<string, string> = {};
|
||||
for (const item of data) {
|
||||
const key = item.engColNm?.trim();
|
||||
const guide = item.columnGuide?.trim();
|
||||
if (key && guide) next[key] = guide;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export async function fetchColumnGuidesRecord(
|
||||
statsCd: string,
|
||||
): Promise<Record<string, string>> {
|
||||
try {
|
||||
const res = await statisticsApi.getColumnGuides(statsCd);
|
||||
if (res.status !== "success" || !Array.isArray(res.data)) {
|
||||
return {};
|
||||
}
|
||||
return mapColumnGuideListToRecord(res.data);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,16 @@ function truncateToFirstDecimal(value: number): number {
|
||||
return Math.floor(value * 10) / 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* IEEE 754 가산 잡음 제거.
|
||||
* 통계 표시 자릿수(대개 0~3)보다 충분히 큰 자리로 반올림해
|
||||
* 112.229999… → 112.23 처럼 만들어 RealGrid `#,##0.000` 표시 오차를 막는다.
|
||||
*/
|
||||
function sanitizeFloatNoise(value: number): number {
|
||||
if (!Number.isFinite(value)) return 0;
|
||||
return Math.round(value * 1e6) / 1e6;
|
||||
}
|
||||
|
||||
/** 합계용 valueCallback 생성 */
|
||||
export function createSumValueCallback(fieldNames: string[]): DataFieldValueCallback {
|
||||
return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => {
|
||||
@@ -22,7 +32,7 @@ export function createSumValueCallback(fieldNames: string[]): DataFieldValueCall
|
||||
const idx = fieldNamesArr.indexOf(f);
|
||||
if (idx >= 0) sum += toNum(values[idx]);
|
||||
}
|
||||
return sum;
|
||||
return sanitizeFloatNoise(sum);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -121,7 +121,7 @@ export interface SewerageSupplyRateSaveRequest {
|
||||
sggNm: string;
|
||||
sewerBzmnCd: string;
|
||||
userId: string;
|
||||
sewerBzmnFrgnrWholPpltnCnt?: number;
|
||||
sewerBzmnArea?: number;
|
||||
saveList?: SewerageSupplyRateSaveItem[];
|
||||
submitList?: SewerageSupplyRateSaveItem[];
|
||||
laePicNm?: string;
|
||||
|
||||
Reference in New Issue
Block a user