공통코드 조회 기반 업무 화면 표시 적용
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import type { GisEditSessionSnapshot, GisEditTool } from "./gisEditTypes";
|
||||
import { useGisEditOperationLabels } from "./useGisEditCommonCodes";
|
||||
|
||||
type Props = GisEditSessionSnapshot & {
|
||||
onRemoveFocusedChange: () => void;
|
||||
@@ -14,13 +15,6 @@ type Props = GisEditSessionSnapshot & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const TOOL_LABELS: Record<GisEditTool, string> = {
|
||||
ADD: "추가",
|
||||
MOVE: "이동",
|
||||
VERTEX_EDIT: "꼭지점편집",
|
||||
DELETE: "삭제",
|
||||
};
|
||||
|
||||
const buildChangeItemClassName = (isFocused: boolean) =>
|
||||
`block w-full rounded-md border px-3 py-2.5 text-left text-[1.1rem] transition ${
|
||||
isFocused
|
||||
@@ -28,8 +22,13 @@ const buildChangeItemClassName = (isFocused: boolean) =>
|
||||
: "border-border bg-background hover:border-primary hover:bg-background"
|
||||
}`;
|
||||
|
||||
const getChangeLabel = (change: GisEditSessionSnapshot["changes"][number]) =>
|
||||
change.attributeOnly ? "상세정보수정" : TOOL_LABELS[change.editType];
|
||||
const getChangeLabel = (
|
||||
change: GisEditSessionSnapshot["changes"][number],
|
||||
operationLabels: Record<GisEditTool, string>,
|
||||
) =>
|
||||
change.attributeOnly
|
||||
? "상세정보수정"
|
||||
: operationLabels[change.editType];
|
||||
|
||||
const getChangeSummary = (
|
||||
change: GisEditSessionSnapshot["changes"][number],
|
||||
@@ -45,12 +44,14 @@ const getChangeSummary = (
|
||||
type ChangeListProps = {
|
||||
changes: GisEditSessionSnapshot["changes"];
|
||||
focusedChangeId: string | null;
|
||||
operationLabels: Record<GisEditTool, string>;
|
||||
onFocusChange: (changeId: string) => void;
|
||||
};
|
||||
|
||||
const ChangeList = ({
|
||||
changes,
|
||||
focusedChangeId,
|
||||
operationLabels,
|
||||
onFocusChange,
|
||||
}: ChangeListProps) => (
|
||||
<>
|
||||
@@ -65,7 +66,8 @@ const ChangeList = ({
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="shrink-0 whitespace-nowrap text-[1.1rem] font-semibold leading-4 text-label">
|
||||
{index + 1}. {getChangeLabel(change)} / {change.objectLabel}
|
||||
{index + 1}. {getChangeLabel(change, operationLabels)} /{" "}
|
||||
{change.objectLabel}
|
||||
</div>
|
||||
<div className="ml-auto flex shrink-0 items-center gap-1">
|
||||
{change.basicInfoSavedAt && focusedChangeId !== change.changeId ? (
|
||||
@@ -103,6 +105,8 @@ export default function GisEditSessionPanel({
|
||||
variant = "inline",
|
||||
className = "",
|
||||
}: Props) {
|
||||
// 인라인·윈도우 팝업 세션 모두 동일한 CMS 공통코드 라벨을 공유한다.
|
||||
const operationLabels = useGisEditOperationLabels();
|
||||
const isCompactInline = variant === "inline" && changes.length === 0;
|
||||
|
||||
return (
|
||||
@@ -125,7 +129,7 @@ export default function GisEditSessionPanel({
|
||||
{selectedLayerName || "레이어 미선택"} ·{" "}
|
||||
{isEditMode
|
||||
? activeTool
|
||||
? TOOL_LABELS[activeTool]
|
||||
? operationLabels[activeTool]
|
||||
: "대기"
|
||||
: "편집 모드 종료"}
|
||||
</div>
|
||||
@@ -190,6 +194,7 @@ export default function GisEditSessionPanel({
|
||||
<ChangeList
|
||||
changes={changes}
|
||||
focusedChangeId={focusedChangeId}
|
||||
operationLabels={operationLabels}
|
||||
onFocusChange={onFocusChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Check, Move, PenTool, Plus, Trash2, X } from "lucide-react";
|
||||
import { Button } from "@common/components/ui";
|
||||
|
||||
import type { GisEditTool } from "./gisEditTypes";
|
||||
import { useGisEditOperationLabels } from "./useGisEditCommonCodes";
|
||||
|
||||
type Props = {
|
||||
isEditMode: boolean;
|
||||
@@ -22,13 +23,12 @@ type Props = {
|
||||
|
||||
const TOOL_BUTTONS: Array<{
|
||||
tool: GisEditTool;
|
||||
label: string;
|
||||
icon: typeof Plus;
|
||||
}> = [
|
||||
{ tool: "ADD", label: "추가", icon: Plus },
|
||||
{ tool: "MOVE", label: "이동", icon: Move },
|
||||
{ tool: "VERTEX_EDIT", label: "꼭지점편집", icon: PenTool },
|
||||
{ tool: "DELETE", label: "삭제", icon: Trash2 },
|
||||
{ tool: "ADD", icon: Plus },
|
||||
{ tool: "MOVE", icon: Move },
|
||||
{ tool: "VERTEX_EDIT", icon: PenTool },
|
||||
{ tool: "DELETE", icon: Trash2 },
|
||||
];
|
||||
|
||||
export default function GisEditToolbar({
|
||||
@@ -45,6 +45,9 @@ export default function GisEditToolbar({
|
||||
onSelectTool,
|
||||
onSave,
|
||||
}: Props) {
|
||||
// 버튼의 업무 코드값은 그대로 유지하고 CMS 공통코드명만 화면 라벨로 사용한다.
|
||||
const operationLabels = useGisEditOperationLabels();
|
||||
|
||||
if (!isEditMode) return null;
|
||||
|
||||
return (
|
||||
@@ -67,10 +70,11 @@ export default function GisEditToolbar({
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 items-center justify-center gap-3">
|
||||
<div className="flex items-center gap-1 rounded-md bg-container p-1">
|
||||
{TOOL_BUTTONS.map(({ tool, label, icon: Icon }) => {
|
||||
{TOOL_BUTTONS.map(({ tool, icon: Icon }) => {
|
||||
const isActive = activeTool === tool;
|
||||
const isToolEnabled =
|
||||
canUseTools && allowedTools.includes(tool);
|
||||
const label = operationLabels[tool];
|
||||
if (tool === "VERTEX_EDIT") {
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { getCommonCodes } from "@/components/lib/common/apiHelpers";
|
||||
|
||||
import type { GisEditTool } from "./gisEditTypes";
|
||||
|
||||
const GIS_EDIT_OPERATION_UP_CD = "GISEDITOPRT";
|
||||
|
||||
/** CMS 조회 실패 시 기존 GIS 편집 화면 동작과 문구를 유지하기 위한 최소 fallback이다. */
|
||||
export const DEFAULT_GIS_EDIT_OPERATION_LABELS: Record<GisEditTool, string> = {
|
||||
ADD: "추가",
|
||||
MOVE: "이동",
|
||||
VERTEX_EDIT: "꼭지점편집",
|
||||
DELETE: "삭제",
|
||||
};
|
||||
|
||||
let cachedOperationLabels: Record<GisEditTool, string> | null = null;
|
||||
let operationLabelsPromise: Promise<Record<GisEditTool, string>> | null = null;
|
||||
|
||||
const isGisEditTool = (value: string): value is GisEditTool =>
|
||||
value === "ADD" ||
|
||||
value === "MOVE" ||
|
||||
value === "VERTEX_EDIT" ||
|
||||
value === "DELETE";
|
||||
|
||||
/** 같은 브라우저 세션에서는 GIS 편집작업코드를 한 번만 조회해 툴바와 세션이 공유한다. */
|
||||
const loadGisEditOperationLabels = async () => {
|
||||
if (cachedOperationLabels) return cachedOperationLabels;
|
||||
|
||||
if (!operationLabelsPromise) {
|
||||
operationLabelsPromise = getCommonCodes(GIS_EDIT_OPERATION_UP_CD)
|
||||
.then((codes) => {
|
||||
const labels = { ...DEFAULT_GIS_EDIT_OPERATION_LABELS };
|
||||
codes.forEach((code) => {
|
||||
if (isGisEditTool(code.lwpoCd) && code.lwpoCdNm) {
|
||||
labels[code.lwpoCd] = code.lwpoCdNm;
|
||||
}
|
||||
});
|
||||
cachedOperationLabels = labels;
|
||||
return labels;
|
||||
})
|
||||
.finally(() => {
|
||||
operationLabelsPromise = null;
|
||||
});
|
||||
}
|
||||
|
||||
return operationLabelsPromise;
|
||||
};
|
||||
|
||||
/** GIS 편집 도구 코드값은 유지하고 사용자 표시명만 CMS 공통코드로 교체한다. */
|
||||
export const useGisEditOperationLabels = () => {
|
||||
const [operationLabels, setOperationLabels] = useState<
|
||||
Record<GisEditTool, string>
|
||||
>(cachedOperationLabels ?? DEFAULT_GIS_EDIT_OPERATION_LABELS);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
loadGisEditOperationLabels()
|
||||
.then((labels) => {
|
||||
if (!cancelled) setOperationLabels(labels);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("GIS 편집작업 공통코드 조회에 실패했습니다.", error);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return operationLabels;
|
||||
};
|
||||
@@ -31,6 +31,7 @@ import { useAlertStore } from "@/shared/lib";
|
||||
import { useUserStore } from "@/shared/lib/use-user-store";
|
||||
import GisUploadPreviewMap from "./GisUploadPreviewMap";
|
||||
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
|
||||
import { useGisManagementCommonCodes } from "./useGisManagementCommonCodes";
|
||||
|
||||
type UploadFacilityType = {
|
||||
facilityTypeCode: string;
|
||||
@@ -108,6 +109,8 @@ type DownloadJob = {
|
||||
retryAfterMillis?: number | null;
|
||||
};
|
||||
|
||||
type DownloadJobStatusListener = (job: DownloadJob) => void;
|
||||
|
||||
const SCREEN_ID = "SN_MN010302";
|
||||
const FACILITY_TYPE_CODE = "PPL";
|
||||
const LAYER_ID = "tb_ms_swage_ppl";
|
||||
@@ -212,12 +215,16 @@ const startDownloadJob = async (url: string): Promise<DownloadJob> => {
|
||||
return job;
|
||||
};
|
||||
|
||||
const waitForDownloadJob = async (jobId: string): Promise<DownloadJob> => {
|
||||
const waitForDownloadJob = async (
|
||||
jobId: string,
|
||||
onStatus?: DownloadJobStatusListener,
|
||||
): Promise<DownloadJob> => {
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < DOWNLOAD_JOB_MAX_WAIT_MS) {
|
||||
const job = (await getData(
|
||||
`v1/map/gisDownloadJobs/${encodeURIComponent(jobId)}`,
|
||||
)) as DownloadJob;
|
||||
onStatus?.(job);
|
||||
if (job?.status === "READY") return job;
|
||||
if (job?.status === "FAILED") {
|
||||
throw new Error(
|
||||
@@ -283,6 +290,12 @@ const postRefreshMessageToOpener = (response: GisUploadResponse) => {
|
||||
export default function GisManagementPopupPage() {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
// GIS 관리 코드값은 기존 분기에 사용하고 CMS 공통코드명은 화면 안내에만 사용한다.
|
||||
const {
|
||||
uploadStatusLabels,
|
||||
downloadStatusLabels,
|
||||
facilityTypeLabels,
|
||||
} = useGisManagementCommonCodes();
|
||||
// 파일 input은 브라우저 보안 정책상 value 직접 제어가 제한돼 ref로 선택/초기화한다.
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
@@ -344,6 +357,8 @@ export default function GisManagementPopupPage() {
|
||||
}, [mngInstOptions, selectedMngInstCd]);
|
||||
|
||||
const allowedExtensionText = facility.allowedExtensions.join(", ");
|
||||
const facilityTypeDisplayName =
|
||||
facilityTypeLabels[FACILITY_TYPE_CODE] || facility.facilityTypeName;
|
||||
const latestDownloadId = downloadIdsByScope[selectedScopeKey] ?? "";
|
||||
const isManagementSelected = Boolean(selectedMngInstCd);
|
||||
|
||||
@@ -543,8 +558,13 @@ export default function GisManagementPopupPage() {
|
||||
const job = await startDownloadJob(
|
||||
`v1/map/gisUploads/swagePpl/downloadJobs?${params.toString()}`,
|
||||
);
|
||||
setNotice(`${selectedScopeLabel} GeoPackage 파일을 생성하고 있습니다.`);
|
||||
const readyJob = await waitForDownloadJob(job.jobId);
|
||||
setNotice(
|
||||
`${selectedScopeLabel} ${downloadStatusLabels[job.status] || "GeoPackage 파일을 생성하고 있습니다."}`,
|
||||
);
|
||||
const readyJob = await waitForDownloadJob(job.jobId, (currentJob) => {
|
||||
const statusLabel = downloadStatusLabels[currentJob.status];
|
||||
if (statusLabel) setNotice(`${selectedScopeLabel} ${statusLabel}`);
|
||||
});
|
||||
const response = await downloadGeoPackage(
|
||||
`v1/map/gisDownloadJobs/${encodeURIComponent(job.jobId)}/file`,
|
||||
);
|
||||
@@ -614,7 +634,11 @@ export default function GisManagementPopupPage() {
|
||||
: [],
|
||||
);
|
||||
setViewMode("validationErrors");
|
||||
setNotice(response.message ?? "GeoPackage 검증 오류가 있습니다.");
|
||||
setNotice(
|
||||
response.message ??
|
||||
uploadStatusLabels[response.status] ??
|
||||
"GeoPackage 검증 오류가 있습니다.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -623,7 +647,11 @@ export default function GisManagementPopupPage() {
|
||||
resetSelectedFile();
|
||||
setValidationErrors([]);
|
||||
setViewMode("history");
|
||||
setNotice(response.message ?? "GIS 업로드가 완료되었습니다.");
|
||||
setNotice(
|
||||
response.message ??
|
||||
uploadStatusLabels[response.status] ??
|
||||
"GIS 업로드가 완료되었습니다.",
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("GIS 업로드 실패:", error);
|
||||
alert(
|
||||
@@ -647,8 +675,14 @@ export default function GisManagementPopupPage() {
|
||||
const job = await startDownloadJob(
|
||||
`v1/map/gisUploads/${encodeURIComponent(selectedHistory.uploadId)}/downloadJobs`,
|
||||
);
|
||||
setNotice("선택한 현황의 GeoPackage 파일을 생성하고 있습니다.");
|
||||
const readyJob = await waitForDownloadJob(job.jobId);
|
||||
setNotice(
|
||||
downloadStatusLabels[job.status] ||
|
||||
"선택한 현황의 GeoPackage 파일을 생성하고 있습니다.",
|
||||
);
|
||||
const readyJob = await waitForDownloadJob(job.jobId, (currentJob) => {
|
||||
const statusLabel = downloadStatusLabels[currentJob.status];
|
||||
if (statusLabel) setNotice(statusLabel);
|
||||
});
|
||||
const response = await downloadGeoPackage(
|
||||
`v1/map/gisDownloadJobs/${encodeURIComponent(job.jobId)}/file`,
|
||||
);
|
||||
@@ -760,7 +794,7 @@ export default function GisManagementPopupPage() {
|
||||
|
||||
<label className="flex min-w-[30rem] flex-1 items-center gap-2">
|
||||
<div className="shrink-0 whitespace-nowrap text-xl font-medium text-label">
|
||||
업로드 파일 ({facility.facilityTypeName}, {allowedExtensionText})
|
||||
업로드 파일 ({facilityTypeDisplayName}, {allowedExtensionText})
|
||||
</div>
|
||||
<Input
|
||||
value={selectedFile?.name ?? ""}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { getCommonCodes } from "@/components/lib/common/apiHelpers";
|
||||
|
||||
type CodeLabelMap = Record<string, string>;
|
||||
|
||||
const COMMON_CODE_GROUPS = {
|
||||
uploadStatus: "GISULDSTTS",
|
||||
downloadStatus: "GISDWNLDSTTS",
|
||||
facilityType: "SNFCLTSE",
|
||||
} as const;
|
||||
|
||||
const DEFAULT_UPLOAD_STATUS_LABELS: CodeLabelMap = {
|
||||
COMPLETED: "업로드완료",
|
||||
VALIDATION_FAILED: "검증실패",
|
||||
FAILED: "업로드실패",
|
||||
};
|
||||
|
||||
const DEFAULT_DOWNLOAD_STATUS_LABELS: CodeLabelMap = {
|
||||
QUEUED: "작업대기",
|
||||
RUNNING: "파일생성중",
|
||||
READY: "다운로드준비완료",
|
||||
FAILED: "파일생성실패",
|
||||
};
|
||||
|
||||
const DEFAULT_FACILITY_TYPE_LABELS: CodeLabelMap = {
|
||||
PPL: "하수관로",
|
||||
MNHL: "맨홀",
|
||||
};
|
||||
|
||||
const cachedLabelMaps = new Map<string, CodeLabelMap>();
|
||||
const pendingLabelMaps = new Map<string, Promise<CodeLabelMap>>();
|
||||
|
||||
/** 같은 브라우저 세션에서 각 GIS 관리 코드그룹을 한 번만 조회한다. */
|
||||
const loadCodeLabelMap = async (
|
||||
upCd: string,
|
||||
fallback: CodeLabelMap,
|
||||
): Promise<CodeLabelMap> => {
|
||||
const cached = cachedLabelMaps.get(upCd);
|
||||
if (cached) return cached;
|
||||
|
||||
const pending = pendingLabelMaps.get(upCd);
|
||||
if (pending) return pending;
|
||||
|
||||
const request = getCommonCodes(upCd)
|
||||
.then((codes) => {
|
||||
const labels = { ...fallback };
|
||||
codes.forEach((code) => {
|
||||
if (code.lwpoCd && code.lwpoCdNm) {
|
||||
labels[code.lwpoCd] = code.lwpoCdNm;
|
||||
}
|
||||
});
|
||||
cachedLabelMaps.set(upCd, labels);
|
||||
return labels;
|
||||
})
|
||||
.finally(() => {
|
||||
pendingLabelMaps.delete(upCd);
|
||||
});
|
||||
|
||||
pendingLabelMaps.set(upCd, request);
|
||||
return request;
|
||||
};
|
||||
|
||||
/** GIS 관리 화면의 코드값은 유지하고 상태·시설구분 표시명만 CMS 기준으로 제공한다. */
|
||||
export const useGisManagementCommonCodes = () => {
|
||||
const [uploadStatusLabels, setUploadStatusLabels] =
|
||||
useState<CodeLabelMap>(
|
||||
cachedLabelMaps.get(COMMON_CODE_GROUPS.uploadStatus) ??
|
||||
DEFAULT_UPLOAD_STATUS_LABELS,
|
||||
);
|
||||
const [downloadStatusLabels, setDownloadStatusLabels] =
|
||||
useState<CodeLabelMap>(
|
||||
cachedLabelMaps.get(COMMON_CODE_GROUPS.downloadStatus) ??
|
||||
DEFAULT_DOWNLOAD_STATUS_LABELS,
|
||||
);
|
||||
const [facilityTypeLabels, setFacilityTypeLabels] =
|
||||
useState<CodeLabelMap>(
|
||||
cachedLabelMaps.get(COMMON_CODE_GROUPS.facilityType) ??
|
||||
DEFAULT_FACILITY_TYPE_LABELS,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const applyLabels = (
|
||||
upCd: string,
|
||||
fallback: CodeLabelMap,
|
||||
setter: (labels: CodeLabelMap) => void,
|
||||
) => {
|
||||
loadCodeLabelMap(upCd, fallback)
|
||||
.then((labels) => {
|
||||
if (!cancelled) setter(labels);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`${upCd} 공통코드 조회에 실패했습니다.`, error);
|
||||
});
|
||||
};
|
||||
|
||||
applyLabels(
|
||||
COMMON_CODE_GROUPS.uploadStatus,
|
||||
DEFAULT_UPLOAD_STATUS_LABELS,
|
||||
setUploadStatusLabels,
|
||||
);
|
||||
applyLabels(
|
||||
COMMON_CODE_GROUPS.downloadStatus,
|
||||
DEFAULT_DOWNLOAD_STATUS_LABELS,
|
||||
setDownloadStatusLabels,
|
||||
);
|
||||
applyLabels(
|
||||
COMMON_CODE_GROUPS.facilityType,
|
||||
DEFAULT_FACILITY_TYPE_LABELS,
|
||||
setFacilityTypeLabels,
|
||||
);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
uploadStatusLabels,
|
||||
downloadStatusLabels,
|
||||
facilityTypeLabels,
|
||||
};
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { Button } from "@common/components";
|
||||
import { VerticalTable } from "@/components/ui/verticalTable";
|
||||
import { getCmsData } from "@/components/lib/api";
|
||||
import type { SinkholeDetail, SinkholeNearbyPipe } from "./sinkholeTypes";
|
||||
import { useSinkholeCommonCodes } from "./useSinkholeCommonCodes";
|
||||
|
||||
type Props = {
|
||||
detail: SinkholeDetail;
|
||||
@@ -74,6 +75,8 @@ export default function SinkholeDetailPanel({
|
||||
onClose,
|
||||
onSelectPipe,
|
||||
}: Props) {
|
||||
// 상세의 원천 복구상태 값은 유지하고 CMS에 대응하는 코드명만 표시한다.
|
||||
const { restoreStatusLabels } = useSinkholeCommonCodes();
|
||||
const [pipeUseCodeMap, setPipeUseCodeMap] = useState<Map<string, string>>(
|
||||
new Map(),
|
||||
);
|
||||
@@ -121,20 +124,25 @@ export default function SinkholeDetailPanel({
|
||||
}, [nearbyPipes]);
|
||||
|
||||
const summaryTableData = useMemo(
|
||||
() => [
|
||||
{
|
||||
acdntNo: display(detail.acdntNo),
|
||||
ocrnYmd: display(detail.ocrnYmd),
|
||||
fullAddress: display(detail.fullAddress || detail.daddr),
|
||||
sizeWidth: display(detail.sizeWidth),
|
||||
sizeLength: display(detail.sizeLength),
|
||||
sizeDepth: display(detail.sizeDepth),
|
||||
detailCause: display(detail.detailCause),
|
||||
restoreStatus: display(detail.restoreStatus),
|
||||
restoreCompletedYmd: display(detail.restoreCompletedYmd),
|
||||
},
|
||||
],
|
||||
[detail],
|
||||
() => {
|
||||
const restoreStatus = String(detail.restoreStatus ?? "").trim();
|
||||
return [
|
||||
{
|
||||
acdntNo: display(detail.acdntNo),
|
||||
ocrnYmd: display(detail.ocrnYmd),
|
||||
fullAddress: display(detail.fullAddress || detail.daddr),
|
||||
sizeWidth: display(detail.sizeWidth),
|
||||
sizeLength: display(detail.sizeLength),
|
||||
sizeDepth: display(detail.sizeDepth),
|
||||
detailCause: display(detail.detailCause),
|
||||
restoreStatus: display(
|
||||
restoreStatusLabels[restoreStatus] || restoreStatus,
|
||||
),
|
||||
restoreCompletedYmd: display(detail.restoreCompletedYmd),
|
||||
},
|
||||
];
|
||||
},
|
||||
[detail, restoreStatusLabels],
|
||||
);
|
||||
|
||||
const summaryHeadList = [
|
||||
|
||||
@@ -25,15 +25,14 @@ import {
|
||||
formatDateInputToYmd,
|
||||
formatYmdToDateInput,
|
||||
normalizeSinkholeFilters,
|
||||
SINKHOLE_COORD_STATUS_OPTIONS,
|
||||
SINKHOLE_DOWNLOAD_EXCEL_HEADERS,
|
||||
SINKHOLE_FILTER_SYNC_MESSAGE,
|
||||
SINKHOLE_MIN_SEARCH_YMD,
|
||||
SINKHOLE_RESTORE_STATUS_OPTIONS,
|
||||
SINKHOLE_ROW_SELECT_MESSAGE,
|
||||
type SinkholeFilters,
|
||||
type SinkholeListItem,
|
||||
} from "./sinkholeTypes";
|
||||
import { useSinkholeCommonCodes } from "./useSinkholeCommonCodes";
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
const DOWNLOAD_BATCH_SIZE = 2000;
|
||||
@@ -44,6 +43,7 @@ type SinkholeTableRow = SinkholeListItem & {
|
||||
sizeLengthDisplay: string;
|
||||
sizeDepthDisplay: string;
|
||||
detailCauseDisplay: string;
|
||||
restoreStatusDisplay: string;
|
||||
restoreCompletedYmdDisplay: string;
|
||||
coordStatusDisplay: string;
|
||||
};
|
||||
@@ -57,7 +57,7 @@ const HEADLIST: UiHeadProps[] = [
|
||||
{ id: "sizeLengthDisplay", title: "발생규모연장", widthPercent: 9 },
|
||||
{ id: "sizeDepthDisplay", title: "발생규모깊이", widthPercent: 9 },
|
||||
{ id: "detailCauseDisplay", title: "상세발생원인", widthPercent: 11 },
|
||||
{ id: "restoreStatus", title: "복구상태", widthPercent: 8 },
|
||||
{ id: "restoreStatusDisplay", title: "복구상태", widthPercent: 8 },
|
||||
{ id: "coordStatusDisplay", title: "좌표보정상태", widthPercent: 9 },
|
||||
{ id: "restoreCompletedYmdDisplay", title: "복구완료일자", widthPercent: 11 },
|
||||
];
|
||||
@@ -67,10 +67,14 @@ const toDisplay = (value: unknown) => {
|
||||
return normalized ? normalized : "-";
|
||||
};
|
||||
|
||||
const toCoordStatusDisplay = (value: unknown) => {
|
||||
const toCoordStatusDisplay = (
|
||||
value: unknown,
|
||||
labels: Record<string, string> = {},
|
||||
) => {
|
||||
const normalized = String(value ?? "")
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
if (labels[normalized]) return labels[normalized];
|
||||
if (normalized === "SUCCESS") return "성공";
|
||||
if (normalized === "NO_ADDRESS") return "주소없음";
|
||||
if (normalized === "FAILED") return "실패";
|
||||
@@ -89,10 +93,29 @@ const toTableRows = (
|
||||
sizeLengthDisplay: toDisplay(item.sizeLength),
|
||||
sizeDepthDisplay: toDisplay(item.sizeDepth),
|
||||
detailCauseDisplay: toDisplay(item.detailCause),
|
||||
restoreStatusDisplay: toDisplay(item.restoreStatus),
|
||||
coordStatusDisplay: toCoordStatusDisplay(item.coordStatus),
|
||||
restoreCompletedYmdDisplay: toDisplay(item.restoreCompletedYmd),
|
||||
}));
|
||||
|
||||
const applyStatusDisplayNames = (
|
||||
rows: SinkholeTableRow[],
|
||||
restoreStatusLabels: Record<string, string>,
|
||||
coordStatusLabels: Record<string, string>,
|
||||
) =>
|
||||
rows.map((row) => {
|
||||
const restoreStatus = String(row.restoreStatus ?? "").trim();
|
||||
return {
|
||||
...row,
|
||||
restoreStatusDisplay:
|
||||
restoreStatusLabels[restoreStatus] || restoreStatus || "-",
|
||||
coordStatusDisplay: toCoordStatusDisplay(
|
||||
row.coordStatus,
|
||||
coordStatusLabels,
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const buildDownloadRows = (rows: SinkholeTableRow[]) =>
|
||||
rows.map((row) => ({
|
||||
rowNo: row.rowNo,
|
||||
@@ -102,7 +125,7 @@ const buildDownloadRows = (rows: SinkholeTableRow[]) =>
|
||||
sizeLength: row.sizeLengthDisplay,
|
||||
sizeDepth: row.sizeDepthDisplay,
|
||||
detailCause: row.detailCauseDisplay,
|
||||
restoreStatus: row.restoreStatus || "-",
|
||||
restoreStatus: row.restoreStatusDisplay,
|
||||
restoreCompletedYmd: row.restoreCompletedYmdDisplay,
|
||||
}));
|
||||
|
||||
@@ -111,6 +134,13 @@ const unwrapPayload = <T,>(response: any): T =>
|
||||
|
||||
export default function SinkholePopupPage() {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
// 검색 파라미터 계약은 유지하고 선택지·목록 표시명만 CMS 공통코드로 구성한다.
|
||||
const {
|
||||
restoreStatusOptions,
|
||||
coordStatusOptions,
|
||||
restoreStatusLabels,
|
||||
coordStatusLabels,
|
||||
} = useSinkholeCommonCodes();
|
||||
const searchParams = useSearchParams();
|
||||
const hasInitialFilterParams =
|
||||
searchParams.has("startYmd") ||
|
||||
@@ -160,6 +190,10 @@ export default function SinkholePopupPage() {
|
||||
const [total, setTotal] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const displayRows = useMemo(
|
||||
() => applyStatusDisplayNames(rows, restoreStatusLabels, coordStatusLabels),
|
||||
[coordStatusLabels, restoreStatusLabels, rows],
|
||||
);
|
||||
const sggOptions = useMemo(
|
||||
() =>
|
||||
filters.ctpvCd === "0"
|
||||
@@ -294,7 +328,11 @@ export default function SinkholePopupPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
const downloadRows = toTableRows(downloadItems, 0, 1);
|
||||
const downloadRows = applyStatusDisplayNames(
|
||||
toTableRows(downloadItems, 0, 1),
|
||||
restoreStatusLabels,
|
||||
coordStatusLabels,
|
||||
);
|
||||
await downloadExcelWithKoreanHeaders({
|
||||
data: buildDownloadRows(downloadRows) as unknown as JSON[],
|
||||
fileName: `지반침하정보_${new Date().toISOString().slice(0, 10)}.xlsx`,
|
||||
@@ -395,7 +433,7 @@ export default function SinkholePopupPage() {
|
||||
setFilters({ ...filters, restoreStatus: event.target.value })
|
||||
}
|
||||
>
|
||||
{SINKHOLE_RESTORE_STATUS_OPTIONS.map((option) => (
|
||||
{restoreStatusOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
@@ -411,7 +449,7 @@ export default function SinkholePopupPage() {
|
||||
setFilters({ ...filters, coordStatus: event.target.value })
|
||||
}
|
||||
>
|
||||
{SINKHOLE_COORD_STATUS_OPTIONS.map((option) => (
|
||||
{coordStatusOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
@@ -445,7 +483,7 @@ export default function SinkholePopupPage() {
|
||||
</PopupSearchPanel>
|
||||
|
||||
<MapPopupDataTable
|
||||
rows={rows as Array<SinkholeTableRow & Record<string, unknown>>}
|
||||
rows={displayRows as Array<SinkholeTableRow & Record<string, unknown>>}
|
||||
headlist={HEADLIST}
|
||||
title="지반침하 목록"
|
||||
total={total}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { getCommonCodes } from "@/components/lib/common/apiHelpers";
|
||||
|
||||
import {
|
||||
SINKHOLE_COORD_STATUS_OPTIONS,
|
||||
SINKHOLE_RESTORE_STATUS_OPTIONS,
|
||||
} from "./sinkholeTypes";
|
||||
|
||||
type CodeLabelMap = Record<string, string>;
|
||||
type StatusOption = { value: string; label: string };
|
||||
|
||||
const COMMON_CODE_GROUPS = {
|
||||
restoreStatus: "SNKHLRSTRSTTS",
|
||||
coordStatus: "SNKHLCRDSTTS",
|
||||
} as const;
|
||||
|
||||
const cachedCodeLists = new Map<
|
||||
string,
|
||||
Array<{ lwpoCd: string; lwpoCdNm: string }>
|
||||
>();
|
||||
const pendingCodeLists = new Map<
|
||||
string,
|
||||
Promise<Array<{ lwpoCd: string; lwpoCdNm: string }>>
|
||||
>();
|
||||
|
||||
/** 같은 브라우저 세션에서는 지반침하 상태 코드그룹별 CMS 요청을 한 번만 수행한다. */
|
||||
const loadCodeList = async (upCd: string) => {
|
||||
const cached = cachedCodeLists.get(upCd);
|
||||
if (cached) return cached;
|
||||
|
||||
const pending = pendingCodeLists.get(upCd);
|
||||
if (pending) return pending;
|
||||
|
||||
const request = getCommonCodes(upCd)
|
||||
.then((codes) => {
|
||||
const codeList = codes.map(({ lwpoCd, lwpoCdNm }) => ({
|
||||
lwpoCd,
|
||||
lwpoCdNm,
|
||||
}));
|
||||
cachedCodeLists.set(upCd, codeList);
|
||||
return codeList;
|
||||
})
|
||||
.finally(() => {
|
||||
pendingCodeLists.delete(upCd);
|
||||
});
|
||||
|
||||
pendingCodeLists.set(upCd, request);
|
||||
return request;
|
||||
};
|
||||
|
||||
/** 지반침하 상태의 검색값 계약은 유지하고 선택지와 표시명만 CMS 기준으로 제공한다. */
|
||||
export const useSinkholeCommonCodes = () => {
|
||||
const [restoreCodes, setRestoreCodes] = useState(
|
||||
cachedCodeLists.get(COMMON_CODE_GROUPS.restoreStatus) ?? [],
|
||||
);
|
||||
const [coordCodes, setCoordCodes] = useState(
|
||||
cachedCodeLists.get(COMMON_CODE_GROUPS.coordStatus) ?? [],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
loadCodeList(COMMON_CODE_GROUPS.restoreStatus)
|
||||
.then((codes) => {
|
||||
if (!cancelled) setRestoreCodes(codes);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("SNKHLRSTRSTTS 공통코드 조회에 실패했습니다.", error);
|
||||
});
|
||||
loadCodeList(COMMON_CODE_GROUPS.coordStatus)
|
||||
.then((codes) => {
|
||||
if (!cancelled) setCoordCodes(codes);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("SNKHLCRDSTTS 공통코드 조회에 실패했습니다.", error);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const restoreStatusOptions = useMemo<StatusOption[]>(
|
||||
() =>
|
||||
restoreCodes.length > 0
|
||||
? [
|
||||
{ value: "0", label: "전체" },
|
||||
...restoreCodes.map((code) => ({
|
||||
// 백엔드는 복구상태 한글 설명값을 검색조건으로 사용하므로 코드명을 전송한다.
|
||||
value: code.lwpoCdNm,
|
||||
label: code.lwpoCdNm,
|
||||
})),
|
||||
]
|
||||
: [...SINKHOLE_RESTORE_STATUS_OPTIONS],
|
||||
[restoreCodes],
|
||||
);
|
||||
|
||||
const coordStatusOptions = useMemo<StatusOption[]>(
|
||||
() =>
|
||||
coordCodes.length > 0
|
||||
? [
|
||||
{ value: "0", label: "전체" },
|
||||
...coordCodes.map((code) => ({
|
||||
value: code.lwpoCd,
|
||||
label: code.lwpoCdNm,
|
||||
})),
|
||||
]
|
||||
: [...SINKHOLE_COORD_STATUS_OPTIONS],
|
||||
[coordCodes],
|
||||
);
|
||||
|
||||
const restoreStatusLabels = useMemo<CodeLabelMap>(() => {
|
||||
const labels: CodeLabelMap = {};
|
||||
restoreCodes.forEach((code) => {
|
||||
labels[code.lwpoCd] = code.lwpoCdNm;
|
||||
labels[code.lwpoCdNm] = code.lwpoCdNm;
|
||||
});
|
||||
return labels;
|
||||
}, [restoreCodes]);
|
||||
|
||||
const coordStatusLabels = useMemo<CodeLabelMap>(() => {
|
||||
const labels: CodeLabelMap = {};
|
||||
coordCodes.forEach((code) => {
|
||||
labels[code.lwpoCd] = code.lwpoCdNm;
|
||||
});
|
||||
return labels;
|
||||
}, [coordCodes]);
|
||||
|
||||
return {
|
||||
restoreStatusOptions,
|
||||
coordStatusOptions,
|
||||
restoreStatusLabels,
|
||||
coordStatusLabels,
|
||||
};
|
||||
};
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
} from "../lib/client";
|
||||
import { formatDredgingCapturedDate } from "../lib/date";
|
||||
import { readPhotoMetadata } from "../lib/exif";
|
||||
import { useDredgingCommonCodes } from "../lib/useDredgingCommonCodes";
|
||||
import { createDredgingClientUuid } from "../lib/uuid";
|
||||
import {
|
||||
EMPTY_PHOTO_METADATA,
|
||||
@@ -69,11 +70,6 @@ const resolvePipeMaterialName = (value: string) => {
|
||||
return pipeMaterialNameByCode.get(normalizedCode) ?? "확인불가";
|
||||
};
|
||||
|
||||
const locationSourceLabel = {
|
||||
EXIF: "사진 GPS",
|
||||
MANUAL: "지도 지정",
|
||||
} as const;
|
||||
|
||||
const formatLocalDate = (date: Date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
@@ -91,6 +87,9 @@ const DredgingDetailPopupPage = ({
|
||||
}: Props) => {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const { confirm } = useConfirm();
|
||||
// 수정 화면도 등록 화면과 같은 CMS 공통코드 표시명을 공유한다.
|
||||
const { registrationModeLabels, locationSourceLabels } =
|
||||
useDredgingCommonCodes();
|
||||
const editable = mode === "edit";
|
||||
const [photos, setPhotos] = useState<DredgingPhotoPreview[]>([]);
|
||||
const [newPhotos, setNewPhotos] = useState<NewPhotoPreview[]>([]);
|
||||
@@ -237,7 +236,7 @@ const DredgingDetailPopupPage = ({
|
||||
? `${matchedPipe.installationYear}년`
|
||||
: "확인불가",
|
||||
],
|
||||
["위치등록", locationSourceLabel[locationSource]],
|
||||
["위치등록", locationSourceLabels[locationSource] || locationSource],
|
||||
];
|
||||
const pipeInfoRows = Array.from(
|
||||
{ length: Math.ceil(pipeInfo.length / 2) },
|
||||
@@ -576,7 +575,7 @@ const DredgingDetailPopupPage = ({
|
||||
onClick={() => void changeRegistrationMode("PHOTO")}
|
||||
data-testid="dredging-edit-photo-mode"
|
||||
>
|
||||
사진 기반 등록
|
||||
{registrationModeLabels.PHOTO}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -588,7 +587,7 @@ const DredgingDetailPopupPage = ({
|
||||
onClick={() => void changeRegistrationMode("MANUAL")}
|
||||
data-testid="dredging-edit-manual-mode"
|
||||
>
|
||||
사진 없이 직접 등록
|
||||
{registrationModeLabels.MANUAL}
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
} from "../lib/client";
|
||||
import { formatDredgingCapturedDate } from "../lib/date";
|
||||
import { readPhotoMetadata } from "../lib/exif";
|
||||
import { useDredgingCommonCodes } from "../lib/useDredgingCommonCodes";
|
||||
import { createDredgingClientUuid } from "../lib/uuid";
|
||||
import {
|
||||
EMPTY_PHOTO_METADATA,
|
||||
@@ -74,14 +75,6 @@ const getResolvedLocation = (group: DredgingDraftGroup): GeoPoint | null => {
|
||||
);
|
||||
};
|
||||
|
||||
const statusLabel = (status: DredgingPhotoDraft["metadata"]["status"]) => {
|
||||
if (status === "COMPLETE") return "정상";
|
||||
if (status === "MISSING_DATE") return "촬영일 없음";
|
||||
if (status === "MISSING_GPS") return "GPS 없음";
|
||||
if (status === "MISSING_BOTH") return "촬영일·GPS 없음";
|
||||
return "수동 입력 필요";
|
||||
};
|
||||
|
||||
const formatLocalDate = (date: Date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
@@ -99,6 +92,12 @@ const DredgingRegisterModal = ({
|
||||
}: DredgingRegisterModalProps) => {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const { confirm } = useConfirm();
|
||||
// 등록방식·위치출처·사진상태 코드값은 유지하고 CMS 코드명을 화면에 표시한다.
|
||||
const {
|
||||
registrationModeLabels,
|
||||
locationSourceLabels,
|
||||
photoStatusLabels,
|
||||
} = useDredgingCommonCodes();
|
||||
const [groups, setGroups] = useState<DredgingDraftGroup[]>([]);
|
||||
const groupsRef = useRef<DredgingDraftGroup[]>([]);
|
||||
const [activeGroupId, setActiveGroupId] = useState<string | null>(null);
|
||||
@@ -706,7 +705,7 @@ const DredgingRegisterModal = ({
|
||||
void changeRegistrationMode(activeGroup.id, "PHOTO")
|
||||
}
|
||||
>
|
||||
사진 기반 등록
|
||||
{registrationModeLabels.PHOTO}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -721,7 +720,7 @@ const DredgingRegisterModal = ({
|
||||
void changeRegistrationMode(activeGroup.id, "MANUAL")
|
||||
}
|
||||
>
|
||||
사진 없이 직접 등록
|
||||
{registrationModeLabels.MANUAL}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -787,7 +786,9 @@ const DredgingRegisterModal = ({
|
||||
: "bg-amber-100 text-amber-800"
|
||||
}`}
|
||||
>
|
||||
{statusLabel(photo.metadata.status)}
|
||||
{photoStatusLabels[
|
||||
photo.metadata.status
|
||||
] || "수동 입력 필요"}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="px-2 text-center">
|
||||
@@ -890,9 +891,9 @@ const DredgingRegisterModal = ({
|
||||
{hasUnappliedLocationDraft
|
||||
? "새 위치 선택됨"
|
||||
: activeGroup.manualLocation
|
||||
? "지도 지정 위치"
|
||||
? `${locationSourceLabels.MANUAL} 위치`
|
||||
: activeResolvedLocation
|
||||
? "사진 GPS 위치"
|
||||
? `${locationSourceLabels.EXIF} 위치`
|
||||
: "위치 지정 필요"}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { getCommonCodes } from "@/components/lib/common/apiHelpers";
|
||||
|
||||
import type {
|
||||
DredgingLocationSource,
|
||||
DredgingRegistrationMode,
|
||||
ExifStatus,
|
||||
} from "./types";
|
||||
|
||||
type CodeLabelMap = Record<string, string>;
|
||||
|
||||
const COMMON_CODE_GROUPS = {
|
||||
registrationMode: "DRDGNREGMODE",
|
||||
locationSource: "DRDGNLOCSRC",
|
||||
photoStatus: "DRDGNPHOTOSTTS",
|
||||
} as const;
|
||||
|
||||
const DEFAULT_REGISTRATION_MODE_LABELS: Record<
|
||||
DredgingRegistrationMode,
|
||||
string
|
||||
> = {
|
||||
PHOTO: "사진 기반 등록",
|
||||
MANUAL: "사진 없이 직접 등록",
|
||||
};
|
||||
|
||||
const DEFAULT_LOCATION_SOURCE_LABELS: Record<DredgingLocationSource, string> = {
|
||||
EXIF: "사진 GPS",
|
||||
MANUAL: "지도 지정",
|
||||
};
|
||||
|
||||
const DEFAULT_PHOTO_STATUS_LABELS: Record<ExifStatus, string> = {
|
||||
COMPLETE: "정상",
|
||||
MISSING_DATE: "촬영일 없음",
|
||||
MISSING_GPS: "GPS 없음",
|
||||
MISSING_BOTH: "촬영일·GPS 없음",
|
||||
UNSUPPORTED: "수동 입력 필요",
|
||||
};
|
||||
|
||||
const cachedLabelMaps = new Map<string, CodeLabelMap>();
|
||||
const pendingLabelMaps = new Map<string, Promise<CodeLabelMap>>();
|
||||
|
||||
/** 같은 브라우저 세션에서는 준설 코드그룹별 CMS 요청을 한 번만 수행한다. */
|
||||
const loadCodeLabelMap = async (
|
||||
upCd: string,
|
||||
fallback: CodeLabelMap,
|
||||
): Promise<CodeLabelMap> => {
|
||||
const cached = cachedLabelMaps.get(upCd);
|
||||
if (cached) return cached;
|
||||
|
||||
const pending = pendingLabelMaps.get(upCd);
|
||||
if (pending) return pending;
|
||||
|
||||
const request = getCommonCodes(upCd)
|
||||
.then((codes) => {
|
||||
const labels = { ...fallback };
|
||||
codes.forEach((code) => {
|
||||
if (code.lwpoCd && code.lwpoCdNm) {
|
||||
labels[code.lwpoCd] = code.lwpoCdNm;
|
||||
}
|
||||
});
|
||||
cachedLabelMaps.set(upCd, labels);
|
||||
return labels;
|
||||
})
|
||||
.finally(() => {
|
||||
pendingLabelMaps.delete(upCd);
|
||||
});
|
||||
|
||||
pendingLabelMaps.set(upCd, request);
|
||||
return request;
|
||||
};
|
||||
|
||||
/** 준설 저장 코드값은 유지하고 버튼·상세·사진상태 표시명만 CMS 기준으로 제공한다. */
|
||||
export const useDredgingCommonCodes = () => {
|
||||
const [registrationModeLabels, setRegistrationModeLabels] =
|
||||
useState<CodeLabelMap>(
|
||||
cachedLabelMaps.get(COMMON_CODE_GROUPS.registrationMode) ??
|
||||
DEFAULT_REGISTRATION_MODE_LABELS,
|
||||
);
|
||||
const [locationSourceLabels, setLocationSourceLabels] =
|
||||
useState<CodeLabelMap>(
|
||||
cachedLabelMaps.get(COMMON_CODE_GROUPS.locationSource) ??
|
||||
DEFAULT_LOCATION_SOURCE_LABELS,
|
||||
);
|
||||
const [photoStatusLabels, setPhotoStatusLabels] = useState<CodeLabelMap>(
|
||||
cachedLabelMaps.get(COMMON_CODE_GROUPS.photoStatus) ??
|
||||
DEFAULT_PHOTO_STATUS_LABELS,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const applyLabels = (
|
||||
upCd: string,
|
||||
fallback: CodeLabelMap,
|
||||
setter: (labels: CodeLabelMap) => void,
|
||||
) => {
|
||||
loadCodeLabelMap(upCd, fallback)
|
||||
.then((labels) => {
|
||||
if (!cancelled) setter(labels);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`${upCd} 공통코드 조회에 실패했습니다.`, error);
|
||||
});
|
||||
};
|
||||
|
||||
applyLabels(
|
||||
COMMON_CODE_GROUPS.registrationMode,
|
||||
DEFAULT_REGISTRATION_MODE_LABELS,
|
||||
setRegistrationModeLabels,
|
||||
);
|
||||
applyLabels(
|
||||
COMMON_CODE_GROUPS.locationSource,
|
||||
DEFAULT_LOCATION_SOURCE_LABELS,
|
||||
setLocationSourceLabels,
|
||||
);
|
||||
applyLabels(
|
||||
COMMON_CODE_GROUPS.photoStatus,
|
||||
DEFAULT_PHOTO_STATUS_LABELS,
|
||||
setPhotoStatusLabels,
|
||||
);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
registrationModeLabels,
|
||||
locationSourceLabels,
|
||||
photoStatusLabels,
|
||||
};
|
||||
};
|
||||
@@ -12,7 +12,47 @@
|
||||
* 🔗 사용 API
|
||||
* - 하위코드페이지 캐시 목록 조회: GET /api/v1/code/cache/{upCd} (cms-service)
|
||||
*/
|
||||
import { getData } from "../api";
|
||||
import { getCmsData, getData } from "../api";
|
||||
|
||||
/** CMS 공통코드 캐시 API가 반환하는 하위코드의 공통 계약이다. */
|
||||
export type CommonCodeItem = {
|
||||
lwpoCd: string;
|
||||
upCd: string;
|
||||
lwpoCdNm: string;
|
||||
unit: string;
|
||||
prntLwpoCd: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 기존 CMS 캐시 조회 URL을 유지하면서 상위코드에 속한 하위코드를 조회한다.
|
||||
* 화면이 잘못된 응답을 정상 코드 목록으로 사용하지 않도록 배열 계약을 확인한다.
|
||||
*/
|
||||
export const getCommonCodes = async (
|
||||
upCd: string,
|
||||
): Promise<CommonCodeItem[]> => {
|
||||
const normalizedUpCd = upCd.trim();
|
||||
if (!normalizedUpCd) return [];
|
||||
|
||||
const data = await getCmsData(
|
||||
`/api/v1/code/cache/${encodeURIComponent(normalizedUpCd)}`,
|
||||
);
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error(`공통코드 응답 형식이 올바르지 않습니다: ${normalizedUpCd}`);
|
||||
}
|
||||
|
||||
return data
|
||||
.map((item: any) => ({
|
||||
lwpoCd: String(item?.lwpoCd ?? "").trim(),
|
||||
upCd: String(item?.upCd ?? normalizedUpCd).trim(),
|
||||
lwpoCdNm: String(item?.lwpoCdNm ?? "").trim(),
|
||||
unit: String(item?.unit ?? "").trim(),
|
||||
prntLwpoCd:
|
||||
item?.prntLwpoCd === null || item?.prntLwpoCd === undefined
|
||||
? null
|
||||
: String(item.prntLwpoCd).trim(),
|
||||
}))
|
||||
.filter((item) => item.lwpoCd.length > 0);
|
||||
};
|
||||
|
||||
export const searchData = async<T extends Record<string, any>>(url: string, paramObj: T) => {
|
||||
const params = new URLSearchParams(
|
||||
@@ -29,10 +69,10 @@ export const searchData = async<T extends Record<string, any>>(url: string, para
|
||||
|
||||
// 상위코드로 하위코드 조회
|
||||
export const searchCmmnCd = async (upCd: string) => {
|
||||
const data = await getData(`/cms-service/api/v1/code/cache/${upCd}`);
|
||||
const data = await getCommonCodes(upCd);
|
||||
const filteredData = data.map((item: any) => ({
|
||||
lwpoCd: item.lwpoCd,
|
||||
lwpoCdNm: item.lwpoCdNm,
|
||||
}));
|
||||
return filteredData;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -629,3 +629,13 @@
|
||||
| 변경 파일 ESLint | 통과 | 오류·경고 0건 |
|
||||
| `sn` TypeScript 검사 | 통과 | `tsc --noEmit`, 기존 설정값은 명령줄 `ignoreDeprecations=5.0`으로 보정 |
|
||||
| 로그인 브라우저 | 대기 | 사업등록 저장 후 두 다운로드와 수정 팝업 버튼 3개 최종 확인 |
|
||||
|
||||
## 40. 2026-09-01 조사사업 공통코드 프론트 표시 빠른 검증
|
||||
|
||||
- 실제 CMS 캐시 API 확인
|
||||
- 결함등급 5건, 기록유형 2건, 결함분류 3건, 심각도 3건, 상태그룹 4건
|
||||
- 관로 조사항목 31건, 맨홀 조사항목 23건
|
||||
- 총 71개 코드가 계획한 그룹별 건수로 조회됐다.
|
||||
- `git diff --check`는 줄바꿈 변환 경고 외 whitespace 오류 없이 통과했다.
|
||||
- 사용자 요청에 따라 전체 TypeScript 검사와 Next 빌드는 수행하지 않았다.
|
||||
- 조사 종합집계 필터, 조사 상세표·Excel, 본단위등급표와 시설 상세의 코드명 수동 검증이 남아 있다.
|
||||
|
||||
@@ -333,3 +333,13 @@
|
||||
- TypeScript compiler API의 `transpileModule`로 `page.tsx`를 TSX 파싱한 결과 error diagnostics 0건이었다.
|
||||
- 프론트 변경 파일 대상 `git diff --check`는 통과했다.
|
||||
- 로그인 세션이 필요한 실제 탭 누적·교체·복귀 상호작용은 수동 검증이 남아 있다.
|
||||
|
||||
### 2.31 2026-09-01 지반침하 상태 공통코드 적용 빠른 검증
|
||||
|
||||
- 실제 CMS 캐시 API 확인
|
||||
- `SNKHLRSTRSTTS`: `RESTORED=복구완료`, `RESTORING=복구중`, `NOT_RESTORED=미복구`
|
||||
- `SNKHLCRDSTTS`: `SUCCESS=보정성공`, `FAILED=보정실패`
|
||||
- 백엔드와 기존 데이터가 복구상태를 `복구완료/복구중/미복구` 설명값으로 처리하는 계약을 코드·기존 검증 문서에서 확인했다.
|
||||
- `git diff --check`는 줄바꿈 변환 경고 외 whitespace 오류 없이 통과했다.
|
||||
- 사용자 요청에 따라 전체 TypeScript 검사와 Next 빌드는 수행하지 않았다.
|
||||
- 실제 지반침하 목록에서 검색조건·목록·Excel·상세 표시와 조회 결과 건수의 수동 검증이 남아 있다.
|
||||
|
||||
@@ -371,3 +371,13 @@ Chrome 확장의 파일 URL 접근 권한이 꺼져 있어 이번 회귀에서 E
|
||||
|
||||
- 검증 화면: `COMMON_DOCS_ROOT/작업내용/SFR-010-012-준설관리/검증/20260828/03-수정화면-촬영일자-날짜만.png`.
|
||||
- 사용자 실행 중인 3002 개발서버를 보호하기 위해 Next build는 수행하지 않았다.
|
||||
|
||||
## 16. 2026-09-01 준설 공통코드 적용 빠른 검증
|
||||
|
||||
- 실제 CMS 캐시 API 확인
|
||||
- `DRDGNREGMODE`: `PHOTO=사진등록`, `MANUAL=수기등록`
|
||||
- `DRDGNLOCSRC`: `EXIF=사진 EXIF`, `MANUAL=수기지정`
|
||||
- `DRDGNPHOTOSTTS`: `COMPLETE=메타정보정상`, `MISSING_BOTH=촬영일시·GPS누락`, `MISSING_DATE=촬영일시누락`, `MISSING_GPS=GPS누락`
|
||||
- `git diff --check`는 줄바꿈 변환 경고 외 whitespace 오류 없이 통과했다.
|
||||
- 사용자 요청에 따라 전체 TypeScript 검사와 Next 빌드는 수행하지 않았다.
|
||||
- 등록·수정 팝업에서 버튼명, 위치출처, 사진상태 표시와 기존 저장 흐름의 수동 검증이 남아 있다.
|
||||
|
||||
@@ -313,3 +313,13 @@
|
||||
- 오류 화면의 `목록` 클릭 시 `GIS 업로드 목록`으로 정상 복귀했다.
|
||||
- 전체 TypeScript, 대상 Prettier와 `git diff --check`를 통과했다.
|
||||
- API·DB 변경 없음. 판정: PASS.
|
||||
|
||||
## 11. 2026-09-01 GIS 관리 공통코드 적용 빠른 검증
|
||||
|
||||
- 실제 CMS 캐시 API 확인
|
||||
- `GISULDSTTS`: `COMPLETED=업로드완료`, `VALIDATION_FAILED=검증실패`, `FAILED=업로드실패`
|
||||
- `GISDWNLDSTTS`: `QUEUED=작업대기`, `RUNNING=파일생성중`, `READY=다운로드준비완료`, `FAILED=파일생성실패`
|
||||
- `SNFCLTSE`: `PPL=관로`, `MNHL=맨홀`
|
||||
- `git diff --check`는 줄바꿈 변환 경고 외 whitespace 오류 없이 통과했다.
|
||||
- 사용자 요청에 따라 전체 TypeScript 검사와 Next 빌드는 이번 단계에서 수행하지 않았다.
|
||||
- 실제 GIS 관리 팝업에서 시설구분명, 다운로드 상태 안내와 업로드 결과 fallback을 확인하는 수동 검증이 남아 있다.
|
||||
|
||||
@@ -759,3 +759,18 @@
|
||||
- 사용자가 실제 지도에서 GIS 편집 두 번째 클릭 시 하단 도구바와 인라인 세션이 함께 사라지는 것을 확인했다.
|
||||
- 다시 GIS 편집 진입 시 기존 변경분과 인라인 세션이 복원되는 것을 확인했다.
|
||||
- 결론: 추가 회귀 보정까지 사용자 수동 검증 완료.
|
||||
|
||||
### 2.32 2026-09-01 GIS 편집작업 공통코드 1차 적용 검증
|
||||
|
||||
- 실제 CMS 캐시 API 확인
|
||||
- `GISEDITOPRT`: `ADD=추가`, `MOVE=이동`, `VERTEX_EDIT=꼭지점편집`, `DELETE=삭제` 4건 확인
|
||||
- `GISEDITTYPE`: `C=등록`, `U=수정`, `D=삭제` 3건 확인
|
||||
- 전체 TypeScript 검사
|
||||
- 명령: `node apps/sn/node_modules/typescript/bin/tsc -p apps/sn/tsconfig.json --noEmit --pretty false --ignoreDeprecations 5.0`
|
||||
- 결과: 통과
|
||||
- SN 프로덕션 빌드
|
||||
- Next.js 최적화 컴파일과 정적 페이지 28개 생성까지 통과했다.
|
||||
- standalone traced file 복사 단계에서 Windows symlink `EPERM`으로 최종 종료코드 1이 발생했다.
|
||||
- TypeScript 또는 이번 변경 코드의 컴파일 오류는 아니다.
|
||||
- `git diff --check`는 줄바꿈 변환 경고 외 whitespace 오류 없이 통과했다.
|
||||
- 실제 지도 화면에서 툴바와 인라인·팝업 세션 라벨이 CMS 응답으로 표시되는지는 로컬 배포 또는 개발 서버 기동 후 수동 확인이 남아 있다.
|
||||
|
||||
@@ -24,3 +24,12 @@
|
||||
- 시트명과 B6 `순번`, D6 업무별 사업코드 헤더, B8 데이터 시작 위치를 자동 확인했다.
|
||||
- SN 개발 서버가 3002 포트에서 실행 중이어서 앱 규칙에 따라 `next build`는 실행하지 않았다.
|
||||
- 로그인·실 API 환경에서 조회조건 다운로드와 다운로드→수정→업로드 브라우저 검증은 남은 통합검증이다.
|
||||
|
||||
## 2026-09-01 운영사업 공통코드 화면 적용 빠른 검증
|
||||
|
||||
- 실제 CMS에서 사업구분 2건, 증빙상태 4건, 사업추진상태 7건을 확인했다.
|
||||
- 개발 DB에서 사업구분 `정밀조사 130건/정비사업 391건`, 추진현황의 `준공·공사중·설계중·발주중` 등 비표준 기존값, 증빙의 `O/X/공단/요청/준비중` 값을 확인했다.
|
||||
- 기존값 보존을 위해 추진·증빙 입력은 자유문자 datalist 방식으로 적용했다.
|
||||
- `git diff --check`는 whitespace 오류 없이 통과했다.
|
||||
- 사용자 요청에 따라 전체 TypeScript 검사와 Next 빌드는 수행하지 않았다.
|
||||
- 실제 목록·등록·수정에서 선택 후보와 기존값 저장 회귀 확인이 남아 있다.
|
||||
|
||||
@@ -9,3 +9,12 @@
|
||||
- `/oper/ctyFldngPrvtnBiz/register/1`: HTTP 200
|
||||
- 0811 원본 구조 확인: `사업 추진 현황` 시트, 상세 시작 `AJ/BK/CS/DX`, 결산 2013~2026년, 데이터 257행을 확인했다.
|
||||
- 실제 API·DB 저장과 브라우저 조작, 원본 파일 업로드 후 DB 대조는 테이블 및 인증 환경에서 추가 확인이 필요하다.
|
||||
|
||||
## 2026-09-01 운영사업 공통코드 화면 적용 빠른 검증
|
||||
|
||||
- 실제 CMS에서 도시침수 진행단계 4건, 지정유형 2건, 사업추진상태 7건을 확인했다.
|
||||
- 개발 DB에서 상세단계 `EM/MP/DD/CW` 각 257건, 지정유형 `우려 4건/피해 253건`을 확인했다.
|
||||
- 추진현황에는 표준 6개 값 외 `완료(연월)` 변형이 다수 있어 자유문자 datalist로 적용했다.
|
||||
- `git diff --check`는 whitespace 오류 없이 통과했다.
|
||||
- 사용자 요청에 따라 전체 TypeScript 검사와 Next 빌드는 수행하지 않았다.
|
||||
- 실제 목록·등록·수정에서 구역 제목, 입력 후보와 기존 변형값 유지 확인이 남아 있다.
|
||||
|
||||
@@ -951,3 +951,12 @@
|
||||
- 수정 진입에서는 `조사보고서 양식다운로드`를 숨기고 `양식다운로드`, `저장`, `취소`만 표시한다.
|
||||
- 처리분구를 수정한 경우 기존 계약대로 먼저 저장한 뒤 최신 처리분구 기준 입력양식을 다시 생성한다.
|
||||
- 공통 조사보고서 XLSX와 등록된 사업보고서 PDF의 다운로드 경로는 변경하지 않았다.
|
||||
|
||||
## 47. 2026-09-01 조사사업 공통코드 프론트 표시 1차 적용
|
||||
|
||||
- `EXMNDFCGRD`, `EXMNRECDTYPE`, `EXMNDFCTYPE`, `EXMNDFCSEV`, `EXMNSTTSGRP`, `PPLEXMNITEM`, `MNHLEXMNITEM`을 기존 CMS 캐시 API로 조회하고 브라우저 세션에서 그룹별 응답과 진행 중 요청을 재사용한다.
|
||||
- PPL 시설 필터의 구조·운영등급 선택지와 결함분류 표시, 조사 상세표·Excel의 결함분류·심각도·관로·맨홀 항목명에 CMS 코드명을 적용했다.
|
||||
- 본단위등급표와 PPL 시설 상세의 기록유형·결함분류·심각도·상태그룹 표시에도 같은 코드명을 적용했다.
|
||||
- 조사 코드값, 상세표 열 순서, API 파라미터와 백엔드 Excel 파서·분류 목록은 변경하지 않았다.
|
||||
- CMS 조회 실패 또는 미등록 코드에는 기존 화면 라벨과 서버 제공값을 fallback으로 유지한다.
|
||||
- 백엔드 API와 DB는 변경하지 않았다.
|
||||
|
||||
@@ -473,6 +473,7 @@
|
||||
- `SinkholePopupPage.tsx` 그리드 첫 데이터 열에 `사고번호(acdntNo)`를 배치하고 화면 `NO` 열은 제거했다. 내부 `rowNo`와 다운로드 순번은 유지했다.
|
||||
- 반경 내 관로 GeoServer 조회의 기존 storage nullable 타입 경고는 저장소 이름 확인 후 요청하도록 방어 처리했다.
|
||||
- BE API·DTO·Repository·DB 변경은 없다.
|
||||
|
||||
- 브라우저·API·빌드 검증은 사용자 요청에 따라 수행하지 않았으며 사용자 검증 대기 상태다.
|
||||
|
||||
### 4.55 2026-07-14 목록 NO 열 제거 후속 반영
|
||||
@@ -499,3 +500,13 @@
|
||||
- 부모가 있는 지반침하 연계 관로는 비활성 상태에서도 `←`를 항상 표시해 일반 관로와 구분되도록 후속 보완했다.
|
||||
- 탭 최대 5개 초과 시 가장 먼저 열린 탭을 제거하는 FIFO를 유지하고, 제거 대상이 부모 지반침하이면 그 부모를 참조하는 연계 관로 탭도 함께 제거하도록 공통 그룹 제거 함수를 적용했다. 연계 관로 단독 제거는 부모에 영향을 주지 않는다.
|
||||
- BE API·DTO·Repository·DB 변경은 없다.
|
||||
|
||||
### 4.58 2026-09-01 지반침하 상태 공통코드 적용
|
||||
|
||||
- `SNKHLRSTRSTTS`, `SNKHLCRDSTTS`를 기존 CMS 캐시 API로 조회하고 브라우저 세션에서 그룹별 응답과 진행 중 요청을 재사용한다.
|
||||
- 좌표보정상태는 기존 `SUCCESS/FAILED` 검색값을 유지하고 선택지·목록 표시명을 공통코드의 `보정성공/보정실패`로 표시한다.
|
||||
- 복구상태는 백엔드가 외부 원천의 한글 설명값을 검색·응답하므로 `RESTORED/RESTORING/NOT_RESTORED`를 API에 전송하지 않는다. CMS의 코드명을 기존 한글 검색값으로 사용해 계약을 유지한다.
|
||||
- 목록은 원본 상태 필드를 유지한 별도 표시 필드를 사용하고, Excel과 상세 패널에도 같은 CMS 코드명을 적용한다.
|
||||
- CMS에 없는 기존 좌표상태 `NO_ADDRESS`는 `주소없음` fallback을 유지한다.
|
||||
- CMS 조회 실패 시 기존 선택지와 상태 문구를 유지한다.
|
||||
- 백엔드 API, 외부 원천 데이터와 DB는 변경하지 않았다.
|
||||
|
||||
@@ -353,3 +353,12 @@
|
||||
- 존재하지 않는 날짜나 빈 값은 기존 화면별 fallback(`-`, `촬영일시 없음`)을 유지한다.
|
||||
- EXIF 원본 `capturedAt`, 첨부정보와 백엔드 저장 payload는 변경하지 않고 화면 렌더링만 변경했다.
|
||||
- 추적 근거는 요구사항 `SFR-010-012`, 화면설계서 슬라이드 53의 `SN_MN040201`이며 세부 시간 숨김은 사용자 지시에 따른 표출 보완이다.
|
||||
|
||||
## 22. 2026-09-01 준설 공통코드 표시명 적용
|
||||
|
||||
- `DRDGNREGMODE`, `DRDGNLOCSRC`, `DRDGNPHOTOSTTS`를 기존 CMS 캐시 API로 조회하고 브라우저 세션에서 그룹별 응답과 진행 중 요청을 재사용한다.
|
||||
- 등록·수정 화면의 `PHOTO/MANUAL` 버튼명, 위치출처 표시와 등록 화면 사진 메타상태명을 CMS의 `lwpoCdNm`으로 표시한다.
|
||||
- 등록방식, 위치출처, 사진상태 코드값과 저장·검증 분기는 변경하지 않았다.
|
||||
- CMS에 없는 기존 `UNSUPPORTED` 상태는 `수동 입력 필요` fallback을 유지한다.
|
||||
- CMS 조회 실패 시 기존 화면 문구를 fallback으로 사용한다.
|
||||
- 백엔드 API, 준설 저장 payload와 DB 구조는 변경하지 않았다.
|
||||
|
||||
@@ -170,3 +170,13 @@
|
||||
- 업로드 이력을 다시 조회하고 목록 화면으로 전환하는 버튼명을 `조회`에서 `목록`으로 변경했다.
|
||||
- 기본 업로드 이력 화면과 오류·미리보기 화면의 동일 액션 문구를 함께 통일했다.
|
||||
- 버튼 이벤트와 API 요청은 변경하지 않았다.
|
||||
|
||||
### 8.5 2026-09-01 GIS 관리 공통코드 1차 적용
|
||||
|
||||
- `GISULDSTTS`, `GISDWNLDSTTS`, `SNFCLTSE`를 기존 CMS 캐시 URL로 기능 진입 시 병렬 조회한다.
|
||||
- 같은 브라우저 세션에서는 그룹별 조회 결과와 진행 중 요청을 재사용해 팝업 재진입 시 중복 호출을 줄였다.
|
||||
- 업로드의 `COMPLETED/VALIDATION_FAILED/FAILED`와 다운로드의 `QUEUED/RUNNING/READY/FAILED` 분기는 그대로 유지하고 안내문 fallback에 공통코드명을 사용한다.
|
||||
- 다운로드 대기 중에는 현재 작업상태를 공통코드명으로 갱신한다.
|
||||
- 시설구분은 백엔드 지원범위 `PPL` 단일을 유지하면서 화면 표시명만 `SNFCLTSE`의 `PPL=관로`로 표시한다. `MNHL` 기능은 새로 활성화하지 않았다.
|
||||
- CMS 조회 실패 시 기존 업로드·다운로드·하수관로 문구를 fallback으로 사용한다.
|
||||
- 백엔드 API, 업로드·다운로드 payload와 DB 계약은 변경하지 않았다.
|
||||
|
||||
@@ -488,3 +488,13 @@
|
||||
- 편집 재진입 시 `gisEditSessionView`를 `inline`으로 복원한다.
|
||||
- 팝업 sync effect도 편집 모드가 꺼져 있으면 실행하지 않는다.
|
||||
- `useGisEdit`의 변경분 배열은 유지하므로 다시 진입하면 기존 변경 카드가 복원된다.
|
||||
|
||||
### 4.32 2026-09-01 GIS 편집작업 공통코드 1차 적용
|
||||
|
||||
- 기존 CMS 호출 구조인 `getCmsData('/api/v1/code/cache/{upCd}')`를 유지한 `getCommonCodes(upCd)` 타입 안전 조회 함수를 추가했다.
|
||||
- 기존 `searchCmmnCd()`도 같은 조회 함수를 사용하도록 정리해 CMS의 배열 응답을 `getData()`의 공통 응답 wrapper로 잘못 해제하던 경로를 제거했다.
|
||||
- `useGisEditOperationLabels()`는 `GISEDITOPRT`를 브라우저 세션당 한 번 조회하고 툴바와 인라인·윈도우 편집 세션이 같은 코드명을 공유한다.
|
||||
- `ADD`, `MOVE`, `VERTEX_EDIT`, `DELETE` 타입과 상태 분기는 유지하고 표시명만 공통코드의 `lwpoCdNm`으로 교체했다.
|
||||
- CMS 조회 실패 시에는 기존 `추가/이동/꼭지점편집/삭제` 문구를 fallback으로 사용해 편집 기능 자체가 중단되지 않게 했다.
|
||||
- `GISEDITTYPE(C/U/D)`는 실제 CMS 응답까지 확인했지만 현재 SN GIS 편집 화면에 직접 표시되는 소비처가 없어 불필요한 조회는 추가하지 않았다.
|
||||
- 백엔드 API, 저장 payload, DB 구조는 변경하지 않았다.
|
||||
|
||||
@@ -139,3 +139,12 @@
|
||||
- 공통 팝업의 12px 간격과 중복되던 별도 여백을 제거하고, WindowPopup 크기를 `1400×720`으로 조정했다. 빈 목록은 목록 카드가 남은 세로 공간을 채우도록 해 카드 밖에 과도한 공백이 남지 않게 했다.
|
||||
- `StandardDataTable`에 팝업 전용 바깥 margin 제거 옵션을 추가해 이 팝업의 검색 카드와 목록 카드 좌우 기준선을 동일하게 맞췄다. 일반 페이지의 기존 카드 margin은 그대로 보존한다. 빈 목록은 남은 높이를 채우고, 조회 결과가 있으면 표의 자연 높이를 보존해 팝업 본문만 세로 이동하도록 공통 동작을 구분했다.
|
||||
- API, 조회조건, 다중선택, Excel 다운로드, 등록과 부모창 메시지 계약은 변경하지 않았다.
|
||||
|
||||
## 2026-09-01 운영사업 공통코드 화면 적용
|
||||
|
||||
- `OBSLTBIZSE`, `EVDNCSBMSNSTTS`, `BIZPRGRSSTTS`를 기존 CMS 캐시 API로 조회하고 브라우저 세션에서 재사용한다.
|
||||
- 사업구분 Select는 CMS 코드명을 기존 한글 전송값으로 사용해 `정밀조사/정비사업` DB·API 계약을 유지한다.
|
||||
- 추진현황과 증빙서류 상태는 기존 `준공`, `발주중`, `O/X`, `공단` 등의 자유문자를 보존하면서 CMS 표준값을 datalist 선택 후보로 제공한다.
|
||||
- 목록은 CMS 코드값 또는 코드명과 정확히 일치하는 값만 코드명으로 표시하고 미등록 기존 값은 그대로 표시한다.
|
||||
- 영문 `lwpoCd` 저장 전환과 기존 데이터 이관은 이번 범위에서 수행하지 않았다.
|
||||
- 백엔드 API, Excel 입출력과 DB는 변경하지 않았다.
|
||||
|
||||
@@ -14,3 +14,11 @@
|
||||
|
||||
- 신규 FE feature와 route만 추가했다.
|
||||
- 기존 공통 store와 UI 컴포넌트는 소비만 하며 수정하지 않았다.
|
||||
|
||||
## 2026-09-01 운영사업 공통코드 화면 적용
|
||||
|
||||
- `CTYFLDNGPRGRSSE`, `CTYFLDNGDSGNTYPE`, `BIZPRGRSSTTS`를 기존 CMS 캐시 API로 조회한다.
|
||||
- `EM/MP/DD/CW` 상세 구역 제목을 CMS 코드명으로 표시하되 상세 코드와 Excel 열 순서는 유지한다.
|
||||
- 지정유형과 두 추진현황 입력은 자유문자 기존값을 유지하면서 CMS 표준값을 datalist 후보로 제공한다.
|
||||
- 목록은 CMS와 정확히 일치하는 지정유형·추진현황만 코드명으로 표시하고 `완료(20.06.)` 같은 기존 상세값은 그대로 표시한다.
|
||||
- 영문 코드 저장과 기존 데이터 이관은 수행하지 않았으며 백엔드 API·Excel·DB는 변경하지 않았다.
|
||||
|
||||
@@ -32,6 +32,7 @@ import type {
|
||||
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { useOperBusinessCommonCodes } from "@/shared/lib/use-oper-business-common-codes";
|
||||
import useStore from "@/store/useStore";
|
||||
|
||||
const DETAIL_META = [
|
||||
@@ -281,6 +282,12 @@ export function CtyFldngPrvtnBizFormPage({ id }: { id?: string }) {
|
||||
const searchParams = useSearchParams();
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
// 자유문자 기존값은 유지하고 지정유형·추진현황 후보와 단계명을 CMS에서 제공한다.
|
||||
const {
|
||||
cityDesignationTypeOptions,
|
||||
businessProgressOptions,
|
||||
cityProgressStageLabels,
|
||||
} = useOperBusinessCommonCodes();
|
||||
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
|
||||
const [form, setForm] = useState<CtyFldngPrvtnBizSaveRequest>(emptyForm);
|
||||
const [loading, setLoading] = useState(Boolean(id));
|
||||
@@ -372,6 +379,14 @@ export function CtyFldngPrvtnBizFormPage({ id }: { id?: string }) {
|
||||
type,
|
||||
]: (typeof BASIC_FIELDS)[number]) => {
|
||||
const fieldId = `cty-fldng-basic-${key}`;
|
||||
const suggestionOptions =
|
||||
key === "dsgnType"
|
||||
? cityDesignationTypeOptions
|
||||
: key === "prplPrst" || key === "bizPrplPrst"
|
||||
? businessProgressOptions
|
||||
: [];
|
||||
const suggestionListId =
|
||||
suggestionOptions.length > 0 ? `${fieldId}-suggestions` : undefined;
|
||||
const span: 1 | 2 | 3 =
|
||||
key === "excptnMttrCn" ? 3 : key === "bizNm" || key === "pstn" ? 2 : 1;
|
||||
return (
|
||||
@@ -382,21 +397,33 @@ export function CtyFldngPrvtnBizFormPage({ id }: { id?: string }) {
|
||||
required={key === "bizNm"}
|
||||
span={span}
|
||||
>
|
||||
<Input
|
||||
id={fieldId}
|
||||
className="w-full !text-[1.3rem]"
|
||||
type={type === "date" ? "date" : type}
|
||||
value={String(form[key] ?? "")}
|
||||
onChange={(event) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
[key]:
|
||||
type === "date"
|
||||
? formatDateInput(event.target.value)
|
||||
: inputValue(event.target.value, type),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<Input
|
||||
id={fieldId}
|
||||
className="w-full !text-[1.3rem]"
|
||||
type={type === "date" ? "date" : type}
|
||||
list={suggestionListId}
|
||||
value={String(form[key] ?? "")}
|
||||
onChange={(event) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
[key]:
|
||||
type === "date"
|
||||
? formatDateInput(event.target.value)
|
||||
: inputValue(event.target.value, type),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
{suggestionListId ? (
|
||||
<datalist id={suggestionListId}>
|
||||
{suggestionOptions.map((option) => (
|
||||
<option key={option.code} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</datalist>
|
||||
) : null}
|
||||
</>
|
||||
</StandardFormField>
|
||||
);
|
||||
};
|
||||
@@ -539,7 +566,7 @@ export function CtyFldngPrvtnBizFormPage({ id }: { id?: string }) {
|
||||
return (
|
||||
<StandardFormSection
|
||||
key={meta.code}
|
||||
title={`${meta.code} ${meta.title}`}
|
||||
title={`${meta.code} ${cityProgressStageLabels[meta.code] || meta.title}`}
|
||||
>
|
||||
<DetailEditor
|
||||
detail={detail}
|
||||
|
||||
@@ -42,6 +42,7 @@ import type {
|
||||
} from "../model/cty-fldng-prvtn-biz-types";
|
||||
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import { useOperBusinessCommonCodes } from "@/shared/lib/use-oper-business-common-codes";
|
||||
import useStore from "@/store/useStore";
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
@@ -276,6 +277,13 @@ const flattenRow = (row: CtyFldngPrvtnBiz, rowNumber: number): TableRow => {
|
||||
export function CtyFldngPrvtnBizListPage() {
|
||||
const router = useRouter();
|
||||
const params = useSearchParams();
|
||||
// 원본 자유문자 값은 유지하고 지정유형·추진현황·단계 표시명만 CMS 기준으로 변환한다.
|
||||
const {
|
||||
businessProgressOptions,
|
||||
cityProgressStageLabels,
|
||||
cityDesignationTypeLabels,
|
||||
businessProgressLabels,
|
||||
} = useOperBusinessCommonCodes();
|
||||
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
const defaultDesignationDateRange = useMemo(
|
||||
@@ -302,6 +310,21 @@ export function CtyFldngPrvtnBizListPage() {
|
||||
);
|
||||
const [data, setData] = useState(EMPTY_PAGE);
|
||||
const [rows, setRows] = useState<TableRow[]>([]);
|
||||
const displayRows = useMemo(
|
||||
() =>
|
||||
rows.map((row) => ({
|
||||
...row,
|
||||
dsgnType:
|
||||
cityDesignationTypeLabels[String(row.dsgnType ?? "")] ||
|
||||
row.dsgnType,
|
||||
prplPrst:
|
||||
businessProgressLabels[String(row.prplPrst ?? "")] || row.prplPrst,
|
||||
bizPrplPrst:
|
||||
businessProgressLabels[String(row.bizPrplPrst ?? "")] ||
|
||||
row.bizPrplPrst,
|
||||
})),
|
||||
[businessProgressLabels, cityDesignationTypeLabels, rows],
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [notice, setNotice] = useState("");
|
||||
const [ctpvCd, setCtpvCd] = useState(params.get("ctpvCd") ?? "0");
|
||||
@@ -325,6 +348,14 @@ export function CtyFldngPrvtnBizListPage() {
|
||||
),
|
||||
[],
|
||||
);
|
||||
const detailGroups = useMemo(
|
||||
() =>
|
||||
DETAIL_GROUPS.map((group) => ({
|
||||
...group,
|
||||
title: cityProgressStageLabels[group.code] || group.title,
|
||||
})),
|
||||
[cityProgressStageLabels],
|
||||
);
|
||||
/** 정렬 변경 시 서버 첫 페이지로 돌아가고 동일 검색조건을 유지한다. */
|
||||
const handleSortChange = useCallback(
|
||||
(nextSort?: UiTableSort) => {
|
||||
@@ -356,7 +387,7 @@ export function CtyFldngPrvtnBizListPage() {
|
||||
createTableColumn(`soa_${year}`, `${year}년`, 7),
|
||||
),
|
||||
},
|
||||
...DETAIL_GROUPS.map((group) => ({
|
||||
...detailGroups.map((group) => ({
|
||||
id: `detail_${group.code}`,
|
||||
header: group.title,
|
||||
columns: DETAIL_FIELDS.map((field, index) =>
|
||||
@@ -378,14 +409,14 @@ export function CtyFldngPrvtnBizListPage() {
|
||||
),
|
||||
),
|
||||
],
|
||||
[handleSortChange, sort, years],
|
||||
[detailGroups, handleSortChange, sort, years],
|
||||
);
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: page - 1,
|
||||
pageSize: PAGE_SIZE,
|
||||
};
|
||||
const table = useReactTable({
|
||||
data: rows,
|
||||
data: displayRows,
|
||||
columns,
|
||||
state: { pagination },
|
||||
rowCount: data.totalElements,
|
||||
@@ -658,6 +689,7 @@ export function CtyFldngPrvtnBizListPage() {
|
||||
</Label>
|
||||
<Input
|
||||
className="w-[15rem] !text-[1.3rem]"
|
||||
list="cty-fldng-progress-suggestions"
|
||||
value={search.prplPrst}
|
||||
onChange={(event) =>
|
||||
setSearch((current) => ({
|
||||
@@ -666,6 +698,13 @@ export function CtyFldngPrvtnBizListPage() {
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<datalist id="cty-fldng-progress-suggestions">
|
||||
{businessProgressOptions.map((option) => (
|
||||
<option key={option.code} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import {
|
||||
getCommonCodes,
|
||||
type CommonCodeItem,
|
||||
} from "@/components/lib/common/apiHelpers";
|
||||
|
||||
type CodeLabelMap = Record<string, string>;
|
||||
type ExmnCodeGroup =
|
||||
| "EXMNDFCGRD"
|
||||
| "EXMNRECDTYPE"
|
||||
| "EXMNDFCTYPE"
|
||||
| "EXMNDFCSEV"
|
||||
| "EXMNSTTSGRP"
|
||||
| "PPLEXMNITEM"
|
||||
| "MNHLEXMNITEM";
|
||||
|
||||
const COMMON_CODE_GROUPS: ExmnCodeGroup[] = [
|
||||
"EXMNDFCGRD",
|
||||
"EXMNRECDTYPE",
|
||||
"EXMNDFCTYPE",
|
||||
"EXMNDFCSEV",
|
||||
"EXMNSTTSGRP",
|
||||
"PPLEXMNITEM",
|
||||
"MNHLEXMNITEM",
|
||||
];
|
||||
|
||||
const cachedCodeGroups = new Map<ExmnCodeGroup, CommonCodeItem[]>();
|
||||
const pendingCodeGroups = new Map<
|
||||
ExmnCodeGroup,
|
||||
Promise<CommonCodeItem[]>
|
||||
>();
|
||||
|
||||
const loadCodeGroup = async (upCd: ExmnCodeGroup) => {
|
||||
const cached = cachedCodeGroups.get(upCd);
|
||||
if (cached) return cached;
|
||||
|
||||
const pending = pendingCodeGroups.get(upCd);
|
||||
if (pending) return pending;
|
||||
|
||||
const request = getCommonCodes(upCd)
|
||||
.then((codes) => {
|
||||
cachedCodeGroups.set(upCd, codes);
|
||||
return codes;
|
||||
})
|
||||
.finally(() => {
|
||||
pendingCodeGroups.delete(upCd);
|
||||
});
|
||||
|
||||
pendingCodeGroups.set(upCd, request);
|
||||
return request;
|
||||
};
|
||||
|
||||
const toLabelMap = (codes: CommonCodeItem[]): CodeLabelMap =>
|
||||
Object.fromEntries(codes.map((code) => [code.lwpoCd, code.lwpoCdNm]));
|
||||
|
||||
/** 조사 코드값과 표 열 순서는 유지하고 화면에서 사용할 CMS 코드명만 제공한다. */
|
||||
export const useExmnCommonCodes = () => {
|
||||
const [codeGroups, setCodeGroups] = useState<
|
||||
Partial<Record<ExmnCodeGroup, CommonCodeItem[]>>
|
||||
>(
|
||||
() =>
|
||||
Object.fromEntries(cachedCodeGroups.entries()) as Partial<
|
||||
Record<ExmnCodeGroup, CommonCodeItem[]>
|
||||
>,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
COMMON_CODE_GROUPS.forEach((upCd) => {
|
||||
loadCodeGroup(upCd)
|
||||
.then((codes) => {
|
||||
if (!cancelled) {
|
||||
setCodeGroups((current) => ({ ...current, [upCd]: codes }));
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`${upCd} 공통코드 조회에 실패했습니다.`, error);
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const defectGradeOptions = useMemo(
|
||||
() =>
|
||||
codeGroups.EXMNDFCGRD?.map((code) => ({
|
||||
value: code.lwpoCd,
|
||||
label: code.lwpoCdNm,
|
||||
})) ??
|
||||
[1, 2, 3, 4, 5].map((grade) => ({
|
||||
value: String(grade),
|
||||
label: `${grade}등급`,
|
||||
})),
|
||||
[codeGroups.EXMNDFCGRD],
|
||||
);
|
||||
|
||||
const recordTypeLabels = useMemo(
|
||||
() => toLabelMap(codeGroups.EXMNRECDTYPE ?? []),
|
||||
[codeGroups.EXMNRECDTYPE],
|
||||
);
|
||||
const defectTypeLabels = useMemo(
|
||||
() => toLabelMap(codeGroups.EXMNDFCTYPE ?? []),
|
||||
[codeGroups.EXMNDFCTYPE],
|
||||
);
|
||||
const severityLabels = useMemo(
|
||||
() => toLabelMap(codeGroups.EXMNDFCSEV ?? []),
|
||||
[codeGroups.EXMNDFCSEV],
|
||||
);
|
||||
const statusGroupLabels = useMemo(
|
||||
() => toLabelMap(codeGroups.EXMNSTTSGRP ?? []),
|
||||
[codeGroups.EXMNSTTSGRP],
|
||||
);
|
||||
const pipeItemLabels = useMemo(
|
||||
() => toLabelMap(codeGroups.PPLEXMNITEM ?? []),
|
||||
[codeGroups.PPLEXMNITEM],
|
||||
);
|
||||
const manholeItemLabels = useMemo(
|
||||
() => toLabelMap(codeGroups.MNHLEXMNITEM ?? []),
|
||||
[codeGroups.MNHLEXMNITEM],
|
||||
);
|
||||
|
||||
return {
|
||||
defectGradeOptions,
|
||||
recordTypeLabels,
|
||||
defectTypeLabels,
|
||||
severityLabels,
|
||||
statusGroupLabels,
|
||||
pipeItemLabels,
|
||||
manholeItemLabels,
|
||||
};
|
||||
};
|
||||
@@ -6,8 +6,9 @@ import {
|
||||
type ExmnDetailSeverity,
|
||||
type ExmnPplUnitDetailRow,
|
||||
} from "../model/exmn-detail-table";
|
||||
import { useExmnCommonCodes } from "../lib/useExmnCommonCodes";
|
||||
|
||||
const SEVERITY_LABELS: Record<ExmnDetailSeverity, string> = {
|
||||
const DEFAULT_SEVERITY_LABELS: Record<ExmnDetailSeverity, string> = {
|
||||
L: "대",
|
||||
M: "중",
|
||||
S: "소",
|
||||
@@ -54,6 +55,13 @@ export function ExmnPplUnitDetailTable({
|
||||
rows,
|
||||
emptyMessage,
|
||||
}: ExmnPplUnitDetailTableProps) {
|
||||
// 본단위 행의 기록유형·심각도·관로항목 표시명만 CMS 코드명으로 교체한다.
|
||||
const { recordTypeLabels, severityLabels: cmsSeverityLabels, pipeItemLabels } =
|
||||
useExmnCommonCodes();
|
||||
const severityLabels: Record<ExmnDetailSeverity, string> = {
|
||||
...DEFAULT_SEVERITY_LABELS,
|
||||
...cmsSeverityLabels,
|
||||
};
|
||||
const pipeSpans = buildRowSpanMap(rows, (row) => row.pipeNo);
|
||||
const unitSpans = buildRowSpanMap(
|
||||
rows,
|
||||
@@ -208,13 +216,15 @@ export function ExmnPplUnitDetailTable({
|
||||
{formatOptionalValue(row.defectPositionM)}
|
||||
</td>
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem] text-left">
|
||||
{row.defectName}
|
||||
{row.recordType
|
||||
? `(${recordTypeLabels[row.recordType] || row.recordType})${pipeItemLabels[row.code] || row.defectName.replace(/^\([^)]*\)/, "")}`
|
||||
: row.defectName}
|
||||
</td>
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem]">
|
||||
{row.code}
|
||||
</td>
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem]">
|
||||
{row.severity ? SEVERITY_LABELS[row.severity] : "-"}
|
||||
{row.severity ? severityLabels[row.severity] : "-"}
|
||||
</td>
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem]">
|
||||
{formatOptionalValue(row.occurrencePosition)}
|
||||
|
||||
@@ -46,11 +46,12 @@ import {
|
||||
type ExmnSeverityCounts,
|
||||
type ExmnSurveyDetailRow,
|
||||
} from "../model/exmn-detail-table";
|
||||
import { useExmnCommonCodes } from "../lib/useExmnCommonCodes";
|
||||
|
||||
import { ExmnPplUnitDetailTable } from "./ExmnPplUnitDetailTable";
|
||||
import { ExmnSurveyStandardDataTable } from "./ExmnSurveyStandardDataTable";
|
||||
|
||||
const SEVERITY_LABELS: Record<ExmnDetailSeverity, string> = {
|
||||
const DEFAULT_SEVERITY_LABELS: Record<ExmnDetailSeverity, string> = {
|
||||
L: "대",
|
||||
M: "중",
|
||||
S: "소",
|
||||
@@ -62,7 +63,7 @@ const PPL_DETAIL_MODE_LABELS: Record<ExmnPplDetailMode, string> = {
|
||||
UNIT: "본단위구간",
|
||||
};
|
||||
|
||||
const DEFECT_TYPE_LABELS: Record<ExmnDetailDefectType, string> = {
|
||||
const DEFAULT_DEFECT_TYPE_LABELS: Record<ExmnDetailDefectType, string> = {
|
||||
ALL: "전체",
|
||||
STRUCTURAL: "구조적결함",
|
||||
OPERATIONAL: "운영적결함",
|
||||
@@ -157,6 +158,14 @@ export function ExmnSurveyDetailPopupPage({
|
||||
jobType,
|
||||
defectType,
|
||||
}: ExmnSurveyDetailPopupPageProps) {
|
||||
// 표의 코드·열 순서는 유지하고 결함분류·심각도·조사항목 표시명만 CMS 기준으로 사용한다.
|
||||
const {
|
||||
recordTypeLabels,
|
||||
defectTypeLabels: cmsDefectTypeLabels,
|
||||
severityLabels: cmsSeverityLabels,
|
||||
pipeItemLabels,
|
||||
manholeItemLabels,
|
||||
} = useExmnCommonCodes();
|
||||
/** 현재 조사유형에 실제 결과가 등록된 처리분구 목록과 팝업 조회 대상이다. */
|
||||
const [districtOptions, setDistrictOptions] = useState<
|
||||
ExmnSectionSummaryRow[]
|
||||
@@ -183,8 +192,26 @@ export function ExmnSurveyDetailPopupPage({
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
const definition = EXMN_DETAIL_TABLE_DEFINITIONS[jobType];
|
||||
const defectTypeLabel = DEFECT_TYPE_LABELS[defectType];
|
||||
const severityLabels: Record<ExmnDetailSeverity, string> = {
|
||||
...DEFAULT_SEVERITY_LABELS,
|
||||
...cmsSeverityLabels,
|
||||
};
|
||||
const defectTypeLabels: Record<ExmnDetailDefectType, string> = {
|
||||
...DEFAULT_DEFECT_TYPE_LABELS,
|
||||
...cmsDefectTypeLabels,
|
||||
};
|
||||
const definition = useMemo(() => {
|
||||
const baseDefinition = EXMN_DETAIL_TABLE_DEFINITIONS[jobType];
|
||||
const itemLabels = jobType === "PPL" ? pipeItemLabels : manholeItemLabels;
|
||||
return {
|
||||
...baseDefinition,
|
||||
defectColumns: baseDefinition.defectColumns.map((column) => ({
|
||||
...column,
|
||||
label: itemLabels[column.code] || column.label,
|
||||
})),
|
||||
};
|
||||
}, [jobType, manholeItemLabels, pipeItemLabels]);
|
||||
const defectTypeLabel = defectTypeLabels[defectType];
|
||||
const selectedDistrict = districtOptions.find(
|
||||
(district) => district.trtdstId === selectedTrtdstId,
|
||||
);
|
||||
@@ -523,9 +550,11 @@ export function ExmnSurveyDetailPopupPage({
|
||||
unitSeq: row.unitSeq,
|
||||
unitLengthM: row.unitLengthM,
|
||||
defectPositionM: row.defectPositionM,
|
||||
defectName: row.defectName,
|
||||
defectName: row.recordType
|
||||
? `(${recordTypeLabels[row.recordType] || row.recordType})${pipeItemLabels[row.code] || row.defectName.replace(/^\([^)]*\)/, "")}`
|
||||
: row.defectName,
|
||||
code: row.code,
|
||||
severity: row.severity ? SEVERITY_LABELS[row.severity] : "-",
|
||||
severity: row.severity ? severityLabels[row.severity] : "-",
|
||||
occurrencePosition: row.occurrencePosition,
|
||||
infiltrationBonus: row.infiltrationBonus,
|
||||
continuous: row.continuous == null ? "-" : row.continuous ? "Y" : "N",
|
||||
@@ -576,7 +605,7 @@ export function ExmnSurveyDetailPopupPage({
|
||||
visibleDefectColumns.forEach((column) => {
|
||||
EXMN_DETAIL_SEVERITIES.forEach((severity) => {
|
||||
detailHeaderMapping[`${column.code}_${severity}`] =
|
||||
`${column.label} (${column.code})-${SEVERITY_LABELS[severity]}`;
|
||||
`${column.label} (${column.code})-${severityLabels[severity]}`;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1049,7 +1078,7 @@ export function ExmnSurveyDetailPopupPage({
|
||||
key={`total-${severity}`}
|
||||
className="border-b border-r border-border !py-[0.6rem] px-2 text-center"
|
||||
>
|
||||
{SEVERITY_LABELS[severity]}
|
||||
{severityLabels[severity]}
|
||||
</th>
|
||||
))}
|
||||
{visibleDefectColumns.flatMap((column) =>
|
||||
@@ -1058,7 +1087,7 @@ export function ExmnSurveyDetailPopupPage({
|
||||
key={`${column.code}-${severity}`}
|
||||
className="border-b border-r border-border !py-[0.6rem] px-2 text-center font-medium"
|
||||
>
|
||||
{SEVERITY_LABELS[severity]}
|
||||
{severityLabels[severity]}
|
||||
</th>
|
||||
)),
|
||||
)}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
DialogTitle,
|
||||
} from "@common/components/ui";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useExmnCommonCodes } from "../lib/useExmnCommonCodes";
|
||||
import type { PplFacility } from "../model/exmn-business-types";
|
||||
import { ExmnStandardDataTable } from "./ExmnStandardDataTable";
|
||||
|
||||
@@ -49,9 +50,25 @@ export function PplFacilityDetailModal({
|
||||
facility,
|
||||
onOpenChange,
|
||||
}: PplFacilityDetailModalProps) {
|
||||
// 정규화 상세의 코드값은 유지하고 기록유형·분류·심각도·상태그룹 표시명만 변환한다.
|
||||
const {
|
||||
defectGradeOptions,
|
||||
recordTypeLabels,
|
||||
defectTypeLabels,
|
||||
severityLabels,
|
||||
statusGroupLabels,
|
||||
} = useExmnCommonCodes();
|
||||
// 선택 단위는 원문 배열 인덱스가 아니라 unitSeq로 보관해 서버 정렬과 무관하게 유지한다.
|
||||
const [selectedUnitSeq, setSelectedUnitSeq] = useState<number | null>(null);
|
||||
|
||||
const defectGradeLabels = useMemo(
|
||||
() =>
|
||||
Object.fromEntries(
|
||||
defectGradeOptions.map((option) => [option.value, option.label]),
|
||||
),
|
||||
[defectGradeOptions],
|
||||
);
|
||||
|
||||
/** 시설이 바뀌면 첫 단위 구간을 기본 선택한다. */
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -62,9 +79,15 @@ export function PplFacilityDetailModal({
|
||||
() =>
|
||||
(facility?.unitSections ?? []).map((unit) => ({
|
||||
...unit,
|
||||
structuralGrade:
|
||||
defectGradeLabels[String(unit.structuralGrade)] ||
|
||||
unit.structuralGrade,
|
||||
operationalGrade:
|
||||
defectGradeLabels[String(unit.operationalGrade)] ||
|
||||
unit.operationalGrade,
|
||||
detailCount: unit.details?.length ?? 0,
|
||||
})),
|
||||
[facility],
|
||||
[defectGradeLabels, facility],
|
||||
);
|
||||
|
||||
const selectedUnit = useMemo(
|
||||
@@ -77,18 +100,32 @@ export function PplFacilityDetailModal({
|
||||
|
||||
const detailRows = useMemo(
|
||||
() =>
|
||||
(selectedUnit?.details ?? []).map((detail) => ({
|
||||
...detail,
|
||||
typeText: detail.type ?? "-",
|
||||
severityText: detail.severity ?? "-",
|
||||
occurrencePositionText: detail.occurrencePosition ?? "-",
|
||||
continuousText:
|
||||
detail.continuous == null ? "-" : detail.continuous ? "Y" : "N",
|
||||
structuralScoreText: detail.structuralScore ?? "-",
|
||||
operationalScoreText: detail.operationalScore ?? "-",
|
||||
statusGroupText: detail.statusGroup ?? "-",
|
||||
})),
|
||||
[selectedUnit],
|
||||
(selectedUnit?.details ?? []).map((detail) => {
|
||||
const defectType = detail.type ?? "";
|
||||
const severity = detail.severity ?? "";
|
||||
const statusGroup = detail.statusGroup ?? "";
|
||||
return {
|
||||
...detail,
|
||||
recordType:
|
||||
recordTypeLabels[detail.recordType] || detail.recordType || "-",
|
||||
typeText: defectTypeLabels[defectType] || defectType || "-",
|
||||
severityText: severityLabels[severity] || severity || "-",
|
||||
occurrencePositionText: detail.occurrencePosition ?? "-",
|
||||
continuousText:
|
||||
detail.continuous == null ? "-" : detail.continuous ? "Y" : "N",
|
||||
structuralScoreText: detail.structuralScore ?? "-",
|
||||
operationalScoreText: detail.operationalScore ?? "-",
|
||||
statusGroupText:
|
||||
statusGroupLabels[statusGroup] || statusGroup || "-",
|
||||
};
|
||||
}),
|
||||
[
|
||||
defectTypeLabels,
|
||||
recordTypeLabels,
|
||||
selectedUnit,
|
||||
severityLabels,
|
||||
statusGroupLabels,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -26,6 +26,7 @@ import { PplFacilityDetailModal } from "./PplFacilityDetailModal";
|
||||
import { ExmnStandardDataTable } from "./ExmnStandardDataTable";
|
||||
import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
|
||||
import { fetchAllPages } from "@/shared/lib/fetch-all-pages";
|
||||
import { useExmnCommonCodes } from "../lib/useExmnCommonCodes";
|
||||
|
||||
const SUMMARY_TABLE_HEAD = [
|
||||
{ id: "trtdstNm", title: "처리분구", widthPercent: 20 },
|
||||
@@ -128,6 +129,8 @@ export function PplSummaryModal({
|
||||
business,
|
||||
onOpenChange,
|
||||
}: PplSummaryModalProps) {
|
||||
// 조사시설 검색 코드값은 유지하고 등급·결함분류 표시명만 CMS 기준으로 사용한다.
|
||||
const { defectGradeOptions, defectTypeLabels } = useExmnCommonCodes();
|
||||
const [businessDetail, setBusinessDetail] =
|
||||
useState<ExmnBusinessDetail | null>(null);
|
||||
const [summaryRows, setSummaryRows] = useState<PplSummaryRow[]>([]);
|
||||
@@ -491,9 +494,9 @@ export function PplSummaryModal({
|
||||
onChange={(event) => setStructuralGrade(event.target.value)}
|
||||
>
|
||||
<option value="">전체</option>
|
||||
{[1, 2, 3, 4, 5].map((grade) => (
|
||||
<option key={grade} value={grade}>
|
||||
{grade}등급
|
||||
{defectGradeOptions.map((grade) => (
|
||||
<option key={grade.value} value={grade.value}>
|
||||
{grade.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -508,9 +511,9 @@ export function PplSummaryModal({
|
||||
}
|
||||
>
|
||||
<option value="">전체</option>
|
||||
{[1, 2, 3, 4, 5].map((grade) => (
|
||||
<option key={grade} value={grade}>
|
||||
{grade}등급
|
||||
{defectGradeOptions.map((grade) => (
|
||||
<option key={grade.value} value={grade.value}>
|
||||
{grade.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -518,7 +521,7 @@ export function PplSummaryModal({
|
||||
</div>
|
||||
<div className="mt-3 flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
결함 분류: {defectType}
|
||||
결함 분류: {defectTypeLabels[defectType] || defectType}
|
||||
</span>
|
||||
<Button
|
||||
disabled={isLoading}
|
||||
|
||||
@@ -43,16 +43,13 @@ import type {
|
||||
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { useOperBusinessCommonCodes } from "@/shared/lib/use-oper-business-common-codes";
|
||||
import useStore from "@/store/useStore";
|
||||
|
||||
type FormKey = keyof ObsltPplBusinessSaveRequest;
|
||||
type FormState = Record<FormKey, string>;
|
||||
type FieldErrorKey = FormKey | "region";
|
||||
const REGION_FORM_KEYS: FormKey[] = ["sggCd", "ctpvNm", "sigunNm"];
|
||||
const BIZ_SE_OPTIONS = [
|
||||
{ text: "정밀조사", val: "정밀조사" },
|
||||
{ text: "정비사업", val: "정비사업" },
|
||||
];
|
||||
const EMPTY_SELECT_VALUE = "__none__";
|
||||
type FieldSpan = 1 | 2 | 3;
|
||||
|
||||
@@ -154,6 +151,12 @@ export function ObsltPplFormPage({ id }: { id?: string }) {
|
||||
const searchParams = useSearchParams();
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
// 기존 한글 저장값은 유지하면서 사업구분·추진·증빙 선택 후보를 CMS에서 구성한다.
|
||||
const {
|
||||
obsltBusinessTypeOptions,
|
||||
evidenceStatusOptions,
|
||||
businessProgressOptions,
|
||||
} = useOperBusinessCommonCodes();
|
||||
const isModify = Boolean(id);
|
||||
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
|
||||
const [form, setForm] = useState<FormState>(createEmptyForm);
|
||||
@@ -296,9 +299,9 @@ export function ObsltPplFormPage({ id }: { id?: string }) {
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{BIZ_SE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.val} value={option.val}>
|
||||
{option.text}
|
||||
{obsltBusinessTypeOptions.map((option) => (
|
||||
<SelectItem key={option.code} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -346,25 +349,45 @@ export function ObsltPplFormPage({ id }: { id?: string }) {
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
const suggestionOptions =
|
||||
field.key === "prplPrst"
|
||||
? businessProgressOptions
|
||||
: field.key === "evdncDcmntSbmsnStts"
|
||||
? evidenceStatusOptions
|
||||
: [];
|
||||
const suggestionListId =
|
||||
suggestionOptions.length > 0 ? `${fieldId}-suggestions` : undefined;
|
||||
control = (
|
||||
<Input
|
||||
id={fieldId}
|
||||
className="w-full !text-[1.3rem]"
|
||||
type={field.type === "number" ? "number" : "text"}
|
||||
step={field.type === "number" ? "any" : undefined}
|
||||
maxLength={DATE_KEYS.has(field.key) ? 10 : undefined}
|
||||
value={form[field.key]}
|
||||
placeholder={field.placeholder}
|
||||
onChange={(event) => {
|
||||
clearFieldError(field.key);
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
[field.key]: DATE_KEYS.has(field.key)
|
||||
? formatDateInput(event.target.value)
|
||||
: event.target.value,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<Input
|
||||
id={fieldId}
|
||||
className="w-full !text-[1.3rem]"
|
||||
type={field.type === "number" ? "number" : "text"}
|
||||
step={field.type === "number" ? "any" : undefined}
|
||||
maxLength={DATE_KEYS.has(field.key) ? 10 : undefined}
|
||||
list={suggestionListId}
|
||||
value={form[field.key]}
|
||||
placeholder={field.placeholder}
|
||||
onChange={(event) => {
|
||||
clearFieldError(field.key);
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
[field.key]: DATE_KEYS.has(field.key)
|
||||
? formatDateInput(event.target.value)
|
||||
: event.target.value,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
{suggestionListId ? (
|
||||
<datalist id={suggestionListId}>
|
||||
{suggestionOptions.map((option) => (
|
||||
<option key={option.code} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</datalist>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,16 +46,12 @@ import {
|
||||
import type { ObsltPplBusiness, ObsltPplPage } from "../model/obslt-ppl-types";
|
||||
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import { useOperBusinessCommonCodes } from "@/shared/lib/use-oper-business-common-codes";
|
||||
import useStore from "@/store/useStore";
|
||||
import { openWindowPopup } from "../../windowPopup/lib/openWindowPopup";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "../../windowPopup/model/types";
|
||||
|
||||
const DEFAULT_LIST_SIZE = 10;
|
||||
const BIZ_SE_OPTIONS = [
|
||||
{ text: "전체", val: "0" },
|
||||
{ text: "정밀조사", val: "정밀조사" },
|
||||
{ text: "정비사업", val: "정비사업" },
|
||||
];
|
||||
const ALL_REGION_OPTION = { text: "전체", val: "0" };
|
||||
|
||||
type ObsltPplTableRow = ObsltPplBusiness & {
|
||||
@@ -155,6 +151,12 @@ const formatTableValue = (value: unknown) => {
|
||||
export function ObsltPplListPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
// 검색값·원본 행은 유지하고 사업구분·추진·증빙 표시명만 CMS 기준으로 변환한다.
|
||||
const {
|
||||
obsltBusinessTypeOptions,
|
||||
evidenceStatusLabels,
|
||||
businessProgressLabels,
|
||||
} = useOperBusinessCommonCodes();
|
||||
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const initialSearch = useMemo<ObsltPplSearch>(
|
||||
@@ -171,6 +173,22 @@ export function ObsltPplListPage() {
|
||||
);
|
||||
const [data, setData] = useState<ObsltPplPage>(EMPTY_PAGE);
|
||||
const [rows, setRows] = useState<ObsltPplTableRow[]>([]);
|
||||
const displayRows = useMemo(
|
||||
() =>
|
||||
rows.map((row) => ({
|
||||
...row,
|
||||
prplPrst:
|
||||
businessProgressLabels[String(row.prplPrst ?? "")] || row.prplPrst,
|
||||
evdncDcmntSbmsnStts:
|
||||
evidenceStatusLabels[String(row.evdncDcmntSbmsnStts ?? "")] ||
|
||||
row.evdncDcmntSbmsnStts,
|
||||
})),
|
||||
[
|
||||
businessProgressLabels,
|
||||
evidenceStatusLabels,
|
||||
rows,
|
||||
],
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [notice, setNotice] = useState("");
|
||||
// 선택박스의 코드와 API 검색에 사용하는 행정구역명을 함께 관리한다.
|
||||
@@ -257,7 +275,7 @@ export function ObsltPplListPage() {
|
||||
|
||||
/** 서버 페이지 번호와 URL 복귀 조건을 공통 DataTable 페이지 상태에 연결한다. */
|
||||
const table = useReactTable({
|
||||
data: rows,
|
||||
data: displayRows,
|
||||
columns,
|
||||
state: { pagination },
|
||||
rowCount: data.totalElements,
|
||||
@@ -510,9 +528,10 @@ export function ObsltPplListPage() {
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{BIZ_SE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.val} value={option.val}>
|
||||
{option.text}
|
||||
<SelectItem value="0">전체</SelectItem>
|
||||
{obsltBusinessTypeOptions.map((option) => (
|
||||
<SelectItem key={option.code} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import {
|
||||
getCommonCodes,
|
||||
type CommonCodeItem,
|
||||
} from "@/components/lib/common/apiHelpers";
|
||||
|
||||
type CodeLabelMap = Record<string, string>;
|
||||
type OperCodeGroup =
|
||||
| "OBSLTBIZSE"
|
||||
| "EVDNCSBMSNSTTS"
|
||||
| "CTYFLDNGPRGRSSE"
|
||||
| "CTYFLDNGDSGNTYPE"
|
||||
| "BIZPRGRSSTTS";
|
||||
|
||||
const COMMON_CODE_GROUPS: OperCodeGroup[] = [
|
||||
"OBSLTBIZSE",
|
||||
"EVDNCSBMSNSTTS",
|
||||
"CTYFLDNGPRGRSSE",
|
||||
"CTYFLDNGDSGNTYPE",
|
||||
"BIZPRGRSSTTS",
|
||||
];
|
||||
|
||||
const cachedCodeGroups = new Map<OperCodeGroup, CommonCodeItem[]>();
|
||||
const pendingCodeGroups = new Map<
|
||||
OperCodeGroup,
|
||||
Promise<CommonCodeItem[]>
|
||||
>();
|
||||
|
||||
const loadCodeGroup = async (upCd: OperCodeGroup) => {
|
||||
const cached = cachedCodeGroups.get(upCd);
|
||||
if (cached) return cached;
|
||||
|
||||
const pending = pendingCodeGroups.get(upCd);
|
||||
if (pending) return pending;
|
||||
|
||||
const request = getCommonCodes(upCd)
|
||||
.then((codes) => {
|
||||
cachedCodeGroups.set(upCd, codes);
|
||||
return codes;
|
||||
})
|
||||
.finally(() => {
|
||||
pendingCodeGroups.delete(upCd);
|
||||
});
|
||||
|
||||
pendingCodeGroups.set(upCd, request);
|
||||
return request;
|
||||
};
|
||||
|
||||
const toLegacyValueOptions = (codes: CommonCodeItem[]) =>
|
||||
codes.map((code) => ({
|
||||
// 현행 운영사업 DB는 한글 설명값을 저장하므로 코드명을 기존 전송값으로 유지한다.
|
||||
value: code.lwpoCdNm,
|
||||
label: code.lwpoCdNm,
|
||||
code: code.lwpoCd,
|
||||
}));
|
||||
|
||||
const toLabelMap = (codes: CommonCodeItem[]): CodeLabelMap => {
|
||||
const labels: CodeLabelMap = {};
|
||||
codes.forEach((code) => {
|
||||
labels[code.lwpoCd] = code.lwpoCdNm;
|
||||
labels[code.lwpoCdNm] = code.lwpoCdNm;
|
||||
});
|
||||
return labels;
|
||||
};
|
||||
|
||||
/** 운영사업의 기존 한글 저장계약을 유지하면서 CMS 선택지·표시명을 제공한다. */
|
||||
export const useOperBusinessCommonCodes = () => {
|
||||
const [codeGroups, setCodeGroups] = useState<
|
||||
Partial<Record<OperCodeGroup, CommonCodeItem[]>>
|
||||
>(
|
||||
() =>
|
||||
Object.fromEntries(cachedCodeGroups.entries()) as Partial<
|
||||
Record<OperCodeGroup, CommonCodeItem[]>
|
||||
>,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
COMMON_CODE_GROUPS.forEach((upCd) => {
|
||||
loadCodeGroup(upCd)
|
||||
.then((codes) => {
|
||||
if (!cancelled) {
|
||||
setCodeGroups((current) => ({ ...current, [upCd]: codes }));
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`${upCd} 공통코드 조회에 실패했습니다.`, error);
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const obsltBusinessTypeOptions = useMemo(
|
||||
() =>
|
||||
codeGroups.OBSLTBIZSE?.length
|
||||
? toLegacyValueOptions(codeGroups.OBSLTBIZSE)
|
||||
: [
|
||||
{ value: "정밀조사", label: "정밀조사", code: "PRCIS_EXMN" },
|
||||
{ value: "정비사업", label: "정비사업", code: "MNTN_BIZ" },
|
||||
],
|
||||
[codeGroups.OBSLTBIZSE],
|
||||
);
|
||||
|
||||
const evidenceStatusOptions = useMemo(
|
||||
() => toLegacyValueOptions(codeGroups.EVDNCSBMSNSTTS ?? []),
|
||||
[codeGroups.EVDNCSBMSNSTTS],
|
||||
);
|
||||
const cityDesignationTypeOptions = useMemo(
|
||||
() => toLegacyValueOptions(codeGroups.CTYFLDNGDSGNTYPE ?? []),
|
||||
[codeGroups.CTYFLDNGDSGNTYPE],
|
||||
);
|
||||
const businessProgressOptions = useMemo(
|
||||
() => toLegacyValueOptions(codeGroups.BIZPRGRSSTTS ?? []),
|
||||
[codeGroups.BIZPRGRSSTTS],
|
||||
);
|
||||
const cityProgressStageLabels = useMemo(
|
||||
() => toLabelMap(codeGroups.CTYFLDNGPRGRSSE ?? []),
|
||||
[codeGroups.CTYFLDNGPRGRSSE],
|
||||
);
|
||||
const obsltBusinessTypeLabels = useMemo(
|
||||
() => toLabelMap(codeGroups.OBSLTBIZSE ?? []),
|
||||
[codeGroups.OBSLTBIZSE],
|
||||
);
|
||||
const evidenceStatusLabels = useMemo(
|
||||
() => toLabelMap(codeGroups.EVDNCSBMSNSTTS ?? []),
|
||||
[codeGroups.EVDNCSBMSNSTTS],
|
||||
);
|
||||
const cityDesignationTypeLabels = useMemo(
|
||||
() => toLabelMap(codeGroups.CTYFLDNGDSGNTYPE ?? []),
|
||||
[codeGroups.CTYFLDNGDSGNTYPE],
|
||||
);
|
||||
const businessProgressLabels = useMemo(
|
||||
() => toLabelMap(codeGroups.BIZPRGRSSTTS ?? []),
|
||||
[codeGroups.BIZPRGRSSTTS],
|
||||
);
|
||||
|
||||
return {
|
||||
obsltBusinessTypeOptions,
|
||||
evidenceStatusOptions,
|
||||
cityDesignationTypeOptions,
|
||||
businessProgressOptions,
|
||||
cityProgressStageLabels,
|
||||
obsltBusinessTypeLabels,
|
||||
evidenceStatusLabels,
|
||||
cityDesignationTypeLabels,
|
||||
businessProgressLabels,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user