diff --git a/apps/sn/app/(map-layout)/map/components/gisEdit/GisEditSessionPanel.tsx b/apps/sn/app/(map-layout)/map/components/gisEdit/GisEditSessionPanel.tsx index 54a7c1b74..c5ef3e8d5 100644 --- a/apps/sn/app/(map-layout)/map/components/gisEdit/GisEditSessionPanel.tsx +++ b/apps/sn/app/(map-layout)/map/components/gisEdit/GisEditSessionPanel.tsx @@ -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 = { - 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, +) => + 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; onFocusChange: (changeId: string) => void; }; const ChangeList = ({ changes, focusedChangeId, + operationLabels, onFocusChange, }: ChangeListProps) => ( <> @@ -65,7 +66,8 @@ const ChangeList = ({ >
- {index + 1}. {getChangeLabel(change)} / {change.objectLabel} + {index + 1}. {getChangeLabel(change, operationLabels)} /{" "} + {change.objectLabel}
{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] : "대기" : "편집 모드 종료"}
@@ -190,6 +194,7 @@ export default function GisEditSessionPanel({
diff --git a/apps/sn/app/(map-layout)/map/components/gisEdit/GisEditToolbar.tsx b/apps/sn/app/(map-layout)/map/components/gisEdit/GisEditToolbar.tsx index 003b01d5c..932146b33 100644 --- a/apps/sn/app/(map-layout)/map/components/gisEdit/GisEditToolbar.tsx +++ b/apps/sn/app/(map-layout)/map/components/gisEdit/GisEditToolbar.tsx @@ -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({
- {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 (
= { + ADD: "추가", + MOVE: "이동", + VERTEX_EDIT: "꼭지점편집", + DELETE: "삭제", +}; + +let cachedOperationLabels: Record | null = null; +let operationLabelsPromise: Promise> | 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 + >(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; +}; diff --git a/apps/sn/app/(map-layout)/map/components/gisManagement/GisManagementPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/gisManagement/GisManagementPopupPage.tsx index d6bceee8e..a4a224440 100644 --- a/apps/sn/app/(map-layout)/map/components/gisManagement/GisManagementPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/gisManagement/GisManagementPopupPage.tsx @@ -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 => { return job; }; -const waitForDownloadJob = async (jobId: string): Promise => { +const waitForDownloadJob = async ( + jobId: string, + onStatus?: DownloadJobStatusListener, +): Promise => { 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(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() {
@@ -787,7 +786,9 @@ const DredgingRegisterModal = ({ : "bg-amber-100 text-amber-800" }`} > - {statusLabel(photo.metadata.status)} + {photoStatusLabels[ + photo.metadata.status + ] || "수동 입력 필요"} @@ -890,9 +891,9 @@ const DredgingRegisterModal = ({ {hasUnappliedLocationDraft ? "새 위치 선택됨" : activeGroup.manualLocation - ? "지도 지정 위치" + ? `${locationSourceLabels.MANUAL} 위치` : activeResolvedLocation - ? "사진 GPS 위치" + ? `${locationSourceLabels.EXIF} 위치` : "위치 지정 필요"} diff --git a/apps/sn/app/(with-base-layout)/oper/dredging/lib/useDredgingCommonCodes.ts b/apps/sn/app/(with-base-layout)/oper/dredging/lib/useDredgingCommonCodes.ts new file mode 100644 index 000000000..bf64d139a --- /dev/null +++ b/apps/sn/app/(with-base-layout)/oper/dredging/lib/useDredgingCommonCodes.ts @@ -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; + +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 = { + EXIF: "사진 GPS", + MANUAL: "지도 지정", +}; + +const DEFAULT_PHOTO_STATUS_LABELS: Record = { + COMPLETE: "정상", + MISSING_DATE: "촬영일 없음", + MISSING_GPS: "GPS 없음", + MISSING_BOTH: "촬영일·GPS 없음", + UNSUPPORTED: "수동 입력 필요", +}; + +const cachedLabelMaps = new Map(); +const pendingLabelMaps = new Map>(); + +/** 같은 브라우저 세션에서는 준설 코드그룹별 CMS 요청을 한 번만 수행한다. */ +const loadCodeLabelMap = async ( + upCd: string, + fallback: CodeLabelMap, +): Promise => { + 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( + cachedLabelMaps.get(COMMON_CODE_GROUPS.registrationMode) ?? + DEFAULT_REGISTRATION_MODE_LABELS, + ); + const [locationSourceLabels, setLocationSourceLabels] = + useState( + cachedLabelMaps.get(COMMON_CODE_GROUPS.locationSource) ?? + DEFAULT_LOCATION_SOURCE_LABELS, + ); + const [photoStatusLabels, setPhotoStatusLabels] = useState( + 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, + }; +}; diff --git a/apps/sn/app/components/lib/common/apiHelpers.ts b/apps/sn/app/components/lib/common/apiHelpers.ts index 8f9abbf08..9094b75de 100644 --- a/apps/sn/app/components/lib/common/apiHelpers.ts +++ b/apps/sn/app/components/lib/common/apiHelpers.ts @@ -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 => { + 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>(url: string, paramObj: T) => { const params = new URLSearchParams( @@ -29,10 +69,10 @@ export const searchData = async>(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; -} \ No newline at end of file +}; diff --git a/apps/sn/docs/작업검증/SFR-010-004~SFR-010-007-조사사업/002-qa-SFR-010-004~007-조사사업-통합검증결과.md b/apps/sn/docs/작업검증/SFR-010-004~SFR-010-007-조사사업/002-qa-SFR-010-004~007-조사사업-통합검증결과.md index 2d7a1d106..88cc194e3 100644 --- a/apps/sn/docs/작업검증/SFR-010-004~SFR-010-007-조사사업/002-qa-SFR-010-004~007-조사사업-통합검증결과.md +++ b/apps/sn/docs/작업검증/SFR-010-004~SFR-010-007-조사사업/002-qa-SFR-010-004~007-조사사업-통합검증결과.md @@ -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, 본단위등급표와 시설 상세의 코드명 수동 검증이 남아 있다. diff --git a/apps/sn/docs/작업검증/SFR-010-010-지반침하정보/002-qa-SFR-010-010-지반침하정보-통합검증결과.md b/apps/sn/docs/작업검증/SFR-010-010-지반침하정보/002-qa-SFR-010-010-지반침하정보-통합검증결과.md index 5ddd15323..946a954e7 100644 --- a/apps/sn/docs/작업검증/SFR-010-010-지반침하정보/002-qa-SFR-010-010-지반침하정보-통합검증결과.md +++ b/apps/sn/docs/작업검증/SFR-010-010-지반침하정보/002-qa-SFR-010-010-지반침하정보-통합검증결과.md @@ -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·상세 표시와 조회 결과 건수의 수동 검증이 남아 있다. diff --git a/apps/sn/docs/작업검증/SFR-010-012-준설관리/001-qa-SFR-010-012-준설관리-검증결과.md b/apps/sn/docs/작업검증/SFR-010-012-준설관리/001-qa-SFR-010-012-준설관리-검증결과.md index fabf5fa86..125937ce7 100644 --- a/apps/sn/docs/작업검증/SFR-010-012-준설관리/001-qa-SFR-010-012-준설관리-검증결과.md +++ b/apps/sn/docs/작업검증/SFR-010-012-준설관리/001-qa-SFR-010-012-준설관리-검증결과.md @@ -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 빌드는 수행하지 않았다. +- 등록·수정 팝업에서 버튼명, 위치출처, 사진상태 표시와 기존 저장 흐름의 수동 검증이 남아 있다. diff --git a/apps/sn/docs/작업검증/SFR-012-002-하수관망도 업로드/002-qa-SFR-012-002-하수관망도 업로드-통합검증결과.md b/apps/sn/docs/작업검증/SFR-012-002-하수관망도 업로드/002-qa-SFR-012-002-하수관망도 업로드-통합검증결과.md index 1c697b1df..025cf7b83 100644 --- a/apps/sn/docs/작업검증/SFR-012-002-하수관망도 업로드/002-qa-SFR-012-002-하수관망도 업로드-통합검증결과.md +++ b/apps/sn/docs/작업검증/SFR-012-002-하수관망도 업로드/002-qa-SFR-012-002-하수관망도 업로드-통합검증결과.md @@ -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을 확인하는 수동 검증이 남아 있다. diff --git a/apps/sn/docs/작업검증/SFR-012-003-하수관로편집/002-qa-SFR-012-003-하수관로편집-통합검증결과.md b/apps/sn/docs/작업검증/SFR-012-003-하수관로편집/002-qa-SFR-012-003-하수관로편집-통합검증결과.md index 93697c3fe..8a31d7391 100644 --- a/apps/sn/docs/작업검증/SFR-012-003-하수관로편집/002-qa-SFR-012-003-하수관로편집-통합검증결과.md +++ b/apps/sn/docs/작업검증/SFR-012-003-하수관로편집/002-qa-SFR-012-003-하수관로편집-통합검증결과.md @@ -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 응답으로 표시되는지는 로컬 배포 또는 개발 서버 기동 후 수동 확인이 남아 있다. diff --git a/apps/sn/docs/작업검증/SN-노후하수관로정비사업/002-qa-SN-노후하수관로정비사업-통합검증결과.md b/apps/sn/docs/작업검증/SN-노후하수관로정비사업/002-qa-SN-노후하수관로정비사업-통합검증결과.md index 76b4a4478..93e2fe0d1 100644 --- a/apps/sn/docs/작업검증/SN-노후하수관로정비사업/002-qa-SN-노후하수관로정비사업-통합검증결과.md +++ b/apps/sn/docs/작업검증/SN-노후하수관로정비사업/002-qa-SN-노후하수관로정비사업-통합검증결과.md @@ -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 빌드는 수행하지 않았다. +- 실제 목록·등록·수정에서 선택 후보와 기존값 저장 회귀 확인이 남아 있다. diff --git a/apps/sn/docs/작업검증/SN-도시침수예방사업/001-qa-SN-도시침수예방사업-FE검증.md b/apps/sn/docs/작업검증/SN-도시침수예방사업/001-qa-SN-도시침수예방사업-FE검증.md index 11f1df2d4..7b6577124 100644 --- a/apps/sn/docs/작업검증/SN-도시침수예방사업/001-qa-SN-도시침수예방사업-FE검증.md +++ b/apps/sn/docs/작업검증/SN-도시침수예방사업/001-qa-SN-도시침수예방사업-FE검증.md @@ -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 빌드는 수행하지 않았다. +- 실제 목록·등록·수정에서 구역 제목, 입력 후보와 기존 변형값 유지 확인이 남아 있다. diff --git a/apps/sn/docs/작업내용/SFR-010-004~SFR-010-007-조사사업/001-fe-SFR-010-004~007-조사사업-통합구현기록.md b/apps/sn/docs/작업내용/SFR-010-004~SFR-010-007-조사사업/001-fe-SFR-010-004~007-조사사업-통합구현기록.md index aa702f91b..b61216ca4 100644 --- a/apps/sn/docs/작업내용/SFR-010-004~SFR-010-007-조사사업/001-fe-SFR-010-004~007-조사사업-통합구현기록.md +++ b/apps/sn/docs/작업내용/SFR-010-004~SFR-010-007-조사사업/001-fe-SFR-010-004~007-조사사업-통합구현기록.md @@ -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는 변경하지 않았다. diff --git a/apps/sn/docs/작업내용/SFR-010-010-지반침하정보/001-fe-SFR-010-010-지반침하정보-통합구현기록.md b/apps/sn/docs/작업내용/SFR-010-010-지반침하정보/001-fe-SFR-010-010-지반침하정보-통합구현기록.md index 2babe2395..adb7fecea 100644 --- a/apps/sn/docs/작업내용/SFR-010-010-지반침하정보/001-fe-SFR-010-010-지반침하정보-통합구현기록.md +++ b/apps/sn/docs/작업내용/SFR-010-010-지반침하정보/001-fe-SFR-010-010-지반침하정보-통합구현기록.md @@ -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는 변경하지 않았다. diff --git a/apps/sn/docs/작업내용/SFR-010-012-준설관리/001-fe-SFR-010-012-준설관리-구현기록.md b/apps/sn/docs/작업내용/SFR-010-012-준설관리/001-fe-SFR-010-012-준설관리-구현기록.md index 30a52ba89..0b6a5370c 100644 --- a/apps/sn/docs/작업내용/SFR-010-012-준설관리/001-fe-SFR-010-012-준설관리-구현기록.md +++ b/apps/sn/docs/작업내용/SFR-010-012-준설관리/001-fe-SFR-010-012-준설관리-구현기록.md @@ -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 구조는 변경하지 않았다. diff --git a/apps/sn/docs/작업내용/SFR-012-002-하수관망도 업로드/001-fe-SFR-012-002-하수관망도 업로드-통합구현기록.md b/apps/sn/docs/작업내용/SFR-012-002-하수관망도 업로드/001-fe-SFR-012-002-하수관망도 업로드-통합구현기록.md index 56ed56d70..4eea57a85 100644 --- a/apps/sn/docs/작업내용/SFR-012-002-하수관망도 업로드/001-fe-SFR-012-002-하수관망도 업로드-통합구현기록.md +++ b/apps/sn/docs/작업내용/SFR-012-002-하수관망도 업로드/001-fe-SFR-012-002-하수관망도 업로드-통합구현기록.md @@ -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 계약은 변경하지 않았다. diff --git a/apps/sn/docs/작업내용/SFR-012-003-하수관로편집/001-fe-SFR-012-003-하수관로편집-통합구현기록.md b/apps/sn/docs/작업내용/SFR-012-003-하수관로편집/001-fe-SFR-012-003-하수관로편집-통합구현기록.md index d764b6ba6..63de2ce70 100644 --- a/apps/sn/docs/작업내용/SFR-012-003-하수관로편집/001-fe-SFR-012-003-하수관로편집-통합구현기록.md +++ b/apps/sn/docs/작업내용/SFR-012-003-하수관로편집/001-fe-SFR-012-003-하수관로편집-통합구현기록.md @@ -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 구조는 변경하지 않았다. diff --git a/apps/sn/docs/작업내용/SN-노후하수관로정비사업/001-fe-SN-노후하수관로정비사업-통합구현기록.md b/apps/sn/docs/작업내용/SN-노후하수관로정비사업/001-fe-SN-노후하수관로정비사업-통합구현기록.md index c226d6a37..0bf92de3e 100644 --- a/apps/sn/docs/작업내용/SN-노후하수관로정비사업/001-fe-SN-노후하수관로정비사업-통합구현기록.md +++ b/apps/sn/docs/작업내용/SN-노후하수관로정비사업/001-fe-SN-노후하수관로정비사업-통합구현기록.md @@ -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는 변경하지 않았다. diff --git a/apps/sn/docs/작업내용/SN-도시침수예방사업/001-fe-SN-도시침수예방사업-구현내용.md b/apps/sn/docs/작업내용/SN-도시침수예방사업/001-fe-SN-도시침수예방사업-구현내용.md index ce42efdcb..51fd49900 100644 --- a/apps/sn/docs/작업내용/SN-도시침수예방사업/001-fe-SN-도시침수예방사업-구현내용.md +++ b/apps/sn/docs/작업내용/SN-도시침수예방사업/001-fe-SN-도시침수예방사업-구현내용.md @@ -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는 변경하지 않았다. diff --git a/apps/sn/src/features/ctyFldngPrvtnBiz/ui/CtyFldngPrvtnBizFormPage.tsx b/apps/sn/src/features/ctyFldngPrvtnBiz/ui/CtyFldngPrvtnBizFormPage.tsx index b4e4a2c32..b4db9d898 100644 --- a/apps/sn/src/features/ctyFldngPrvtnBiz/ui/CtyFldngPrvtnBizFormPage.tsx +++ b/apps/sn/src/features/ctyFldngPrvtnBiz/ui/CtyFldngPrvtnBizFormPage.tsx @@ -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(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} > - - setForm((current) => ({ - ...current, - [key]: - type === "date" - ? formatDateInput(event.target.value) - : inputValue(event.target.value, type), - })) - } - /> + <> + + setForm((current) => ({ + ...current, + [key]: + type === "date" + ? formatDateInput(event.target.value) + : inputValue(event.target.value, type), + })) + } + /> + {suggestionListId ? ( + + {suggestionOptions.map((option) => ( + + ))} + + ) : null} + ); }; @@ -539,7 +566,7 @@ export function CtyFldngPrvtnBizFormPage({ id }: { id?: string }) { return ( { 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(null); const defaultDesignationDateRange = useMemo( @@ -302,6 +310,21 @@ export function CtyFldngPrvtnBizListPage() { ); const [data, setData] = useState(EMPTY_PAGE); const [rows, setRows] = useState([]); + 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() { setSearch((current) => ({ @@ -666,6 +698,13 @@ export function CtyFldngPrvtnBizListPage() { })) } /> + + {businessProgressOptions.map((option) => ( + + ))} +
diff --git a/apps/sn/src/features/exmn/lib/useExmnCommonCodes.ts b/apps/sn/src/features/exmn/lib/useExmnCommonCodes.ts new file mode 100644 index 000000000..93d180776 --- /dev/null +++ b/apps/sn/src/features/exmn/lib/useExmnCommonCodes.ts @@ -0,0 +1,137 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; + +import { + getCommonCodes, + type CommonCodeItem, +} from "@/components/lib/common/apiHelpers"; + +type CodeLabelMap = Record; +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(); +const pendingCodeGroups = new Map< + ExmnCodeGroup, + Promise +>(); + +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> + >( + () => + Object.fromEntries(cachedCodeGroups.entries()) as Partial< + Record + >, + ); + + 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, + }; +}; diff --git a/apps/sn/src/features/exmn/ui/ExmnPplUnitDetailTable.tsx b/apps/sn/src/features/exmn/ui/ExmnPplUnitDetailTable.tsx index 04a0f093a..a82853091 100644 --- a/apps/sn/src/features/exmn/ui/ExmnPplUnitDetailTable.tsx +++ b/apps/sn/src/features/exmn/ui/ExmnPplUnitDetailTable.tsx @@ -6,8 +6,9 @@ import { type ExmnDetailSeverity, type ExmnPplUnitDetailRow, } from "../model/exmn-detail-table"; +import { useExmnCommonCodes } from "../lib/useExmnCommonCodes"; -const SEVERITY_LABELS: Record = { +const DEFAULT_SEVERITY_LABELS: Record = { L: "대", M: "중", S: "소", @@ -54,6 +55,13 @@ export function ExmnPplUnitDetailTable({ rows, emptyMessage, }: ExmnPplUnitDetailTableProps) { + // 본단위 행의 기록유형·심각도·관로항목 표시명만 CMS 코드명으로 교체한다. + const { recordTypeLabels, severityLabels: cmsSeverityLabels, pipeItemLabels } = + useExmnCommonCodes(); + const severityLabels: Record = { + ...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)} - {row.defectName} + {row.recordType + ? `(${recordTypeLabels[row.recordType] || row.recordType})${pipeItemLabels[row.code] || row.defectName.replace(/^\([^)]*\)/, "")}` + : row.defectName} {row.code} - {row.severity ? SEVERITY_LABELS[row.severity] : "-"} + {row.severity ? severityLabels[row.severity] : "-"} {formatOptionalValue(row.occurrencePosition)} diff --git a/apps/sn/src/features/exmn/ui/ExmnSurveyDetailPopupPage.tsx b/apps/sn/src/features/exmn/ui/ExmnSurveyDetailPopupPage.tsx index a750d4e6e..6ff255ceb 100644 --- a/apps/sn/src/features/exmn/ui/ExmnSurveyDetailPopupPage.tsx +++ b/apps/sn/src/features/exmn/ui/ExmnSurveyDetailPopupPage.tsx @@ -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 = { +const DEFAULT_SEVERITY_LABELS: Record = { L: "대", M: "중", S: "소", @@ -62,7 +63,7 @@ const PPL_DETAIL_MODE_LABELS: Record = { UNIT: "본단위구간", }; -const DEFECT_TYPE_LABELS: Record = { +const DEFAULT_DEFECT_TYPE_LABELS: Record = { 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(null); - const definition = EXMN_DETAIL_TABLE_DEFINITIONS[jobType]; - const defectTypeLabel = DEFECT_TYPE_LABELS[defectType]; + const severityLabels: Record = { + ...DEFAULT_SEVERITY_LABELS, + ...cmsSeverityLabels, + }; + const defectTypeLabels: Record = { + ...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]} ))} {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]} )), )} diff --git a/apps/sn/src/features/exmn/ui/PplFacilityDetailModal.tsx b/apps/sn/src/features/exmn/ui/PplFacilityDetailModal.tsx index 09811e159..936ae0431 100644 --- a/apps/sn/src/features/exmn/ui/PplFacilityDetailModal.tsx +++ b/apps/sn/src/features/exmn/ui/PplFacilityDetailModal.tsx @@ -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(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 ( diff --git a/apps/sn/src/features/exmn/ui/PplSummaryModal.tsx b/apps/sn/src/features/exmn/ui/PplSummaryModal.tsx index 9d7e5580b..7ab586316 100644 --- a/apps/sn/src/features/exmn/ui/PplSummaryModal.tsx +++ b/apps/sn/src/features/exmn/ui/PplSummaryModal.tsx @@ -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(null); const [summaryRows, setSummaryRows] = useState([]); @@ -491,9 +494,9 @@ export function PplSummaryModal({ onChange={(event) => setStructuralGrade(event.target.value)} > - {[1, 2, 3, 4, 5].map((grade) => ( - ))} @@ -508,9 +511,9 @@ export function PplSummaryModal({ } > - {[1, 2, 3, 4, 5].map((grade) => ( - ))} @@ -518,7 +521,7 @@ export function PplSummaryModal({
- 결함 분류: {defectType} + 결함 분류: {defectTypeLabels[defectType] || defectType}