diff --git a/apps/sn/.env.local b/apps/sn/.env.local index dce96bf40..3d871b007 100644 --- a/apps/sn/.env.local +++ b/apps/sn/.env.local @@ -12,8 +12,8 @@ NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={x}&tilerow={y} SERVER_API_URL = http://dev.onasys.co.kr:8000 -SEWERAGE_API_URL = http://localhost:8883 -# SEWERAGE_API_URL = http://con.mind-one.co.kr:11083 +# SEWERAGE_API_URL = http://localhost:8883 +SEWERAGE_API_URL = http://con.mind-one.co.kr:11083 DC_URL = https://apigateway.apps.rsonesoft.com # NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /auth/login diff --git a/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipeLayerSettingPanel.tsx b/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipeLayerSettingPanel.tsx index 9a6174db1..9dda96f8f 100644 --- a/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipeLayerSettingPanel.tsx +++ b/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipeLayerSettingPanel.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import { useAlertStore } from "@/shared/lib"; import { validateElapsedYears, type AgedPipeFilters } from "./agedPipeUtils"; +import LayerSettingCard from "../layerSetting/LayerSettingCard"; type MaybePromise = T | Promise; @@ -11,6 +12,9 @@ type Props = { onFiltersChange: (filters: AgedPipeFilters) => void; onApply: (filters?: AgedPipeFilters) => MaybePromise; compact?: boolean; + enabled?: boolean; + onEnabledChange?: (enabled: boolean) => void; + onClose?: () => void; }; export default function AgedPipeLayerSettingPanel({ @@ -18,6 +22,9 @@ export default function AgedPipeLayerSettingPanel({ onFiltersChange, onApply, compact = false, + enabled = true, + onEnabledChange = () => {}, + onClose = () => {}, }: Props) { const alert = useAlertStore((state) => state.setMessage); const [isCollapsed, setIsCollapsed] = useState(false); @@ -47,16 +54,16 @@ export default function AgedPipeLayerSettingPanel({ }; const inputClass = - "h-[22px] border border-input bg-background px-[2px] text-center text-[1rem] leading-none text-foreground"; + "h-[20px] !rounded border border-input bg-background px-[1px] text-center text-[1.1rem] leading-none text-foreground"; const numberInputClass = inputClass + " w-[40px]"; const applyButtonClass = - "grid h-[20px] w-[20px] shrink-0 place-items-center rounded-full bg-primary text-[0.95rem] font-bold leading-none text-primary-foreground"; + "grid h-[16px] w-[16px] shrink-0 place-items-center rounded-full bg-primary text-[0.85rem] font-bold leading-none text-primary-foreground"; const indicator = ( ); const settingRow = ( -
+
{indicator}
@@ -96,9 +103,14 @@ export default function AgedPipeLayerSettingPanel({ if (compact) { return ( -
+ {settingRow} -
+ ); } diff --git a/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipePopupPage.tsx b/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipePopupPage.tsx index 5a8caf2d9..b2a12c783 100644 --- a/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipePopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipePopupPage.tsx @@ -57,7 +57,7 @@ export default function AgedPipePopupPage() { const sggOptions = useMemo( () => filters.ctpvCd === "0" - ? sgg + ? [] : sgg.filter((item) => String(item.val).startsWith(filters.ctpvCd.slice(0, 2)), ), diff --git a/apps/sn/app/(map-layout)/map/components/exmn/ExmnLayerSettingPanel.tsx b/apps/sn/app/(map-layout)/map/components/exmn/ExmnLayerSettingPanel.tsx index 8d0907f5a..a8e0a020f 100644 --- a/apps/sn/app/(map-layout)/map/components/exmn/ExmnLayerSettingPanel.tsx +++ b/apps/sn/app/(map-layout)/map/components/exmn/ExmnLayerSettingPanel.tsx @@ -1,11 +1,15 @@ "use client"; import type { ExmnMapFilters, ExmnMapJobType } from "./exmnMapTypes"; +import LayerSettingCard from "../layerSetting/LayerSettingCard"; type Props = { jobType: ExmnMapJobType; filters: ExmnMapFilters; onFiltersChange: (filters: ExmnMapFilters) => void; + enabled?: boolean; + onEnabledChange?: (enabled: boolean) => void; + onClose?: () => void; }; const GRADE_OPTIONS = ["", "1", "2", "3", "4", "5"]; @@ -15,6 +19,9 @@ export default function ExmnLayerSettingPanel({ jobType, filters, onFiltersChange, + enabled = true, + onEnabledChange = () => {}, + onClose = () => {}, }: Props) { const updateFilter = (key: keyof ExmnMapFilters, value: string) => { onFiltersChange({ @@ -23,23 +30,25 @@ export default function ExmnLayerSettingPanel({ }); }; const selectClass = - "h-[24px] w-[54px] rounded-[4px] border border-input bg-background px-2 text-[1rem] text-foreground outline-none focus:border-primary"; - const labelClass = "shrink-0 text-[1.05rem] font-medium text-label"; + "h-[20px] w-[62px] rounded border border-input bg-background px-[1px] text-center text-[1.1rem] leading-none text-foreground outline-none focus:border-primary"; + const labelClass = "shrink-0 text-[1.15rem] font-medium text-label"; + const title = jobType === "PPL" ? "관로조사" : "맨홀조사"; return ( -
-
- {/* 관로/맨홀 조사유형을 구분하는 지도 범례 의미색이므로 고정색을 유지한다. */} - - - {jobType === "PPL" ? "관로조사" : "맨홀조사"} - -
-
-
+ ); } diff --git a/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryLayerSettingPanel.tsx b/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryLayerSettingPanel.tsx new file mode 100644 index 000000000..0cf554a46 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryLayerSettingPanel.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { SelectBox } from "@common/business_components/ui"; +import { + FLOOD_HISTORY_MIN_YEAR, + normalizeFloodHistoryYearRange, + type FloodHistoryYearRange, +} from "./fldngHstryTypes"; + +type Props = { + yearRange: FloodHistoryYearRange; + enabled: boolean; + onYearRangeChange: (yearRange: FloodHistoryYearRange) => void; + onEnabledChange: (enabled: boolean) => void; + onClose: () => void; +}; + +/** 침수흔적위선·심선에 공통 적용되는 즉시 반영 침수년도 설정 카드다. */ +export default function FldngHstryLayerSettingPanel({ + yearRange, + enabled, + onYearRangeChange, + onEnabledChange, + onClose, +}: Props) { + const normalizedRange = normalizeFloodHistoryYearRange(yearRange); + const currentYear = new Date().getFullYear(); + const yearOptions = Array.from( + { length: currentYear - FLOOD_HISTORY_MIN_YEAR + 1 }, + (_, index) => String(currentYear - index), + ); + const startYearOptions = yearOptions + .filter((year) => Number(year) <= Number(normalizedRange.endYear)) + .map((year) => ({ val: year, text: year })); + const endYearOptions = yearOptions + .filter((year) => Number(year) >= Number(normalizedRange.startYear)) + .map((year) => ({ val: year, text: year })); + const yearSelectClass = + "h-[20px] w-[62px] !rounded border border-input bg-background px-[1px] text-center text-[1.1rem] leading-none text-foreground"; + + const changeYear = (key: keyof FloodHistoryYearRange, value: string) => { + onYearRangeChange( + normalizeFloodHistoryYearRange({ + ...normalizedRange, + [key]: value, + }), + ); + }; + + return ( +
+
+ + 침수흔적도 + +
+ + +
+
+
+ + 침수년도 +
+
+ changeYear("startYear", String(value))} + selectContentClass="z-[101]" + labelClass="hidden" + /> +
+ + ~ + +
+ changeYear("endYear", String(value))} + selectContentClass="z-[101]" + labelClass="hidden" + /> +
+
+
+
+ ); +} diff --git a/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx index 9191a4be5..42750f87d 100644 --- a/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx @@ -126,7 +126,7 @@ export default function FldngHstryPopupPage() { const sggOptions = useMemo( () => filters.ctpvCd === "0" - ? sgg + ? [] : sgg.filter((item) => String(item.val).startsWith(filters.ctpvCd.slice(0, 2)), ), diff --git a/apps/sn/app/(map-layout)/map/components/fldngHstry/fldngHstryTypes.ts b/apps/sn/app/(map-layout)/map/components/fldngHstry/fldngHstryTypes.ts index 28f176144..3d5649b7f 100644 --- a/apps/sn/app/(map-layout)/map/components/fldngHstry/fldngHstryTypes.ts +++ b/apps/sn/app/(map-layout)/map/components/fldngHstry/fldngHstryTypes.ts @@ -1,17 +1,62 @@ "use client"; import type { UiHeadProps } from "@common/business_components/ui"; +import { appendApiSearchParams } from "@/shared/util/util"; export const FLOOD_HISTORY_POPUP_KEY = "fldngHstryPopup"; export const FLDNG_HSTRY_ROW_SELECT_MESSAGE = "SN_FLOOD_HISTORY_ROW_SELECT"; export const FLOOD_HISTORY_LAYER_TYPE = "tb_ms_fldng_wtlv_map"; +export const FLOOD_HISTORY_DEPTH_LAYER_TYPE = "tb_ms_fldng_dpth_map"; export const FLOOD_HISTORY_PREVIEW_LAYER_TYPE = "tb_sn_flood_trace_mock"; export const FLOOD_HISTORY_LAYER_NAME = "침수흔적위선"; +export const FLOOD_HISTORY_DEPTH_LAYER_NAME = "침수흔적심선"; export const FLOOD_HISTORY_ROOT_NAME = "침수흔적도"; export const FLOOD_HISTORY_AREA_LAYER_ID = "SN_FLOOD_HISTORY_AREA"; export const FLOOD_HISTORY_DEPTH_LAYER_ID = "SN_FLOOD_HISTORY_DEPTH"; export const FLDNG_HSTRY_PAGE_SIZE = 10; export const FLDNG_HSTRY_DOWNLOAD_BATCH_SIZE = 2000; +export const FLOOD_HISTORY_MIN_YEAR = 2002; + +/** 지도 침수흔적위선·심선이 공통으로 사용하는 침수년도 범위다. */ +export type FloodHistoryYearRange = { + startYear: string; + endYear: string; +}; + +/** 침수년도 설정의 초기 범위는 데이터 최소연도부터 현재년도까지다. */ +export const buildFloodHistoryDefaultYearRange = ( + currentYear = new Date().getFullYear(), +): FloodHistoryYearRange => ({ + startYear: String(FLOOD_HISTORY_MIN_YEAR), + endYear: String(currentYear), +}); + +/** 연도 설정값을 4자리 숫자로 정규화하고 유효하지 않으면 기본 범위를 사용한다. */ +export const normalizeFloodHistoryYearRange = ( + range?: Partial | null, + fallback: FloodHistoryYearRange = buildFloodHistoryDefaultYearRange(), +): FloodHistoryYearRange => { + const normalizeYear = (value: unknown) => + String(value ?? "") + .replace(/[^0-9]/g, "") + .slice(0, 4); + const startYear = normalizeYear(range?.startYear); + const endYear = normalizeYear(range?.endYear); + return { + startYear: /^\d{4}$/.test(startYear) ? startYear : fallback.startYear, + endYear: /^\d{4}$/.test(endYear) ? endYear : fallback.endYear, + }; +}; + +/** 위선·심선 WFS에 동일하게 결합할 침수년도 CQL 조건을 생성한다. */ +export const buildFloodHistoryYearCql = ( + range: FloodHistoryYearRange, + enabled: boolean, +) => { + if (!enabled) return ""; + const normalized = normalizeFloodHistoryYearRange(range); + return `(fldng_yr >= '${normalized.startYear}' AND fldng_yr <= '${normalized.endYear}')`; +}; export type FldngHstryFilters = { ctpvCd: string; @@ -375,15 +420,14 @@ export const buildFldngHstryListQueryString = ( size: String(size), }); - if (normalized.ctpvCd !== "0") - params.set("ctpvCd", normalized.ctpvCd.slice(0, 2)); - if (normalized.sggCd !== "0") - params.set("sggCd", normalized.sggCd.slice(0, 5)); - if (normalized.floodYear) params.set("fldngYr", normalized.floodYear); - if (normalized.disasterName) - params.set("fldngDstrNm", normalized.disasterName); - if (normalized.rainStartDate) params.set("rainBgngTm", normalized.rainStartDate); - if (normalized.districtName) params.set("fldngDstrctNm", normalized.districtName); + appendApiSearchParams(params, { + ctpvCd: normalized.ctpvCd === "0" ? "0" : normalized.ctpvCd.slice(0, 2), + sggCd: normalized.sggCd === "0" ? "0" : normalized.sggCd.slice(0, 5), + fldngYr: normalized.floodYear, + fldngDstrNm: normalized.disasterName, + rainBgngTm: normalized.rainStartDate, + fldngDstrctNm: normalized.districtName, + }); return params.toString(); }; @@ -494,11 +538,32 @@ export const isFldngHstryNode = (node: any) => { const layerType = resolveFldngHstryLayerType(node); return ( name === FLOOD_HISTORY_LAYER_NAME || + name === FLOOD_HISTORY_DEPTH_LAYER_NAME || layerType === FLOOD_HISTORY_LAYER_TYPE || + layerType === FLOOD_HISTORY_DEPTH_LAYER_TYPE || layerType === FLOOD_HISTORY_PREVIEW_LAYER_TYPE ); }; +/** 침수흔적위선 체크 상태를 심선과 분리해 판별한다. */ +export const isFloodHistoryWaterLevelNode = (node: any) => { + const name = resolveFldngHstryNodeName(node); + const layerType = resolveFldngHstryLayerType(node); + return ( + name === FLOOD_HISTORY_LAYER_NAME || layerType === FLOOD_HISTORY_LAYER_TYPE + ); +}; + +/** 침수흔적심선 체크 상태를 위선과 분리해 판별한다. */ +export const isFloodHistoryDepthNode = (node: any) => { + const name = resolveFldngHstryNodeName(node); + const layerType = resolveFldngHstryLayerType(node); + return ( + name === FLOOD_HISTORY_DEPTH_LAYER_NAME || + layerType === FLOOD_HISTORY_DEPTH_LAYER_TYPE + ); +}; + export const isFldngHstryRootNode = (node: any) => resolveFldngHstryNodeName(node) === FLOOD_HISTORY_ROOT_NAME; diff --git a/apps/sn/app/(map-layout)/map/components/floodDngrMap/FloodDngrMapPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/floodDngrMap/FloodDngrMapPopupPage.tsx index b65c7b38f..62821229f 100644 --- a/apps/sn/app/(map-layout)/map/components/floodDngrMap/FloodDngrMapPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/floodDngrMap/FloodDngrMapPopupPage.tsx @@ -101,7 +101,7 @@ export default function FloodDngrMapPopupPage() { availableSggCodes.has(String(item.val).trim().slice(0, 5)), ); return filters.ctpvCd === "0" - ? scopedOptions + ? [] : scopedOptions.filter((item) => String(item.val).startsWith(filters.ctpvCd.slice(0, 2)), ); diff --git a/apps/sn/app/(map-layout)/map/components/floodDngrMap/floodDngrMapTypes.ts b/apps/sn/app/(map-layout)/map/components/floodDngrMap/floodDngrMapTypes.ts index ab03f132d..1d4d2df65 100644 --- a/apps/sn/app/(map-layout)/map/components/floodDngrMap/floodDngrMapTypes.ts +++ b/apps/sn/app/(map-layout)/map/components/floodDngrMap/floodDngrMapTypes.ts @@ -1,6 +1,7 @@ "use client"; import type { UiHeadProps } from "@common/business_components/ui"; +import { appendApiSearchParams } from "@/shared/util/util"; export const FLOOD_DNGR_MAP_POPUP_KEY = "floodDngrMapPopup"; export const FLOOD_DNGR_MAP_LAYER_TYPE = "tb_ms_flood_dngr_map"; @@ -89,16 +90,12 @@ export const buildFloodDngrMapListQueryString = ( page: String(page), size: String(size), }); - if (filters.ctpvCd && filters.ctpvCd !== "0") - params.set("ctpvCd", filters.ctpvCd.slice(0, 2)); - if (filters.sggCd && filters.sggCd !== "0") - params.set("sggCd", filters.sggCd.slice(0, 5)); - if (filters.sbmrgDpthlCd && filters.sbmrgDpthlCd !== "0") { - params.set("sbmrgDpthlCd", filters.sbmrgDpthlCd); - } - if (filters.sbmrgFrqExpln && filters.sbmrgFrqExpln !== "0") { - params.set("sbmrgFrqExpln", filters.sbmrgFrqExpln); - } + appendApiSearchParams(params, { + ctpvCd: filters.ctpvCd === "0" ? "0" : filters.ctpvCd.slice(0, 2), + sggCd: filters.sggCd === "0" ? "0" : filters.sggCd.slice(0, 5), + sbmrgDpthlCd: filters.sbmrgDpthlCd, + sbmrgFrqExpln: filters.sbmrgFrqExpln, + }); return params.toString(); }; 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() {