From 3cdcf6a6f706c9bfb4b1223116837e2615580fc7 Mon Sep 17 00:00:00 2001 From: ljpark Date: Mon, 31 Aug 2026 23:16:57 +0900 Subject: [PATCH 01/12] =?UTF-8?q?sn:=20=EC=B9=A8=EC=88=98=ED=9D=94?= =?UTF-8?q?=EC=A0=81=EC=9C=84=EC=84=A0=20=EC=8B=AC=EC=84=A0/=EC=9C=84?= =?UTF-8?q?=EC=84=A0=20=EB=AA=A8=EB=91=90=20=EB=82=98=EC=98=A4=EB=8F=84?= =?UTF-8?q?=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../popupComponent/featureClickEventPopup.tsx | 257 ++++++++++++------ apps/sn/app/(map-layout)/map/page.tsx | 24 ++ apps/sn/app/components/ui/verticalTable.tsx | 4 +- 3 files changed, 199 insertions(+), 86 deletions(-) diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx index aa0320652..96ff8edb2 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx @@ -186,10 +186,17 @@ const FeatureClickEventPopup = ({ const [floodDepthRows, setFloodDepthRows] = useState< Record[] >([]); - const [selectedFloodDepthYear, setSelectedFloodDepthYear] = useState< + const [floodWtlvRows, setFloodWtlvRows] = useState[]>( + [], + ); + const [selectedFloodDepthFid, setSelectedFloodDepthFid] = useState< + string | null + >(null); + const [selectedFloodWtlvFid, setSelectedFloodWtlvFid] = useState< string | null >(null); const floodDepthYearListRef = useRef(null); + const floodWtlvListRef = useRef(null); const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore(); // ๐ŸŽฏ ๊ด€๋กœ๊ด€๋ฆฌ๋ฒˆํ˜ธ ์ถ”์ถœ @@ -850,7 +857,9 @@ const FeatureClickEventPopup = ({ if (selectedLayerName === "์นจ์ˆ˜ํ”์ ์œ„์„ " && featureId) { const depthLookupKey = normalizeFldngWtlvMapApiFid(featureId); setFloodDepthRows([]); - setSelectedFloodDepthYear(null); + setFloodWtlvRows([]); + setSelectedFloodDepthFid(null); + setSelectedFloodWtlvFid(null); try { const normalizedFloodMapFid = normalizeFldngWtlvMapApiFid(featureId); @@ -877,18 +886,25 @@ const FeatureClickEventPopup = ({ `v1/fldng/hstry/depth-map/${encodeURIComponent(depthLookupKey)}`, ); const depthPayload = depthRows?.data ?? depthRows; - const normalizedDepthRows = Array.isArray(depthPayload) - ? depthPayload.filter((row) => row && typeof row === "object") + const depthItems = Array.isArray(depthPayload) + ? depthPayload + : depthPayload?.depthMaps; + const waterLevelItems = Array.isArray( + depthPayload?.waterLevelMaps, + ) + ? depthPayload.waterLevelMaps : []; + const normalizedDepthRows = Array.isArray(depthItems) + ? depthItems.filter((row) => row && typeof row === "object") + : []; + const normalizedWaterLevelRows = waterLevelItems.filter( + (row: unknown) => row && typeof row === "object", + ); setFloodDepthRows(normalizedDepthRows); - const years = normalizedDepthRows - .map((row) => - String(row.fldng_yr ?? row.fldngYr ?? row.yr ?? "").trim(), - ) - .filter(Boolean); - console.log("depth years:", years); - // ์ตœ์ดˆ์—๋Š” ์—ฐ๋„๋ฅผ ์„ ํƒํ•˜์ง€ ์•Š๊ณ  ๋ชฉ๋ก๋งŒ ํ‘œ์‹œํ•œ๋‹ค. - setSelectedFloodDepthYear(null); + setFloodWtlvRows(normalizedWaterLevelRows); + // ์ตœ์ดˆ์—๋Š” ์—ฐ๊ณ„ ํ•ญ๋ชฉ์„ ์„ ํƒํ•˜์ง€ ์•Š๊ณ  ๋ฒ„ํŠผ ๋ชฉ๋ก๋งŒ ํ‘œ์‹œํ•œ๋‹ค. + setSelectedFloodDepthFid(null); + setSelectedFloodWtlvFid(null); } } catch (depthError) { console.warn("์นจ์ˆ˜๊นŠ์ด์ง€๋„ ์กฐํšŒ ์‹คํŒจ:", depthError); @@ -1097,82 +1113,153 @@ const FeatureClickEventPopup = ({ > 0 ? ( -
+ layerNm === "์นจ์ˆ˜ํ”์ ์œ„์„ " && + (floodDepthRows.length > 0 || floodWtlvRows.length > 0) ? ( +
๊ธฐ๋ณธ์ •๋ณด -
- -
- {Array.from( - new Set( - floodDepthRows - .map((row) => - String( - row.fldng_yr ?? row.fldngYr ?? row.yr ?? "", - ).trim(), - ) - .filter(Boolean), - ), - ).map((year) => ( - +
+ {[...floodDepthRows] + .map((row, index) => ({ + row, + index, + year: String( + row.fldng_yr ?? row.fldngYr ?? row.yr ?? "", + ).trim(), + })) + .filter(({ year }) => Boolean(year)) + .sort((left, right) => { + const yearOrder = left.year.localeCompare( + right.year, + undefined, + { numeric: true }, ); - const depthFid = String( - selectedDepthRow?.fldng_dpth_map_sn ?? - selectedDepthRow?.fid ?? - "", - ).trim(); - if (depthFid) { - const core = GisApp.Module.layer as any; - await core.showSearchResultsFromGeoJSON?.( - "tb_ms_fldng_dpth_map", - `tb_ms_fldng_dpth_map.${depthFid.replace(/^tb_ms_fldng_dpth_map\./, "")}`, - ); - } - }} - className={`rounded border px-3 py-1 text-[1.2rem] font-medium leading-5 ${selectedFloodDepthYear === year ? "border-blue-600 bg-white text-blue-700" : "border-border bg-muted text-muted-foreground"}`} - > - {year}๋…„ - - ))} + return yearOrder !== 0 + ? yearOrder + : left.index - right.index; + }) + .map(({ row, index, year }) => ( + + ))} +
+
- -
+ )} + {floodWtlvRows.length > 0 && ( +
+ + ์œ„์„  + + +
+ {floodWtlvRows.map((row, index) => { + const waterLevelFid = String(row.fid ?? "").trim(); + const disasterName = String( + row.fldng_dstr_nm ?? row.fldngDstrNm ?? "", + ).trim(); + return ( + + ); + })} +
+ +
+ )}
) : layerNm === "ํ•˜์ˆ˜๊ด€๋กœ" && tableData?.elapsedYears ? ( <> diff --git a/apps/sn/app/(map-layout)/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx index f56934c44..bdda554da 100644 --- a/apps/sn/app/(map-layout)/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -5208,6 +5208,30 @@ const Map = () => { return; } + // ์—ฐ๊ณ„ ์‹ฌ์„ /์œ„์„  ์กฐํšŒ๋Š” ๊ณต์šฉ search-layer๋ฅผ ๊ต์ฒดํ•œ๋‹ค. ์ด๋•Œ ํƒญ์— ๋ณด๊ด€๋œ + // ๊ธฐ์กด ์นจ์ˆ˜ํ”์  feature๋Š” ๋” ์ด์ƒ ์ง€๋„ ๋ ˆ์ด์–ด์— ์†ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ, ๋‹จ์ˆœํžˆ + // Select ์ปฌ๋ ‰์…˜์— ๋‹ค์‹œ ๋„ฃ์–ด๋„ ๊ฐ•์กฐ๊ฐ€ ๋ Œ๋”๋ง๋˜์ง€ ์•Š๋Š”๋‹ค. ํƒญ์œผ๋กœ ๋Œ์•„์˜ฌ + // ๋•Œ๋Š” FID๋กœ search-layer๋ฅผ ๋ณต์›ํ•œ ๋’ค ๋กœ๋“œ๋œ feature๋ฅผ ์„ ํƒํ•˜๊ฒŒ ํ•œ๋‹ค. + if ( + layerTypeName === FLOOD_HISTORY_LAYER_TYPE || + layerTypeName === "tb_ms_fldng_dpth_map" + ) { + clearSinkholeFocusedPipeLayer(); + const featureIdentifier = getRightPanelFeatureIdentifier(data).replace( + new RegExp(`^${layerTypeName}\\.`), + "", + ); + if (featureIdentifier) { + const core = GisApp.Module.layer as any; + void core?.showSearchResultsFromGeoJSON?.( + layerTypeName, + `${layerTypeName}.${featureIdentifier}`, + ); + } + + return; + } + if (!data || typeof data.getGeometry !== "function") { clearSinkholeFocusedPipeLayer(); diff --git a/apps/sn/app/components/ui/verticalTable.tsx b/apps/sn/app/components/ui/verticalTable.tsx index 3a012de61..f2685d98c 100644 --- a/apps/sn/app/components/ui/verticalTable.tsx +++ b/apps/sn/app/components/ui/verticalTable.tsx @@ -165,7 +165,9 @@ export function VerticalTable({ <> {headName ? (
-

{headName}

+

+ {headName} +

{showSaveButton && onSaveClick && ( + +
+
+
+ + ์นจ์ˆ˜๋…„๋„ +
+
+ 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/fldngHstryTypes.ts b/apps/sn/app/(map-layout)/map/components/fldngHstry/fldngHstryTypes.ts index 28f176144..bca73fc0e 100644 --- a/apps/sn/app/(map-layout)/map/components/fldngHstry/fldngHstryTypes.ts +++ b/apps/sn/app/(map-layout)/map/components/fldngHstry/fldngHstryTypes.ts @@ -5,13 +5,57 @@ import type { UiHeadProps } from "@common/business_components/ui"; 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; @@ -382,8 +426,10 @@ export const buildFldngHstryListQueryString = ( 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); + if (normalized.rainStartDate) + params.set("rainBgngTm", normalized.rainStartDate); + if (normalized.districtName) + params.set("fldngDstrctNm", normalized.districtName); return params.toString(); }; @@ -494,11 +540,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/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx index 48e75e968..9b6037bdf 100644 --- a/apps/sn/app/(map-layout)/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -102,6 +102,7 @@ import { useRdiiResultMap } from "./components/rdiiResult/useRdiiResultMap"; import SinkholeDetailPanel from "./components/sinkhole/SinkholeDetailPanel"; import SinkholeLayerSettingPanel from "./components/sinkhole/SinkholeLayerSettingPanel"; import FldngHstryDetailPanel from "./components/fldngHstry/FldngHstryDetailPanel"; +import FldngHstryLayerSettingPanel from "./components/fldngHstry/FldngHstryLayerSettingPanel"; import ExmnLayerSettingPanel from "./components/exmn/ExmnLayerSettingPanel"; import { getExmnMapLayer } from "./components/exmn/exmnMapApi"; import { @@ -143,14 +144,20 @@ import { } from "./components/sinkhole/sinkholeTypes"; import { FLOOD_HISTORY_AREA_LAYER_ID, + FLOOD_HISTORY_DEPTH_LAYER_NAME, + FLOOD_HISTORY_DEPTH_LAYER_TYPE, FLOOD_HISTORY_DEPTH_LAYER_ID, FLOOD_HISTORY_PREVIEW_LAYER_TYPE, FLOOD_HISTORY_LAYER_TYPE, FLOOD_HISTORY_LAYER_NAME, FLOOD_HISTORY_POPUP_KEY, FLDNG_HSTRY_ROW_SELECT_MESSAGE, - isFldngHstryNode, + buildFloodHistoryDefaultYearRange, + buildFloodHistoryYearCql, + isFloodHistoryDepthNode, + isFloodHistoryWaterLevelNode, isFldngHstryRootNode, + type FloodHistoryYearRange, type FldngHstryItem, } from "./components/fldngHstry/fldngHstryTypes"; import { @@ -186,6 +193,8 @@ const AGED_PIPE_HIGHLIGHT_LAYER_ID = "SN_AGED_PIPE_HIGHLIGHT"; // ๋…ธํ›„๊ด€๋กœ ์ง€๋„ ๊ฐ•์กฐ์„ ๊ณผ ์‹œ์„ค๋ฌผ ํŠธ๋ฆฌ์˜ ์„ ํ˜• ๋ฒ”๋ก€๊ฐ€ ๋™์ผํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๋Š” ๋นจ๊ฐ„์ƒ‰์ด๋‹ค. const AGED_PIPE_HIGHLIGHT_COLOR = "rgba(235, 40, 40, 0.82)"; const FLDNG_HSTRY_WFS_LAYER_ID = "SN_FLDNG_HSTRY_WFS"; +// ์‹ฌ์„ ์€ ๋™์ผ ๋ ˆ์ด์–ด ์œ ํ˜•์— 6๊ฐœ ๊นŠ์ด ๋“ฑ๊ธ‰ ์„ค์ •์ด ์žˆ์–ด prefix ๋’ค์— lyrSn์„ ๋ถ™์—ฌ ๊ฐ๊ฐ ์ƒ์„ฑํ•œ๋‹ค. +const FLDNG_DEPTH_WFS_LAYER_ID_PREFIX = "SN_FLDNG_DEPTH_WFS"; const AGED_PIPE_ROW_SELECT_MESSAGE = "SN_AGED_PIPE_ROW_SELECT"; const AGED_PIPE_POPUP_MINIMIZE_MESSAGE = "SN_AGED_PIPE_POPUP_MINIMIZE"; const AGED_PIPE_FILTER_SYNC_MESSAGE = "SN_AGED_PIPE_FILTER_SYNC"; @@ -3005,6 +3014,24 @@ const Map = () => { const [isAgedPipeChecked, setIsAgedPipeChecked] = useState(false); const [isSinkholeChecked, setIsSinkholeChecked] = useState(false); const [isFldngHstryChecked, setIsFldngHstryChecked] = useState(false); + // ์นจ์ˆ˜ํ”์ ์œ„์„ ๊ณผ ์‹ฌ์„ ์€ ํ•˜๋‚˜์˜ ์—ฐ๋„ ๋ฒ”์œ„๋ฅผ ๊ณต์œ ํ•˜์ง€๋งŒ ์ฒดํฌ ์ƒํƒœ๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ๊ด€๋ฆฌํ•œ๋‹ค. + const [isFloodWtlvChecked, setIsFloodWtlvChecked] = useState(false); + const [isFloodDepthChecked, setIsFloodDepthChecked] = useState(false); + // ์‹ฌ์„ ์€ 6๊ฐœ ๊นŠ์ด ๋“ฑ๊ธ‰ ์ค‘ ์‚ฌ์šฉ์ž๊ฐ€ ์‹ค์ œ ์ฒดํฌํ•œ lyrSn๋งŒ ํ•„ํ„ฐ ๋ ˆ์ด์–ด๋กœ ์ƒ์„ฑํ•œ๋‹ค. + const [checkedFloodDepthLayerKeys, setCheckedFloodDepthLayerKeys] = useState< + string[] + >([]); + const [floodYearRange, setFloodYearRange] = useState( + () => buildFloodHistoryDefaultYearRange(), + ); + const [isFloodYearFilterEnabled, setIsFloodYearFilterEnabled] = + useState(true); + const [isFloodYearSettingVisible, setIsFloodYearSettingVisible] = + useState(true); + // ๋‹ซํžŒ ์„ค์ • ์นด๋“œ๋Š” ๋ ˆ์ด์–ด๊ฐ€ ์™„์ „ํžˆ ํ•ด์ œ๋œ ๋’ค ๋‹ค์‹œ ์ฒดํฌ๋  ๋•Œ๋งŒ ์ž๋™์œผ๋กœ ์—ฐ๋‹ค. + const floodLayerCheckedRef = useRef(false); + // ์ฒดํฌ ๋“ฑ๊ธ‰ ๋ณ€๊ฒฝ๊ณผ ์—ฐ๋„ ํ•„ํ„ฐ ๋ณ€๊ฒฝ์„ ๊ตฌ๋ถ„ํ•ด ์—ฐ๋„ ๋ณ€๊ฒฝ ๋•Œ๋งŒ ๊ธฐ์กด ์„ ํƒ ์ƒ์„ธ๋ฅผ ์ •๋ฆฌํ•œ๋‹ค. + const floodYearFilterSignatureRef = useRef(""); const [isExmnPplChecked, setIsExmnPplChecked] = useState(false); const [isExmnMnhlChecked, setIsExmnMnhlChecked] = useState(false); const [exmnPplFilters, setExmnPplFilters] = useState(() => ({ @@ -3463,7 +3490,10 @@ const Map = () => { selectedWaterRiskMapView?.riskStatus ?? selectedWaterRiskResultRow?.riskStatus ?? "NORMAL", - [selectedWaterRiskMapView?.riskStatus, selectedWaterRiskResultRow?.riskStatus], + [ + selectedWaterRiskMapView?.riskStatus, + selectedWaterRiskResultRow?.riskStatus, + ], ); /** ์„ ํƒ ์˜์—ญ ์˜ค๋ฅธ์ชฝ์— ์ค‘์ ๊ด€๋ฆฌ์ง€์—ญ ๊ฐ•์šฐยท์นจ์ˆ˜ ์š”์•ฝ์นด๋“œ๋ฅผ ๋ฐฐ์น˜ํ•œ๋‹ค. */ @@ -3521,7 +3551,8 @@ const Map = () => { const loadFloodPredictionGridLayer = useCallback( async (sggCd: string, analysisDateTime: string, extent: number[]) => { const map = gisMaps.current; - if (!map || !/^\d{5}$/.test(sggCd) || !/^\d{12}$/.test(analysisDateTime)) return; + if (!map || !/^\d{5}$/.test(sggCd) || !/^\d{12}$/.test(analysisDateTime)) + return; removeFloodPredictionGridLayer(); try { const response = await getRawJson( @@ -3529,29 +3560,74 @@ const Map = () => { ); const payload = response?.data ?? response; const row = payload && !Array.isArray(payload) ? payload : null; - if (!row || extent.length !== 4 || extent.some((value) => !Number.isFinite(value))) return; + if ( + !row || + extent.length !== 4 || + extent.some((value) => !Number.isFinite(value)) + ) + return; const center = [ (extent[0]! + extent[2]!) / 2, (extent[1]! + extent[3]!) / 2, ]; - const observed = Number(row.observedAccumulatedRainfall ?? 0).toFixed(1); - const predicted = Number(row.predictedAccumulatedRainfall ?? 0).toFixed(1); + const observed = Number(row.observedAccumulatedRainfall ?? 0).toFixed( + 1, + ); + const predicted = Number(row.predictedAccumulatedRainfall ?? 0).toFixed( + 1, + ); const container = document.createElement("div"); - container.style.cssText = "position:relative; background:rgba(255,255,255,.55); color:#111827; border:1px solid rgba(17,24,39,.7); box-shadow:0 2px 8px rgba(0,0,0,.2); padding:8px 10px; font:400 11px/1.35 sans-serif; min-width:230px;"; + container.style.cssText = + "position:relative; background:rgba(255,255,255,.55); color:#111827; border:1px solid rgba(17,24,39,.7); box-shadow:0 2px 8px rgba(0,0,0,.2); padding:8px 10px; font:400 11px/1.35 sans-serif; min-width:230px;"; const close = document.createElement("button"); close.type = "button"; close.textContent = "ร—"; close.title = "์นจ์ˆ˜์˜ˆ์ธก ์˜ค๋ฒ„๋ ˆ์ด ๋‹ซ๊ธฐ"; - close.style.cssText = "position:absolute; right:3px; top:1px; border:0; background:transparent; color:#111827; font-size:16px; line-height:16px; cursor:pointer;"; + close.style.cssText = + "position:absolute; right:3px; top:1px; border:0; background:transparent; color:#111827; font-size:16px; line-height:16px; cursor:pointer;"; close.addEventListener("click", () => removeFloodPredictionGridLayer()); const table = document.createElement("table"); table.style.cssText = "border-collapse:collapse; width:100%;"; const title = table.createTHead().insertRow(); - const titleCell = title.insertCell(); titleCell.colSpan = 2; titleCell.textContent = `${row.ctpvNm ?? row.ctpvCd ?? "-"} ${row.sggNm ?? row.sggCd ?? "-"} ์นจ์ˆ˜์˜ˆ์ธก`; titleCell.style.cssText = "font-weight:500; text-align:center; padding:2px 16px 6px 2px; border-bottom:1px solid #9ca3af;"; + const titleCell = title.insertCell(); + titleCell.colSpan = 2; + titleCell.textContent = `${row.ctpvNm ?? row.ctpvCd ?? "-"} ${row.sggNm ?? row.sggCd ?? "-"} ์นจ์ˆ˜์˜ˆ์ธก`; + titleCell.style.cssText = + "font-weight:500; text-align:center; padding:2px 16px 6px 2px; border-bottom:1px solid #9ca3af;"; const body = table.createTBody(); - [["๋ถ„์„์‹œ๊ฐ", formatFloodPredictionDateTime(row.analysisDateTime ?? analysisDateTime)], ["๊ฐ•์šฐ์‹œ์ž‘์‹œ๊ฐ", formatFloodPredictionDateTime(row.rainBeginningDateTime)], ["๊ด€์ธก ๋ˆ„์ ๊ฐ•์ˆ˜๋Ÿ‰(mm)", observed], ["์˜ˆ์ธก์‹œ๊ฐ", formatFloodPredictionDateTime(row.predictionDateTime)], ["์˜ˆ์ธก ๋ˆ„์ ๊ฐ•์ˆ˜๋Ÿ‰(mm)", predicted], ["์ดˆ๊ณผ ๋นˆ๋„", `${row.exceededFrequencyYear ?? "-"}๋…„`], ["์นจ์ˆ˜์œ„ํ—˜๋„", row.floodRiskGrade ?? "-"]].forEach(([label, value]) => { const tr = body.insertRow(); const key = tr.insertCell(); key.textContent = label; key.style.cssText = "font-weight:400; padding:2px 8px 2px 2px; white-space:nowrap; border-bottom:1px solid #e5e7eb;"; const val = tr.insertCell(); val.textContent = value; val.style.cssText = "font-weight:400; text-align:right; padding:2px 2px; white-space:nowrap; border-bottom:1px solid #e5e7eb;"; }); + [ + [ + "๋ถ„์„์‹œ๊ฐ", + formatFloodPredictionDateTime( + row.analysisDateTime ?? analysisDateTime, + ), + ], + [ + "๊ฐ•์šฐ์‹œ์ž‘์‹œ๊ฐ", + formatFloodPredictionDateTime(row.rainBeginningDateTime), + ], + ["๊ด€์ธก ๋ˆ„์ ๊ฐ•์ˆ˜๋Ÿ‰(mm)", observed], + ["์˜ˆ์ธก์‹œ๊ฐ", formatFloodPredictionDateTime(row.predictionDateTime)], + ["์˜ˆ์ธก ๋ˆ„์ ๊ฐ•์ˆ˜๋Ÿ‰(mm)", predicted], + ["์ดˆ๊ณผ ๋นˆ๋„", `${row.exceededFrequencyYear ?? "-"}๋…„`], + ["์นจ์ˆ˜์œ„ํ—˜๋„", row.floodRiskGrade ?? "-"], + ].forEach(([label, value]) => { + const tr = body.insertRow(); + const key = tr.insertCell(); + key.textContent = label; + key.style.cssText = + "font-weight:400; padding:2px 8px 2px 2px; white-space:nowrap; border-bottom:1px solid #e5e7eb;"; + const val = tr.insertCell(); + val.textContent = value; + val.style.cssText = + "font-weight:400; text-align:right; padding:2px 2px; white-space:nowrap; border-bottom:1px solid #e5e7eb;"; + }); container.append(close, table); - const overlay = new Overlay({ element: container, positioning: "center-center", stopEvent: true }); + const overlay = new Overlay({ + element: container, + positioning: "center-center", + stopEvent: true, + }); overlay.set("id", FLOOD_PREDICTION_GRID_LAYER_ID); overlay.setPosition(center); map.addOverlay(overlay); @@ -3682,7 +3758,9 @@ const Map = () => { : []; const sggCd = String(data.payload?.sggCd ?? "").trim(); const frequency = String(data.payload?.sbmrgFrqExpln ?? "").trim(); - const analysisDateTime = String(data.payload?.analysisDateTime ?? "").trim(); + const analysisDateTime = String( + data.payload?.analysisDateTime ?? "", + ).trim(); const map = gisMaps.current; const select = GisApp.Module.select as any; if ( @@ -6147,18 +6225,24 @@ const Map = () => { try { setIsAgedPipeListLoading(true); setAgedPipeDownloadProgressText("Excel ํŒŒ์ผ์„ ์ค€๋น„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค."); - await downloadAgedPipeExcel(appliedAgedPipeFilters, (job) => { - if (job.status === "QUEUED") { - setAgedPipeDownloadProgressText("Excel ๋‹ค์šด๋กœ๋“œ ์ž‘์—…์ด ๋Œ€๊ธฐ ์ค‘์ž…๋‹ˆ๋‹ค."); - return; - } + await downloadAgedPipeExcel( + appliedAgedPipeFilters, + (job) => { + if (job.status === "QUEUED") { + setAgedPipeDownloadProgressText( + "Excel ๋‹ค์šด๋กœ๋“œ ์ž‘์—…์ด ๋Œ€๊ธฐ ์ค‘์ž…๋‹ˆ๋‹ค.", + ); + return; + } - if (job.status === "RUNNING") { - setAgedPipeDownloadProgressText( - `Excel ํŒŒ์ผ ์ƒ์„ฑ ์ค‘์ž…๋‹ˆ๋‹ค. (${job.progressPercent ?? 0}%)`, - ); - } - }, agedPipeTotalCount); + if (job.status === "RUNNING") { + setAgedPipeDownloadProgressText( + `Excel ํŒŒ์ผ ์ƒ์„ฑ ์ค‘์ž…๋‹ˆ๋‹ค. (${job.progressPercent ?? 0}%)`, + ); + } + }, + agedPipeTotalCount, + ); setAgedPipeDownloadProgressText(""); } catch (error) { console.error("๋…ธํ›„๊ด€๋กœ ๋ชฉ๋ก ๋‹ค์šด๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜:", error); @@ -6343,10 +6427,33 @@ const Map = () => { WATER_DEPTH_LAYER_TYPES.includes(String(node?.data?.lyrTyNm ?? "")), ); setIsAgedPipeChecked(agedPipeChecked); - const fldngHstryChecked = checkedNodesData.some((node) => - isFldngHstryNode(node), + const floodWtlvChecked = checkedNodesData.some((node) => + isFloodHistoryWaterLevelNode(node), + ); + const checkedFloodDepthNodes = checkedNodesData.filter((node) => + isFloodHistoryDepthNode(node), + ); + const checkedDepthLayerKeys = Array.from( + new Set( + checkedFloodDepthNodes + .map((node) => String(node?.data?.lyrSn ?? node?.id ?? "").trim()) + .filter(Boolean), + ), + ).sort((left, right) => Number(left) - Number(right)); + const floodDepthChecked = checkedDepthLayerKeys.length > 0; + const fldngHstryChecked = floodWtlvChecked || floodDepthChecked; + setIsFloodWtlvChecked(floodWtlvChecked); + setIsFloodDepthChecked(floodDepthChecked); + setCheckedFloodDepthLayerKeys((currentKeys) => + currentKeys.join(",") === checkedDepthLayerKeys.join(",") + ? currentKeys + : checkedDepthLayerKeys, ); setIsFldngHstryChecked(fldngHstryChecked); + if (fldngHstryChecked && !floodLayerCheckedRef.current) { + setIsFloodYearSettingVisible(true); + } + floodLayerCheckedRef.current = fldngHstryChecked; if (!fldngHstryChecked) { clearFldngHstrySelection(); } @@ -6365,6 +6472,7 @@ const Map = () => { setActiveLayerSettingOrder((currentOrder) => { const activeKeys = [ waterDepthChecked ? "waterDepthLegend" : null, + fldngHstryChecked ? "fldngHstry" : null, sinkholeChecked ? "sinkhole" : null, agedPipeChecked ? "agedPipe" : null, exmnPplChecked ? "exmnPpl" : null, @@ -6522,6 +6630,23 @@ const Map = () => { }); } + if (isFldngHstryChecked && isFloodYearSettingVisible) { + nextItems.push({ + key: "fldngHstry", + label: "์นจ์ˆ˜ํ”์ ๋„", + order: resolveSettingOrder("fldngHstry", 9997), + content: ( + setIsFloodYearSettingVisible(false)} + /> + ), + }); + } + if (isSinkholeChecked) { nextItems.push({ key: "sinkhole", @@ -6603,9 +6728,13 @@ const Map = () => { exmnMnhlFilters, exmnPplFilters, handleApplyAgedPipeFilters, + floodYearRange, isAgedPipeChecked, isExmnMnhlChecked, isExmnPplChecked, + isFldngHstryChecked, + isFloodYearFilterEnabled, + isFloodYearSettingVisible, isSinkholeChecked, isWaterDepthLayerActive, lastSinkholeSyncSummary, @@ -6732,39 +6861,173 @@ const Map = () => { if (!mapReady || !gisMaps.current) return; const core = GisApp.Module.core as any; - core?.deleteLayer?.(FLDNG_HSTRY_WFS_LAYER_ID); + const yearCql = buildFloodHistoryYearCql( + floodYearRange, + isFloodYearFilterEnabled, + ); + const yearFilterSignature = `${floodYearRange.startYear}:${floodYearRange.endYear}:${isFloodYearFilterEnabled}`; + if ( + floodYearFilterSignatureRef.current && + floodYearFilterSignatureRef.current !== yearFilterSignature + ) { + clearFldngHstrySelection(); + } + floodYearFilterSignatureRef.current = yearFilterSignature; - if (!isFldngHstryChecked) return; - - const baseLayerCode = Object.values(GisApp.layerCode ?? {}).find( - (item: any) => - item?.lyrTyNm === FLOOD_HISTORY_LAYER_TYPE && item?.actlLyrYn === "Y", - ) as any; - - if (!baseLayerCode) return; - - const layerCode = { - ...baseLayerCode, - id: FLDNG_HSTRY_WFS_LAYER_ID, - lyrSn: FLDNG_HSTRY_WFS_LAYER_ID, - lyrNm: FLOOD_HISTORY_LAYER_NAME, - lyrTyNm: FLOOD_HISTORY_LAYER_TYPE, - actlLyrYn: "Y", - exprsYn: "Y", - clickPsblYn: "Y", - cmncNm: "bbox", - srvcNm: "wfs", + /** ๊ธฐ์ค€ ๋ ˆ์ด์–ด ํ•„ํ„ฐ๋ฅผ ๋ณด์กดํ•˜๋ฉด์„œ ์„ ํƒํ•œ ์นจ์ˆ˜๋…„๋„ ์กฐ๊ฑด์„ ๊ฒฐํ•ฉํ•œ๋‹ค. */ + const resolveFloodFilter = (baseLayerCode: any) => { + const baseFilter = String(baseLayerCode?.flterSql ?? "").trim(); + return [baseFilter && `(${baseFilter})`, yearCql] + .filter(Boolean) + .join(" AND "); }; - const layer = core?.addWfsLayer?.(layerCode); - layer?.set?.("id", FLDNG_HSTRY_WFS_LAYER_ID); - layer?.set?.("lyrNm", FLOOD_HISTORY_LAYER_NAME); - layer?.set?.("lyrTyNm", FLOOD_HISTORY_LAYER_TYPE); + type FloodLayerSpec = { + id: string; + layerType: string; + layerName: string; + baseLayerCode: any; + filterSql: string; + }; + const desiredLayers: FloodLayerSpec[] = []; + const layerCodes = Object.values(GisApp.layerCode ?? {}) as any[]; + if (isFloodWtlvChecked) { + const baseLayerCode = layerCodes.find( + (item) => + item?.lyrTyNm === FLOOD_HISTORY_LAYER_TYPE && item?.actlLyrYn === "Y", + ); + if (baseLayerCode) { + desiredLayers.push({ + id: FLDNG_HSTRY_WFS_LAYER_ID, + layerType: FLOOD_HISTORY_LAYER_TYPE, + layerName: baseLayerCode?.lyrNm || FLOOD_HISTORY_LAYER_NAME, + baseLayerCode, + filterSql: resolveFloodFilter(baseLayerCode), + }); + } + } + + if (isFloodDepthChecked) { + const selectedDepthLayerKeys = new Set(checkedFloodDepthLayerKeys); + layerCodes + .filter((item) => { + const layerKey = String(item?.lyrSn ?? item?.id ?? "").trim(); + return ( + item?.lyrTyNm === FLOOD_HISTORY_DEPTH_LAYER_TYPE && + item?.actlLyrYn === "Y" && + selectedDepthLayerKeys.has(layerKey) + ); + }) + .forEach((baseLayerCode) => { + const layerKey = String( + baseLayerCode?.lyrSn ?? baseLayerCode?.id, + ).trim(); + desiredLayers.push({ + id: `${FLDNG_DEPTH_WFS_LAYER_ID_PREFIX}_${layerKey}`, + layerType: FLOOD_HISTORY_DEPTH_LAYER_TYPE, + layerName: baseLayerCode?.lyrNm || FLOOD_HISTORY_DEPTH_LAYER_NAME, + baseLayerCode, + filterSql: resolveFloodFilter(baseLayerCode), + }); + }); + } + + const desiredLayerIds = new Set(desiredLayers.map((item) => item.id)); + const mapLayers = gisMaps.current?.getLayers?.()?.getArray?.() ?? []; + const isManagedFloodLayerId = (id: string) => + id === FLDNG_HSTRY_WFS_LAYER_ID || + id.startsWith(`${FLDNG_DEPTH_WFS_LAYER_ID_PREFIX}_`); + + // ์ฒดํฌ ํ•ด์ œ๋œ ๋“ฑ๊ธ‰๋งŒ ์‚ญ์ œํ•˜๊ณ , ๊ณ„์† ์ฒดํฌ๋œ ๋“ฑ๊ธ‰์€ ์ง€๋„์— ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•œ๋‹ค. + mapLayers.forEach((mapLayer: any) => { + const layerId = String(mapLayer?.get?.("id") ?? ""); + if (isManagedFloodLayerId(layerId) && !desiredLayerIds.has(layerId)) { + core?.deleteLayer?.(layerId); + } + }); + + desiredLayers.forEach((spec) => { + const filterSignature = `${spec.layerType}|${spec.filterSql}`; + // ๊ณตํ†ต getLayerById๋Š” ID๋ฅผ Number๋กœ ๋น„๊ตํ•˜๋ฏ€๋กœ ๋ฌธ์ž์—ด ID์ธ ํ•„ํ„ฐ ๋ ˆ์ด์–ด๋ฅผ ์ฐพ์ง€ ๋ชปํ•œ๋‹ค. + // ์ง€๋„ ์ปฌ๋ ‰์…˜์—์„œ ๋ฌธ์ž์—ด ID๋ฅผ ์ง์ ‘ ๋น„๊ตํ•ด ๊ธฐ์กด ๋ ˆ์ด์–ด์™€ WFS source๋ฅผ ์žฌ์‚ฌ์šฉํ•œ๋‹ค. + const existingLayer = gisMaps.current + ?.getLayers?.() + ?.getArray?.() + ?.find( + (mapLayer: any) => String(mapLayer?.get?.("id") ?? "") === spec.id, + ); + if (existingLayer?.get?.("floodFilterSignature") === filterSignature) { + existingLayer?.setVisible?.(true); + return; + } + + // ์—ฐ๋„๋‚˜ ์ ์šฉ ์—ฌ๋ถ€๊ฐ€ ๋‹ฌ๋ผ์ง„ ๋ ˆ์ด์–ด๋งŒ ๊ต์ฒดํ•˜๊ณ , ๋™์ผ ํ•„ํ„ฐ ๋ ˆ์ด์–ด๋Š” ์žฌ์‚ฌ์šฉํ•œ๋‹ค. + core?.deleteLayer?.(spec.id); + const layerCode = { + ...spec.baseLayerCode, + id: spec.id, + lyrSn: spec.id, + lyrNm: spec.layerName, + lyrTyNm: spec.layerType, + actlLyrYn: "Y", + exprsYn: "Y", + clickPsblYn: "Y", + cmncNm: "bbox", + srvcNm: "wfs", + flterSql: spec.filterSql, + }; + const layer = core?.addWfsLayer?.(layerCode); + layer?.set?.("id", spec.id); + layer?.set?.("lyrNm", spec.layerName); + layer?.set?.("lyrTyNm", spec.layerType); + layer?.set?.("floodFilterSignature", filterSignature); + }); + + // ์ „์šฉ ํ•„ํ„ฐ ๋ ˆ์ด์–ด๊ฐ€ ๋‹ด๋‹นํ•˜๋Š” ์œ ํ˜•์˜ ์›๋ณธ ๋ ˆ์ด์–ด๋งŒ ์ˆจ๊ฒจ ์ค‘๋ณต ํ‘œ์ถœ์„ ๋ง‰๋Š”๋‹ค. + const activeFilteredTypes = new Set( + desiredLayers.map((item) => item.layerType), + ); + gisMaps.current + ?.getLayers?.() + ?.getArray?.() + ?.forEach((mapLayer: any) => { + const layerId = String(mapLayer?.get?.("id") ?? ""); + const layerType = String(mapLayer?.get?.("lyrTyNm") ?? ""); + if ( + activeFilteredTypes.has(layerType) && + !isManagedFloodLayerId(layerId) + ) { + mapLayer?.setVisible?.(false); + } + }); + }, [ + checkedFloodDepthLayerKeys, + clearFldngHstrySelection, + floodYearRange, + isFldngHstryChecked, + isFloodDepthChecked, + isFloodWtlvChecked, + isFloodYearFilterEnabled, + mapReady, + ]); + + // ํŽ˜์ด์ง€๊ฐ€ ๋‹ซํž ๋•Œ๋งŒ ๊ด€๋ฆฌ ๋Œ€์ƒ ํ•„ํ„ฐ ๋ ˆ์ด์–ด ์ „์ฒด๋ฅผ ์ •๋ฆฌํ•œ๋‹ค. + useEffect(() => { return () => { - core?.deleteLayer?.(FLDNG_HSTRY_WFS_LAYER_ID); + const map = gisMaps.current; + const layers = [...(map?.getLayers?.()?.getArray?.() ?? [])]; + layers.forEach((layer: any) => { + const layerId = String(layer?.get?.("id") ?? ""); + if ( + layerId === FLDNG_HSTRY_WFS_LAYER_ID || + layerId.startsWith(`${FLDNG_DEPTH_WFS_LAYER_ID_PREFIX}_`) + ) { + map?.removeLayer?.(layer); + } + }); }; - }, [isFldngHstryChecked, mapReady]); + }, []); const openAnalysisPopup = useCallback( (popupKey: WindowPopupKey) => { @@ -8936,15 +9199,11 @@ const Map = () => { : "bg-[#e8f6f4] text-[#0f766e]" }`} > - {buildWaterRiskStatusLabel( - selectedWaterRiskDisplayStatus, - )} + {buildWaterRiskStatusLabel(selectedWaterRiskDisplayStatus)}
-
- GIS ๋งคํ•‘ ๊ด€๋กœ ์ค‘ Capacity 0.8 ์ด์ƒ์ด 1๊ฐœ๋ผ๋„ ์žˆ์œผ๋ฉด -
+
GIS ๋งคํ•‘ ๊ด€๋กœ ์ค‘ Capacity 0.8 ์ด์ƒ์ด 1๊ฐœ๋ผ๋„ ์žˆ์œผ๋ฉด
์œ„ํ—˜, ์—†์œผ๋ฉด{" "} ์ •์ƒ diff --git a/apps/sn/app/store/useOnsemiroAddrStore.ts b/apps/sn/app/store/useOnsemiroAddrStore.ts index f48f02af1..3702c1e8f 100644 --- a/apps/sn/app/store/useOnsemiroAddrStore.ts +++ b/apps/sn/app/store/useOnsemiroAddrStore.ts @@ -111,6 +111,22 @@ const loadSelectOptions = async (url: string) => { .filter((item): item is SelectDataType => Boolean(item)); }; +/** ONSEMIRO ํ™˜๊ฒฝ์ฒญ ์‘๋‹ต์„ ์ฝ”๋“œ/ํ™˜๊ฒฝ์ฒญ๋ช… ๊ธฐ์ค€์œผ๋กœ SelectBox ์˜ต์…˜์œผ๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค. */ +const loadGovAreaOptions = async () => { + const response = await getRawJson( + "/sewerage-service/api/v1/onsemiro/addr/govArea", + ); + return toResponseList(response) + .map((item) => { + if (!item || typeof item !== "object") return null; + const { lclgvrnmntCd, lclgvrnmntNm } = item as Record; + const code = String(lclgvrnmntCd ?? "").trim(); + const name = String(lclgvrnmntNm ?? "").trim(); + return code && name ? { text: name, val: code } : null; + }) + .filter((item): item is SelectDataType => Boolean(item)); +}; + /** ONSEMIRO ํ™˜๊ฒฝ์ฒญยท์‹œ๋„ยท์‹œ๊ตฐ๊ตฌ ์„ ํƒ๋ชฉ๋ก์„ ๊ณต์œ ํ•˜๋Š” ์ƒํƒœ ์ €์žฅ์†Œ๋‹ค. */ const useOnsemiroAddrStore = create((set) => ({ govArea: [], @@ -118,9 +134,7 @@ const useOnsemiroAddrStore = create((set) => ({ sgg: [], /** ํ™˜๊ฒฝ์ฒญ ์„ ํƒ๋ชฉ๋ก์„ ์กฐํšŒํ•ด ํ•œ๊ธ€๋ช… ์ˆœ์œผ๋กœ ์ €์žฅํ•œ๋‹ค. */ fetchGovArea: async () => { - const govArea = await loadSelectOptions( - "/sewerage-service/api/v1/onsemiro/addr/govArea", - ); + const govArea = await loadGovAreaOptions(); set({ govArea: govArea.sort((left, right) => left.text.localeCompare(right.text, "ko"), From 8198fda45b6707b2ab4d2304a214d87f06859b40 Mon Sep 17 00:00:00 2001 From: kiminchan Date: Tue, 1 Sep 2026 15:55:02 +0900 Subject: [PATCH 04/12] =?UTF-8?q?=EA=B3=B5=ED=86=B5=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=B0=98=20=EC=97=85=EB=AC=B4=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=ED=91=9C=EC=8B=9C=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gisEdit/GisEditSessionPanel.tsx | 27 +-- .../map/components/gisEdit/GisEditToolbar.tsx | 16 +- .../gisEdit/useGisEditCommonCodes.ts | 75 +++++++++ .../gisManagement/GisManagementPopupPage.tsx | 50 +++++- .../useGisManagementCommonCodes.ts | 127 ++++++++++++++ .../sinkhole/SinkholeDetailPanel.tsx | 36 ++-- .../components/sinkhole/SinkholePopupPage.tsx | 56 ++++++- .../sinkhole/useSinkholeCommonCodes.ts | 138 ++++++++++++++++ .../components/DredgingDetailPopupPage.tsx | 15 +- .../components/DredgingRegisterModal.tsx | 27 +-- .../dredging/lib/useDredgingCommonCodes.ts | 135 +++++++++++++++ .../app/components/lib/common/apiHelpers.ts | 46 +++++- .../002-qa-SFR-010-004~007-์กฐ์‚ฌ์‚ฌ์—…-ํ†ตํ•ฉ๊ฒ€์ฆ๊ฒฐ๊ณผ.md | 10 ++ .../002-qa-SFR-010-010-์ง€๋ฐ˜์นจํ•˜์ •๋ณด-ํ†ตํ•ฉ๊ฒ€์ฆ๊ฒฐ๊ณผ.md | 10 ++ .../001-qa-SFR-010-012-์ค€์„ค๊ด€๋ฆฌ-๊ฒ€์ฆ๊ฒฐ๊ณผ.md | 10 ++ .../002-qa-SFR-012-002-ํ•˜์ˆ˜๊ด€๋ง๋„ ์—…๋กœ๋“œ-ํ†ตํ•ฉ๊ฒ€์ฆ๊ฒฐ๊ณผ.md | 10 ++ .../002-qa-SFR-012-003-ํ•˜์ˆ˜๊ด€๋กœํŽธ์ง‘-ํ†ตํ•ฉ๊ฒ€์ฆ๊ฒฐ๊ณผ.md | 15 ++ .../002-qa-SN-๋…ธํ›„ํ•˜์ˆ˜๊ด€๋กœ์ •๋น„์‚ฌ์—…-ํ†ตํ•ฉ๊ฒ€์ฆ๊ฒฐ๊ณผ.md | 9 + .../001-qa-SN-๋„์‹œ์นจ์ˆ˜์˜ˆ๋ฐฉ์‚ฌ์—…-FE๊ฒ€์ฆ.md | 9 + .../001-fe-SFR-010-004~007-์กฐ์‚ฌ์‚ฌ์—…-ํ†ตํ•ฉ๊ตฌํ˜„๊ธฐ๋ก.md | 9 + .../001-fe-SFR-010-010-์ง€๋ฐ˜์นจํ•˜์ •๋ณด-ํ†ตํ•ฉ๊ตฌํ˜„๊ธฐ๋ก.md | 11 ++ .../001-fe-SFR-010-012-์ค€์„ค๊ด€๋ฆฌ-๊ตฌํ˜„๊ธฐ๋ก.md | 9 + .../001-fe-SFR-012-002-ํ•˜์ˆ˜๊ด€๋ง๋„ ์—…๋กœ๋“œ-ํ†ตํ•ฉ๊ตฌํ˜„๊ธฐ๋ก.md | 10 ++ .../001-fe-SFR-012-003-ํ•˜์ˆ˜๊ด€๋กœํŽธ์ง‘-ํ†ตํ•ฉ๊ตฌํ˜„๊ธฐ๋ก.md | 10 ++ .../001-fe-SN-๋…ธํ›„ํ•˜์ˆ˜๊ด€๋กœ์ •๋น„์‚ฌ์—…-ํ†ตํ•ฉ๊ตฌํ˜„๊ธฐ๋ก.md | 9 + .../001-fe-SN-๋„์‹œ์นจ์ˆ˜์˜ˆ๋ฐฉ์‚ฌ์—…-๊ตฌํ˜„๋‚ด์šฉ.md | 8 + .../ui/CtyFldngPrvtnBizFormPage.tsx | 59 +++++-- .../ui/CtyFldngPrvtnBizListPage.tsx | 45 ++++- .../features/exmn/lib/useExmnCommonCodes.ts | 137 ++++++++++++++++ .../exmn/ui/ExmnPplUnitDetailTable.tsx | 16 +- .../exmn/ui/ExmnSurveyDetailPopupPage.tsx | 47 +++++- .../exmn/ui/PplFacilityDetailModal.tsx | 63 +++++-- .../src/features/exmn/ui/PplSummaryModal.tsx | 17 +- .../features/obsltPpl/ui/ObsltPplFormPage.tsx | 73 ++++++--- .../features/obsltPpl/ui/ObsltPplListPage.tsx | 37 ++++- .../lib/use-oper-business-common-codes.ts | 155 ++++++++++++++++++ 36 files changed, 1379 insertions(+), 157 deletions(-) create mode 100644 apps/sn/app/(map-layout)/map/components/gisEdit/useGisEditCommonCodes.ts create mode 100644 apps/sn/app/(map-layout)/map/components/gisManagement/useGisManagementCommonCodes.ts create mode 100644 apps/sn/app/(map-layout)/map/components/sinkhole/useSinkholeCommonCodes.ts create mode 100644 apps/sn/app/(with-base-layout)/oper/dredging/lib/useDredgingCommonCodes.ts create mode 100644 apps/sn/src/features/exmn/lib/useExmnCommonCodes.ts create mode 100644 apps/sn/src/shared/lib/use-oper-business-common-codes.ts 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}
@@ -96,9 +103,14 @@ export default function AgedPipeLayerSettingPanel({ if (compact) { return ( -
+ {settingRow} -
+ ); } 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 index ed579f904..0cf554a46 100644 --- a/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryLayerSettingPanel.tsx +++ b/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryLayerSettingPanel.tsx @@ -36,7 +36,7 @@ export default function FldngHstryLayerSettingPanel({ .filter((year) => Number(year) >= Number(normalizedRange.startYear)) .map((year) => ({ val: year, text: year })); const yearSelectClass = - "h-[22px] w-[62px] rounded-none border border-input bg-background px-[4px] text-center text-[1rem] leading-none text-foreground"; + "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( @@ -49,7 +49,7 @@ export default function FldngHstryLayerSettingPanel({ return (
-
+
์นจ์ˆ˜ํ”์ ๋„ @@ -58,18 +58,18 @@ export default function FldngHstryLayerSettingPanel({ type="button" role="switch" aria-checked={enabled} - aria-label="์นจ์ˆ˜์—ฐ๋„ ํ•„ํ„ฐ ์ ์šฉ ์—ฌ๋ถ€" - title={enabled ? "์นจ์ˆ˜์—ฐ๋„ ํ•„ํ„ฐ ์ ์šฉ ์ค‘" : "์นจ์ˆ˜์—ฐ๋„ ํ•„ํ„ฐ ๋ฏธ์ ์šฉ"} - className={`relative h-[18px] w-[34px] rounded-full transition-colors ${enabled ? "bg-primary" : "bg-muted-foreground/40"}`} + aria-label="์นจ์ˆ˜๋…„๋„ ํ•„ํ„ฐ ์ ์šฉ ์—ฌ๋ถ€" + title={enabled ? "์นจ์ˆ˜๋…„๋„ ํ•„ํ„ฐ ์ ์šฉ ์ค‘" : "์นจ์ˆ˜๋…„๋„ ํ•„ํ„ฐ ๋ฏธ์ ์šฉ"} + className={`relative h-[16px] w-[28px] rounded-full border transition-colors ${enabled ? "border-primary bg-primary" : "border-[#94a3b8] bg-[#cbd5e1]"}`} onClick={() => onEnabledChange(!enabled)} >
-
+
์นจ์ˆ˜๋…„๋„
diff --git a/apps/sn/app/(map-layout)/map/components/layerSetting/LayerSettingCard.tsx b/apps/sn/app/(map-layout)/map/components/layerSetting/LayerSettingCard.tsx new file mode 100644 index 000000000..04429c927 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/layerSetting/LayerSettingCard.tsx @@ -0,0 +1,59 @@ +"use client"; + +import type { ReactNode } from "react"; + +type Props = { + title: string; + enabled: boolean; + onEnabledChange: (enabled: boolean) => void; + onClose: () => void; + children?: ReactNode; + showToggle?: boolean; +}; + +/** ์นจ์ˆ˜ํ”์ ๋„ ์„ค์ •๊ณผ ๊ฐ™์€ ์ œ๋ชฉยทํ† ๊ธ€ยท๋‹ซ๊ธฐยท๋ณธ๋ฌธ ์—ฌ๋ฐฑ์„ ์ œ๊ณตํ•œ๋‹ค. */ +export default function LayerSettingCard({ + title, + enabled, + onEnabledChange, + onClose, + children, + showToggle = true, +}: Props) { + return ( +
+
+ {title} +
+ {showToggle ? ( + + ) : null} + +
+
+ {children ? ( +
{children}
+ ) : null} +
+ ); +} diff --git a/apps/sn/app/(map-layout)/map/components/sinkhole/SinkholeLayerSettingPanel.tsx b/apps/sn/app/(map-layout)/map/components/sinkhole/SinkholeLayerSettingPanel.tsx index d928a653e..3765cbba5 100644 --- a/apps/sn/app/(map-layout)/map/components/sinkhole/SinkholeLayerSettingPanel.tsx +++ b/apps/sn/app/(map-layout)/map/components/sinkhole/SinkholeLayerSettingPanel.tsx @@ -10,6 +10,7 @@ import { type SinkholeSyncSummary, type SinkholeYearRange, } from "./sinkholeTypes"; +import LayerSettingCard from "../layerSetting/LayerSettingCard"; type MaybePromise = T | Promise; @@ -21,6 +22,9 @@ type Props = { lastSyncSummary?: SinkholeSyncSummary | null; isAdmin?: boolean; compact?: boolean; + enabled?: boolean; + onEnabledChange?: (enabled: boolean) => void; + onClose?: () => void; }; export default function SinkholeLayerSettingPanel({ @@ -31,6 +35,9 @@ export default function SinkholeLayerSettingPanel({ lastSyncSummary, isAdmin = false, compact = false, + enabled = true, + onEnabledChange = () => {}, + onClose = () => {}, }: Props) { const alert = useAlertStore((state) => state.setMessage); const [isCollapsed, setIsCollapsed] = useState(false); @@ -94,7 +101,7 @@ export default function SinkholeLayerSettingPanel({ const currentYear = new Date().getFullYear(); const yearSelectClass = - "h-[22px] w-[62px] rounded-none border border-input bg-background px-[4px] text-center text-[1rem] leading-none text-foreground"; + "h-[20px] !w-[58px] !rounded border border-input bg-background px-[1px] text-center text-[1.1rem] leading-none text-foreground"; const startYearOptions = Array.from( { length: currentYear - SINKHOLE_MIN_SEARCH_YEAR + 1 }, (_, index) => String(currentYear - index), @@ -117,17 +124,17 @@ export default function SinkholeLayerSettingPanel({ .map((year) => ({ val: year, text: year })); 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 settingRows = ( -
-
+
+
{indicator}
-
+
{indicator}
@@ -208,7 +215,7 @@ export default function SinkholeLayerSettingPanel({ title="์ ์šฉ" aria-label="์—ฐ๋„๋ฒ”์œ„ ์ ์šฉ" > - โœ“ + โœ“
@@ -217,9 +224,14 @@ export default function SinkholeLayerSettingPanel({ if (compact) { return ( -
+
{settingRows}
-
+ ); } diff --git a/apps/sn/app/(map-layout)/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx index 9b6037bdf..ff0c97513 100644 --- a/apps/sn/app/(map-layout)/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -104,6 +104,7 @@ import SinkholeLayerSettingPanel from "./components/sinkhole/SinkholeLayerSettin import FldngHstryDetailPanel from "./components/fldngHstry/FldngHstryDetailPanel"; import FldngHstryLayerSettingPanel from "./components/fldngHstry/FldngHstryLayerSettingPanel"; import ExmnLayerSettingPanel from "./components/exmn/ExmnLayerSettingPanel"; +import LayerSettingCard from "./components/layerSetting/LayerSettingCard"; import { getExmnMapLayer } from "./components/exmn/exmnMapApi"; import { EMPTY_EXMN_MAP_FILTERS, @@ -3013,6 +3014,24 @@ const Map = () => { const [checkedLayerLegendVersion, setCheckedLayerLegendVersion] = useState(0); const [isAgedPipeChecked, setIsAgedPipeChecked] = useState(false); const [isSinkholeChecked, setIsSinkholeChecked] = useState(false); + const [isSinkholeSettingEnabled, setIsSinkholeSettingEnabled] = + useState(true); + const [isSinkholeSettingVisible, setIsSinkholeSettingVisible] = + useState(true); + const [isAgedPipeSettingEnabled, setIsAgedPipeSettingEnabled] = + useState(true); + const [isAgedPipeSettingVisible, setIsAgedPipeSettingVisible] = + useState(true); + const [isExmnPplSettingEnabled, setIsExmnPplSettingEnabled] = useState(true); + const [isExmnPplSettingVisible, setIsExmnPplSettingVisible] = useState(true); + const [isExmnMnhlSettingEnabled, setIsExmnMnhlSettingEnabled] = + useState(true); + const [isExmnMnhlSettingVisible, setIsExmnMnhlSettingVisible] = + useState(true); + const [isWaterDepthLegendEnabled, setIsWaterDepthLegendEnabled] = + useState(true); + const [isWaterDepthLegendVisible, setIsWaterDepthLegendVisible] = + useState(true); const [isFldngHstryChecked, setIsFldngHstryChecked] = useState(false); // ์นจ์ˆ˜ํ”์ ์œ„์„ ๊ณผ ์‹ฌ์„ ์€ ํ•˜๋‚˜์˜ ์—ฐ๋„ ๋ฒ”์œ„๋ฅผ ๊ณต์œ ํ•˜์ง€๋งŒ ์ฒดํฌ ์ƒํƒœ๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ๊ด€๋ฆฌํ•œ๋‹ค. const [isFloodWtlvChecked, setIsFloodWtlvChecked] = useState(false); @@ -3030,6 +3049,11 @@ const Map = () => { useState(true); // ๋‹ซํžŒ ์„ค์ • ์นด๋“œ๋Š” ๋ ˆ์ด์–ด๊ฐ€ ์™„์ „ํžˆ ํ•ด์ œ๋œ ๋’ค ๋‹ค์‹œ ์ฒดํฌ๋  ๋•Œ๋งŒ ์ž๋™์œผ๋กœ ์—ฐ๋‹ค. const floodLayerCheckedRef = useRef(false); + const sinkholeLayerCheckedRef = useRef(false); + const agedPipeLayerCheckedRef = useRef(false); + const exmnPplLayerCheckedRef = useRef(false); + const exmnMnhlLayerCheckedRef = useRef(false); + const waterDepthLayerCheckedRef = useRef(false); // ์ฒดํฌ ๋“ฑ๊ธ‰ ๋ณ€๊ฒฝ๊ณผ ์—ฐ๋„ ํ•„ํ„ฐ ๋ณ€๊ฒฝ์„ ๊ตฌ๋ถ„ํ•ด ์—ฐ๋„ ๋ณ€๊ฒฝ ๋•Œ๋งŒ ๊ธฐ์กด ์„ ํƒ ์ƒ์„ธ๋ฅผ ์ •๋ฆฌํ•œ๋‹ค. const floodYearFilterSignatureRef = useRef(""); const [isExmnPplChecked, setIsExmnPplChecked] = useState(false); @@ -3682,12 +3706,30 @@ const Map = () => { if (isSinkholePopupFilterActive) { return normalizeSinkholeFilters(currentFilters, sinkholeScope); } + if (!isSinkholeSettingEnabled) { + return normalizeSinkholeFilters( + { + ...buildSinkholeFiltersFromYearRange( + sinkholeYearRange, + sinkholeScope, + ), + startYmd: "", + endYmd: "", + }, + sinkholeScope, + ); + } return normalizeSinkholeFilters( buildSinkholeFiltersFromYearRange(sinkholeYearRange, sinkholeScope), sinkholeScope, ); }); - }, [isSinkholePopupFilterActive, sinkholeScope, sinkholeYearRange]); + }, [ + isSinkholePopupFilterActive, + isSinkholeSettingEnabled, + sinkholeScope, + sinkholeYearRange, + ]); useEffect(() => { const timer = window.setInterval(() => { @@ -6426,6 +6468,14 @@ const Map = () => { const waterDepthChecked = checkedNodesData.some((node) => WATER_DEPTH_LAYER_TYPES.includes(String(node?.data?.lyrTyNm ?? "")), ); + if (agedPipeChecked && !agedPipeLayerCheckedRef.current) { + setIsAgedPipeSettingVisible(true); + } + if (waterDepthChecked && !waterDepthLayerCheckedRef.current) { + setIsWaterDepthLegendVisible(true); + } + agedPipeLayerCheckedRef.current = agedPipeChecked; + waterDepthLayerCheckedRef.current = waterDepthChecked; setIsAgedPipeChecked(agedPipeChecked); const floodWtlvChecked = checkedNodesData.some((node) => isFloodHistoryWaterLevelNode(node), @@ -6460,6 +6510,10 @@ const Map = () => { const sinkholeChecked = checkedNodesData.some((node) => isSinkholeLayerNode(node), ); + if (sinkholeChecked && !sinkholeLayerCheckedRef.current) { + setIsSinkholeSettingVisible(true); + } + sinkholeLayerCheckedRef.current = sinkholeChecked; setIsSinkholeChecked(sinkholeChecked); const exmnPplChecked = checkedNodesData.some((node) => isExmnResultPplNode(node), @@ -6467,6 +6521,14 @@ const Map = () => { const exmnMnhlChecked = checkedNodesData.some((node) => isExmnResultMnhlNode(node), ); + if (exmnPplChecked && !exmnPplLayerCheckedRef.current) { + setIsExmnPplSettingVisible(true); + } + if (exmnMnhlChecked && !exmnMnhlLayerCheckedRef.current) { + setIsExmnMnhlSettingVisible(true); + } + exmnPplLayerCheckedRef.current = exmnPplChecked; + exmnMnhlLayerCheckedRef.current = exmnMnhlChecked; setIsExmnPplChecked(exmnPplChecked); setIsExmnMnhlChecked(exmnMnhlChecked); setActiveLayerSettingOrder((currentOrder) => { @@ -6522,15 +6584,9 @@ const Map = () => { ); setSinkholeYearRange(normalizedRange); setIsSinkholePopupFilterActive(false); - setAppliedSinkholeFilters( - normalizeSinkholeFilters( - buildSinkholeFiltersFromYearRange(normalizedRange, sinkholeScope), - sinkholeScope, - ), - ); setSinkholeSelectionVersion((version) => version + 1); }, - [sinkholeScope, sinkholeYearRange], + [sinkholeYearRange], ); /** ์นจ์ˆ˜์œ„ํ—˜์ง€๋„ ๋ฃจํŠธ์˜ ๋ชฉ๋ก๋ณด๊ธฐ ๋ฒ„ํŠผ์—์„œ ์ „์šฉ ์กฐํšŒ ํŒ์—…์„ ์—ฐ๋‹ค. */ @@ -6600,7 +6656,7 @@ const Map = () => { return index >= 0 ? index : fallback; }; - if (isExmnPplChecked) { + if (isExmnPplChecked && isExmnPplSettingVisible) { nextItems.push({ key: "exmnPpl", label: "๊ด€๋กœ์กฐ์‚ฌ", @@ -6610,12 +6666,15 @@ const Map = () => { jobType="PPL" filters={exmnPplFilters} onFiltersChange={setExmnPplFilters} + enabled={isExmnPplSettingEnabled} + onEnabledChange={setIsExmnPplSettingEnabled} + onClose={() => setIsExmnPplSettingVisible(false)} /> ), }); } - if (isExmnMnhlChecked) { + if (isExmnMnhlChecked && isExmnMnhlSettingVisible) { nextItems.push({ key: "exmnMnhl", label: "๋งจํ™€์กฐ์‚ฌ", @@ -6625,6 +6684,9 @@ const Map = () => { jobType="MNHL" filters={exmnMnhlFilters} onFiltersChange={setExmnMnhlFilters} + enabled={isExmnMnhlSettingEnabled} + onEnabledChange={setIsExmnMnhlSettingEnabled} + onClose={() => setIsExmnMnhlSettingVisible(false)} /> ), }); @@ -6647,7 +6709,7 @@ const Map = () => { }); } - if (isSinkholeChecked) { + if (isSinkholeChecked && isSinkholeSettingVisible) { nextItems.push({ key: "sinkhole", label: "์ง€๋ฐ˜์นจํ•˜์ •๋ณด", @@ -6660,13 +6722,16 @@ const Map = () => { onApplyYearRange={applySinkholeYearRange} lastSyncSummary={lastSinkholeSyncSummary} isAdmin={userInfo?.adminRoleYn === "Y"} + enabled={isSinkholeSettingEnabled} + onEnabledChange={setIsSinkholeSettingEnabled} + onClose={() => setIsSinkholeSettingVisible(false)} compact /> ), }); } - if (isAgedPipeChecked) { + if (isAgedPipeChecked && isAgedPipeSettingVisible) { nextItems.push({ key: "agedPipe", label: "๋…ธํ›„๊ด€๋กœ", @@ -6676,43 +6741,47 @@ const Map = () => { filters={draftAgedPipeFilters} onFiltersChange={setDraftAgedPipeFilters} onApply={handleApplyAgedPipeFilters} + enabled={isAgedPipeSettingEnabled} + onEnabledChange={setIsAgedPipeSettingEnabled} + onClose={() => setIsAgedPipeSettingVisible(false)} compact /> ), }); } - if (isWaterDepthLayerActive) { + if (isWaterDepthLayerActive && isWaterDepthLegendVisible) { nextItems.push({ key: "waterDepthLegend", label: "์นจ์ˆ˜์‹ฌ ๋ฒ”๋ก€", order: resolveSettingOrder("waterDepthLegend", 9997), content: ( -
-
-
-
- ์นจ์ˆ˜์‹ฌ ๋ฒ”๋ก€ -
-
- {waterDepthLegendItems.map((item) => ( -
- - - {item.label} - -
- ))} -
+ setIsWaterDepthLegendVisible(false)} + showToggle={false} + > + {isWaterDepthLegendEnabled ? ( +
+ {waterDepthLegendItems.map((item) => ( +
+ + + {item.label} + +
+ ))}
-
-
+ ) : null} + ), }); } @@ -6730,13 +6799,23 @@ const Map = () => { handleApplyAgedPipeFilters, floodYearRange, isAgedPipeChecked, + isAgedPipeSettingEnabled, + isAgedPipeSettingVisible, isExmnMnhlChecked, + isExmnMnhlSettingEnabled, + isExmnMnhlSettingVisible, isExmnPplChecked, + isExmnPplSettingEnabled, + isExmnPplSettingVisible, isFldngHstryChecked, isFloodYearFilterEnabled, isFloodYearSettingVisible, isSinkholeChecked, + isSinkholeSettingEnabled, + isSinkholeSettingVisible, isWaterDepthLayerActive, + isWaterDepthLegendEnabled, + isWaterDepthLegendVisible, lastSinkholeSyncSummary, sinkholeRadiusMeters, sinkholeYearRange, @@ -6775,24 +6854,9 @@ const Map = () => { return (
-
- - ์„ ํƒ ๋ ˆ์ด์–ด ์„ค์ • - - - {activeLayerSettingItems.length}๊ฑด - -
-
- {activeLayerSettingItems.map((item) => ( - - {item.label} - - ))} -
+ + ์„ ํƒ ๋ ˆ์ด์–ด ์„ค์ • +
{ const core = GisApp.Module.core as any; core?.deleteLayer?.(AGED_PIPE_HIGHLIGHT_LAYER_ID); - if (!isAgedPipeChecked) return; + if (!isAgedPipeChecked || !isAgedPipeSettingEnabled) return; const baseLayerCode = Object.values(GisApp.layerCode ?? {}).find( (item: any) => @@ -6855,7 +6919,12 @@ const Map = () => { return () => { core?.deleteLayer?.(AGED_PIPE_HIGHLIGHT_LAYER_ID); }; - }, [appliedAgedPipeFilters, isAgedPipeChecked, mapReady]); + }, [ + appliedAgedPipeFilters, + isAgedPipeChecked, + isAgedPipeSettingEnabled, + mapReady, + ]); useEffect(() => { if (!mapReady || !gisMaps.current) return; @@ -9038,10 +9107,14 @@ const Map = () => { removeExmnResultLayer("PPL"); return; } - void loadExmnResultLayer("PPL", exmnPplFilters); + void loadExmnResultLayer( + "PPL", + isExmnPplSettingEnabled ? exmnPplFilters : EMPTY_EXMN_MAP_FILTERS, + ); }, [ exmnPplFilters, isExmnPplChecked, + isExmnPplSettingEnabled, loadExmnResultLayer, mapReady, removeExmnResultLayer, @@ -9052,10 +9125,14 @@ const Map = () => { removeExmnResultLayer("MNHL"); return; } - void loadExmnResultLayer("MNHL", exmnMnhlFilters); + void loadExmnResultLayer( + "MNHL", + isExmnMnhlSettingEnabled ? exmnMnhlFilters : EMPTY_EXMN_MAP_FILTERS, + ); }, [ exmnMnhlFilters, isExmnMnhlChecked, + isExmnMnhlSettingEnabled, loadExmnResultLayer, mapReady, removeExmnResultLayer, diff --git a/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx b/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx index 3c9cb48f5..266b4b59c 100644 --- a/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx +++ b/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx @@ -89,7 +89,9 @@ const hasLegendStyle = (layerInfo: any) => const normalizeLegendLabel = (label: string) => { const normalized = String(label).trim(); - return normalized.endsWith("๋ฏธ๋งŒ") ? normalized : `${normalized} ๋ฏธ๋งŒ`; + return /(?:๋ฏธ๋งŒ|์ด์ƒ)$/.test(normalized) + ? normalized + : `${normalized} ๋ฏธ๋งŒ`; }; export const extractWaterDepthLegendItems = ( 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 f50dfac15..1e06ae146 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 @@ -346,3 +346,22 @@ - ์ˆ˜์ • ์ง„์ž…์€ `์–‘์‹๋‹ค์šด๋กœ๋“œ โ†’ ์ €์žฅ โ†’ ์ทจ์†Œ`๋งŒ ํ‘œ์‹œํ•˜๊ณ  ์กฐ์‚ฌ๋ณด๊ณ ์„œ ๊ณตํ†ต ์–‘์‹ ๋ฒ„ํŠผ์€ ์ˆจ๊ธด๋‹ค. - ์ˆ˜์ • ํ™”๋ฉด์˜ ์–‘์‹๋‹ค์šด๋กœ๋“œ๋Š” ์ €์žฅ๋œ ์ตœ์‹  ์ฒ˜๋ฆฌ๋ถ„๊ตฌ ๊ธฐ์ค€์œผ๋กœ ์ž…๋ ฅ์–‘์‹์„ ๋‹ค์‹œ ์ƒ์„ฑํ•œ๋‹ค. - ๋“ฑ๋ก๋œ ์‹ค์ œ ์‚ฌ์—…๋ณด๊ณ ์„œ PDF๋Š” ๊ธฐ์กด ์กฐ์‚ฌ๋ชฉ๋ก์˜ ๋ณด๊ณ ์„œ ์—ด์—์„œ ๋‹ค์šด๋กœ๋“œํ•˜๋Š” ๊ณ„์•ฝ์„ ์œ ์ง€ํ•œ๋‹ค. + +## 27. 2026-09-01 ์ง€๋„ ์กฐ์‚ฌ ์„ค์ • ์นด๋“œ UI ํ†ต์ผ ๊ณ„ํš + +- `ExmnLayerSettingPanel`์˜ ๊ด€๋กœ์กฐ์‚ฌยท๋งจํ™€์กฐ์‚ฌ ์นด๋“œ๋ฅผ ์นจ์ˆ˜ํ”์ ๋„ ์„ค์ •๊ณผ ๋™์ผํ•œ header/body ๊ตฌ์กฐ๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค. +- ์ œ๋ชฉ ์˜ค๋ฅธ์ชฝ์˜ ์•ก์…˜ `div`์—๋Š” `์ ์šฉ ์—ฌ๋ถ€ ํ† ๊ธ€ โ†’ ๋‹ซ๊ธฐ(ร—)`๋ฅผ ๋ฐฐ์น˜ํ•˜๊ณ  ํ† ๊ธ€ ๊ธฐ๋ณธ๊ฐ’์€ ON์œผ๋กœ ํ•œ๋‹ค. +- ํ† ๊ธ€ OFF์—์„œ๋Š” ํ•ด๋‹น ์กฐ์‚ฌ์œ ํ˜•์˜ ์‚ฌ์—…์ฐจ์ˆ˜ยท๊ตฌ์กฐยท์šด์˜ ํ•„ํ„ฐ ์ ์šฉ์„ ํ•ด์ œํ•˜๋˜ ์ž…๋ ฅ๊ฐ’์€ ์œ ์ง€ํ•˜๋ฉฐ, ๋‹ซ๊ธฐ๋Š” ์นด๋“œ๋งŒ ์ˆจ๊ธฐ๊ณ  ๋ ˆ์ด์–ด ์ฒดํฌยทํ•„ํ„ฐ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•œ๋‹ค. +- ๋‘ ์กฐ์‚ฌ์œ ํ˜•์˜ ์ ์šฉ ์—ฌ๋ถ€, ๋‹ซ๊ธฐ, ์ž…๋ ฅ๊ฐ’์€ ๋…๋ฆฝ ์ƒํƒœ๋กœ ๊ด€๋ฆฌํ•œ๋‹ค. +- ์นด๋“œ ์ œ๋ชฉ `1.15rem`, ๋ณธ๋ฌธ `1.05rem`, header `32px`, ๋ณธ๋ฌธ padding `8px`, ์„ ํƒ ์ž…๋ ฅ `22px`, font `1rem`, ์ขŒ์šฐ padding `4px`๋ฅผ ์ ์šฉํ•œ๋‹ค. +- ๊ด€๋กœ ๋…ธ๋ž‘ยท๋งจํ™€ ๋ณด๋ผ ์˜๋ฏธ์ƒ‰์€ indicator๋กœ ์œ ์ง€ํ•˜๊ณ  ์นด๋“œ ์™ธ๊ณฝยท์ œ๋ชฉ์€ ์ค‘๋ฆฝ ์Šคํƒ€์ผ๋กœ ํ†ต์ผํ•œ๋‹ค. +- ์ƒํƒœ๋“ฑ๊ธ‰ select๋Š” ์นจ์ˆ˜ํ”์ ๋„ ์„ค์ •์˜ SelectBox์™€ ๊ฐ™์€ fontยทpaddingยทborder/background ์Šคํƒ€์ผ์„ ์ ์šฉํ•œ๋‹ค. +- ๊ธฐ์กด ์ž๋™๋ฐ˜์˜ ๋™์ž‘์„ ์œ ์ง€ํ•˜๋ฉฐ ์›๋ž˜ ์—†๋Š” `โœ“` ๋ฒ„ํŠผ์„ ์ƒˆ๋กœ ์ถ”๊ฐ€ํ•˜์ง€ ์•Š๋Š”๋‹ค. ์ง€๋ฐ˜์นจํ•˜ยท๋…ธํ›„๊ด€๋กœ ๋“ฑ ๊ธฐ์กด `โœ“`๊ฐ€ ์žˆ๋Š” ์นด๋“œ์˜ ๋ฒ„ํŠผ์€ ๊ณตํ†ต ๋ณ€๊ฒฝ์œผ๋กœ ์ œ๊ฑฐํ•˜์ง€ ์•Š๋Š”๋‹ค. +- `map/page.tsx`์˜ ์„ ํƒ ๋ ˆ์ด์–ด ์„ค์ • header์—์„œ๋Š” ํ™œ์„ฑ ์นด๋“œ ๊ฐœ์ˆ˜์™€ ์นด๋“œ๋ช… pill/chip ๋ชฉ๋ก์„ ์ œ๊ฑฐํ•˜๊ณ  ์ œ๋ชฉ๋งŒ ์œ ์ง€ํ•œ๋‹ค. +- ๊ฒ€์ฆ์€ ๊ด€๋กœยท๋งจํ™€ ์นด๋“œ ๋™์‹œ ํ‘œ์‹œ, ๋…๋ฆฝ ํ† ๊ธ€ยท๋‹ซ๊ธฐ, ์Šคํƒ€์ผ ํ†ต์ผ, ์ž๋™๋ฐ˜์˜, ์ƒ๋‹จ ๊ฐœ์ˆ˜ยทchip ๋ฏธํ‘œ์‹œ์™€ ์ง€๋„ ํ•„ํ„ฐ ํšŒ๊ท€๋ฅผ ํฌํ•จํ•œ๋‹ค. +- APIยทDTOยทDB ๋ณ€๊ฒฝ์€ ์—†๋‹ค. + +### 27.1 ๊ตฌํ˜„ ๊ฒฐ๊ณผ + +- `ExmnLayerSettingPanel`์„ ๊ณตํ†ต ์นด๋“œ headerยทํ† ๊ธ€ยท๋‹ซ๊ธฐ ๊ตฌ์กฐ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  ๊ด€๋กœยท๋งจํ™€ ์ƒํƒœ๋ฅผ ๋…๋ฆฝ ๊ด€๋ฆฌํ•œ๋‹ค. +- ์‚ฌ์—…์ฐจ์ˆ˜ยท์ƒํƒœ๋“ฑ๊ธ‰์˜ ๊ธฐ์กด ์ž๋™๋ฐ˜์˜์€ ์œ ์ง€ํ•˜๋ฉฐ ํ† ๊ธ€ OFF์—์„œ๋Š” ๋นˆ ํ•„ํ„ฐ๋กœ ๋‹ค์‹œ ์กฐํšŒํ•œ๋‹ค. 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 03072e28a..dc8b2f69e 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 @@ -273,3 +273,24 @@ - ๋ถ€๋ชจ๊ฐ€ ์žˆ๋Š” ๊ด€๋กœ ํƒญ์€ ํ™œ์„ฑ ์—ฌ๋ถ€์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ `โ†` ๋ฒ„ํŠผ์„ ํ•ญ์ƒ ํ‘œ์‹œํ•ด ์ผ๋ฐ˜ ๊ด€๋กœ์™€ ๊ตฌ๋ถ„ํ•˜๊ณ  ์ง€๋ฐ˜์นจํ•˜ ์ƒ์„ธ๋กœ ๋ณต๊ท€ํ•œ๋‹ค. - ๊ณต์šฉ ์šฐ์ธก ํƒญ์€ ์ตœ๋Œ€ 5๊ฐœ FIFO๋ฅผ ์ ์šฉํ•˜๋˜, ์ œ๊ฑฐ ๋Œ€์ƒ์ด ๋ถ€๋ชจ ์ง€๋ฐ˜์นจํ•˜์ด๋ฉด ๊ทธ ๋ถ€๋ชจ์—์„œ ์—ด๋ฆฐ ์—ฐ๊ณ„ ๊ด€๋กœ๋„ ํ•จ๊ป˜ ์ œ๊ฑฐํ•œ๋‹ค. ์—ฐ๊ณ„ ๊ด€๋กœ๋งŒ ์ œ๊ฑฐ๋  ๋•Œ๋Š” ๋ถ€๋ชจ ์ง€๋ฐ˜์นจํ•˜๋ฅผ ์œ ์ง€ํ•œ๋‹ค. - ๋ณ€๊ฒฝ ๋ฒ”์œ„๋Š” `app/(map-layout)/map/page.tsx`์ด๋ฉฐ BE APIยทDTOยทDB ๊ณ„์•ฝ์€ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๋Š”๋‹ค. + +## 16. 2026-09-01 ์„ ํƒ ๋ ˆ์ด์–ด ์„ค์ • ์ง€๋ฐ˜์นจํ•˜ ์นด๋“œ UI ํ†ต์ผ ๊ณ„ํš + +- ์ž‘์—… ๋ชจ๋“œ: `maintenance` +- ์ด๋ฒˆ ๋‹จ๊ณ„๋Š” ์ž‘์—…๊ณ„ํš๋งŒ ์ˆ˜์ •ํ•˜๊ณ  ์ฝ”๋“œ๋Š” ํ›„์† ์Šน์ธ ํ›„ ๋ณ€๊ฒฝํ•œ๋‹ค. +- `SinkholeLayerSettingPanel`์„ ์นจ์ˆ˜ํ”์ ๋„ ์„ค์ • ์นด๋“œ์™€ ๋™์ผํ•œ compact ์นด๋“œ ๊ตฌ์กฐ๋กœ ์ •๋ฆฌํ•œ๋‹ค. +- ํ—ค๋”์— `์ง€๋ฐ˜์นจํ•˜์ •๋ณด` ์ œ๋ชฉ์„ ํ‘œ์‹œํ•˜๊ณ  ์šฐ์ธก ์•ก์…˜ `div`์— `์ ์šฉ ์—ฌ๋ถ€ ํ† ๊ธ€ โ†’ ๋‹ซ๊ธฐ(ร—)` ์ˆœ์„œ๋กœ ๋ฐฐ์น˜ํ•œ๋‹ค. +- ํ† ๊ธ€ ๊ธฐ๋ณธ๊ฐ’์€ ON์ด๋ฉฐ OFF์—์„œ๋Š” ์—ฐ๋„ ์กฐํšŒ์กฐ๊ฑด์„ ์ œ๊ฑฐํ•˜๋˜ ํ˜„์žฌ ์—ฐ๋„ยท๋ฐ˜๊ฒฝ ์ž…๋ ฅ๊ฐ’์€ ์œ ์ง€ํ•œ๋‹ค. +- ๋‹ซ๊ธฐ๋Š” ์„ค์ • ์นด๋“œ๋งŒ ์ˆจ๊ธฐ๊ณ  ๋ ˆ์ด์–ด์™€ ํ˜„์žฌ ์„ค์ •๊ฐ’์€ ์œ ์ง€ํ•œ๋‹ค. +- ์นจ์ˆ˜ํ”์ ๋„ ์„ค์ •๊ณผ ๋™์ผํ•˜๊ฒŒ ์นด๋“œ header `32px`, ์ œ๋ชฉ `1.15rem`, ๋ณธ๋ฌธ `1.05rem`, ๋ณธ๋ฌธ padding `8px`, SelectBox `62ร—22px`, SelectBox font `1rem`ยท์ขŒ์šฐ padding `4px`๋ฅผ ์ ์šฉํ•œ๋‹ค. +- ์ฃผํ™ฉ ์นด๋“œยท์ œ๋ชฉ ์Šคํƒ€์ผ์€ ์ œ๊ฑฐํ•˜๊ณ  ๊ณตํ†ต border/background/label ์ƒ‰์ƒ์œผ๋กœ ํ†ต์ผํ•œ๋‹ค. +- ๋ฐ˜๊ฒฝ๊ณผ ๋…„๋„์„ค์ •์˜ `โœ“` ์ ์šฉ ๋ฒ„ํŠผ์€ ๋ชจ๋‘ ์œ ์ง€ํ•˜๋ฉฐ SelectBox ์„ ํƒ๋งŒ์œผ๋กœ ์ž๋™ ์ ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค. +- `map/page.tsx`์˜ ์„ ํƒ ๋ ˆ์ด์–ด ์„ค์ • ๊ณตํ†ต header์—์„œ๋Š” ํ™œ์„ฑ ์นด๋“œ ๊ฐœ์ˆ˜์™€ ์นด๋“œ๋ช… pill/chip ๋ชฉ๋ก์„ ์ œ๊ฑฐํ•˜๊ณ  `์„ ํƒ ๋ ˆ์ด์–ด ์„ค์ •` ์ œ๋ชฉ๋งŒ ์œ ์ง€ํ•œ๋‹ค. +- ๊ณตํ†ต ์ƒ๋‹จ ํ‘œ์‹œ ์ œ๊ฑฐ๋Š” ์ง€๋ฐ˜์นจํ•˜ยท์นจ์ˆ˜ํ”์ ๋„ยท์กฐ์‚ฌ์‹œ์„คยท๋…ธํ›„๊ด€๋กœยท์นจ์ˆ˜์‹ฌ ๋ฒ”๋ก€ ๋“ฑ ๋ชจ๋“  ์„ค์ • ์นด๋“œ์— ์ ์šฉํ•œ๋‹ค. +- ๊ฒ€์ฆ์€ ํ† ๊ธ€/๋‹ซ๊ธฐ ๋ฐฐ์น˜, ์นด๋“œยทSelectBox ์Šคํƒ€์ผ ์ผ์น˜, `โœ“` ์ ์šฉ ์œ ์ง€, ์ƒ๋‹จ ๊ฐœ์ˆ˜ยทchip ๋ฏธํ‘œ์‹œ, ๋‹ค์ค‘ ์นด๋“œ ์ˆœ์„œยท์Šคํฌ๋กค ํšŒ๊ท€๋ฅผ ํฌํ•จํ•œ๋‹ค. +- APIยทDTOยทDB ๋ณ€๊ฒฝ์€ ์—†๋‹ค. + +### 16.1 ๊ตฌํ˜„ ๊ฒฐ๊ณผ + +- `SinkholeLayerSettingPanel` compact ์นด๋“œ์— ๊ณตํ†ต headerยทํ† ๊ธ€ยท๋‹ซ๊ธฐ์™€ ์นจ์ˆ˜ํ”์ ๋„ ๊ธฐ์ค€ ์ž…๋ ฅ ์Šคํƒ€์ผ์„ ์ ์šฉํ–ˆ๋‹ค. +- ๋ฐ˜๊ฒฝยท๋…„๋„ `โœ“` ์ ์šฉ ๋ฒ„ํŠผ์€ ์œ ์ง€ํ–ˆ๊ณ  ํ† ๊ธ€ยท๋‹ซ๊ธฐ ์ƒํƒœ๋Š” `map/page.tsx`์—์„œ ๊ด€๋ฆฌํ•œ๋‹ค. From 4cde9b67c1d395f18e123b09e80489f643e7f107 Mon Sep 17 00:00:00 2001 From: ljpark Date: Wed, 2 Sep 2026 18:10:50 +0900 Subject: [PATCH 06/12] =?UTF-8?q?sn:=20util.ts=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ctyFldngPrvtnBiz/ui/CtyFldngPrvtnBizListPage.tsx | 10 +++------- apps/sn/src/features/obsltPpl/lib/obslt-ppl-excel.ts | 2 +- .../sn/src/features/obsltPpl/ui/ObsltPplFormPage.tsx | 2 +- .../sn/src/features/obsltPpl/ui/ObsltPplListPage.tsx | 10 +++------- apps/sn/src/{features/common => shared/util}/util.ts | 12 ++++++++++++ 5 files changed, 20 insertions(+), 16 deletions(-) rename apps/sn/src/{features/common => shared/util}/util.ts (60%) diff --git a/apps/sn/src/features/ctyFldngPrvtnBiz/ui/CtyFldngPrvtnBizListPage.tsx b/apps/sn/src/features/ctyFldngPrvtnBiz/ui/CtyFldngPrvtnBizListPage.tsx index 1068af5a1..12666c3be 100644 --- a/apps/sn/src/features/ctyFldngPrvtnBiz/ui/CtyFldngPrvtnBizListPage.tsx +++ b/apps/sn/src/features/ctyFldngPrvtnBiz/ui/CtyFldngPrvtnBizListPage.tsx @@ -26,7 +26,7 @@ import { type PaginationState, } from "@tanstack/react-table"; -import { formatDate } from "../../common/util"; +import { appendApiSearchParams, formatDate } from "@/shared/util/util"; import { getAllCtyFldngPrvtnBusinesses, getCtyFldngPrvtnBusinesses, @@ -454,9 +454,7 @@ export function CtyFldngPrvtnBizListPage() { ? `${nextSort.key},${nextSort.direction}` : "ctyFldngPrvtnBizSn,desc", }); - Object.entries(condition).forEach(([k, v]) => { - if (v.trim()) query.set(k, v.trim()); - }); + appendApiSearchParams(query, condition); setLoading(true); try { const result = await getCtyFldngPrvtnBusinesses(query); @@ -529,9 +527,7 @@ export function CtyFldngPrvtnBizListPage() { setLoading(true); try { const query = new URLSearchParams(); - Object.entries(applied).forEach(([k, v]) => { - if (v.trim()) query.set(k, v.trim()); - }); + appendApiSearchParams(query, applied); await downloadCtyFldngPrvtnExcel( await getAllCtyFldngPrvtnBusinesses( query, diff --git a/apps/sn/src/features/obsltPpl/lib/obslt-ppl-excel.ts b/apps/sn/src/features/obsltPpl/lib/obslt-ppl-excel.ts index 62ff0630b..6fef56b6d 100644 --- a/apps/sn/src/features/obsltPpl/lib/obslt-ppl-excel.ts +++ b/apps/sn/src/features/obsltPpl/lib/obslt-ppl-excel.ts @@ -1,7 +1,7 @@ import * as XLSX from "xlsx"; import XlsxPopulate from "xlsx-populate/browser/xlsx-populate"; -import { formatDate } from "../../common/util"; +import { formatDate } from "@/shared/util/util"; import type { ObsltPplBusiness, ObsltPplBusinessSaveRequest } from "../model/obslt-ppl-types"; type BusinessType = ObsltPplBusinessSaveRequest["bizSe"]; diff --git a/apps/sn/src/features/obsltPpl/ui/ObsltPplFormPage.tsx b/apps/sn/src/features/obsltPpl/ui/ObsltPplFormPage.tsx index 0cc4631f5..b005b9793 100644 --- a/apps/sn/src/features/obsltPpl/ui/ObsltPplFormPage.tsx +++ b/apps/sn/src/features/obsltPpl/ui/ObsltPplFormPage.tsx @@ -21,7 +21,7 @@ import { Textarea, } from "@common/components"; -import { formatDate } from "../../common/util"; +import { formatDate } from "@/shared/util/util"; import { createObsltPplBusiness, deleteObsltPplBusiness, diff --git a/apps/sn/src/features/obsltPpl/ui/ObsltPplListPage.tsx b/apps/sn/src/features/obsltPpl/ui/ObsltPplListPage.tsx index 541263d84..214eca881 100644 --- a/apps/sn/src/features/obsltPpl/ui/ObsltPplListPage.tsx +++ b/apps/sn/src/features/obsltPpl/ui/ObsltPplListPage.tsx @@ -34,7 +34,7 @@ import { getObsltPplBusinesses, uploadObsltPplExcel, } from "../api/obslt-ppl-api"; -import { formatDate } from "../../common/util"; +import { appendApiSearchParams, formatDate } from "@/shared/util/util"; import { downloadObsltPplExcel, parseObsltPplExcel, @@ -322,9 +322,7 @@ export function ObsltPplListPage() { ? `${nextSort.key},${nextSort.direction}` : "obsltSwagePplBizSn,desc", }); - Object.entries(targetSearch).forEach(([key, value]) => { - if (value.trim()) params.set(key, value.trim()); - }); + appendApiSearchParams(params, targetSearch); setLoading(true); try { @@ -423,9 +421,7 @@ export function ObsltPplListPage() { setLoading(true); try { const params = new URLSearchParams(); - Object.entries(appliedSearch).forEach(([key, value]) => { - if (value.trim()) params.set(key, value.trim()); - }); + appendApiSearchParams(params, appliedSearch); const exportRows = await getAllObsltPplBusinesses( params, sort ? `${sort.key},${sort.direction}` : undefined, diff --git a/apps/sn/src/features/common/util.ts b/apps/sn/src/shared/util/util.ts similarity index 60% rename from apps/sn/src/features/common/util.ts rename to apps/sn/src/shared/util/util.ts index 7686cafc1..3b4c4981e 100644 --- a/apps/sn/src/features/common/util.ts +++ b/apps/sn/src/shared/util/util.ts @@ -1,5 +1,17 @@ import moment from "moment"; +/** ๋นˆ ๊ฐ’๊ณผ ํ™”๋ฉด์˜ ์ „์ฒด ์„ ํƒ๊ฐ’ `0`์„ ์ œ์™ธํ•˜๊ณ  API ๊ฒ€์ƒ‰ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค. */ +export const appendApiSearchParams = ( + params: URLSearchParams, + search: Readonly>, +) => { + Object.entries(search).forEach(([key, value]) => { + const normalized = value.trim(); + if (!normalized || normalized === "0") return; + params.set(key, normalized); + }); +}; + /** 8์ž๋ฆฌ ๋˜๋Š” ๊ตฌ๋ถ„์ž๊ฐ€ ์„ž์ธ ๋‚ ์งœ ๋ฌธ์ž์—ด์„ YYYY-MM-DD ํ˜•์‹์œผ๋กœ ์ •๊ทœํ™”ํ•œ๋‹ค. */ export const formatDate = (value?: string | null) => { if (!value) return ""; From 5da89e8ee73ffad29d5e1782e8048136e21d4519 Mon Sep 17 00:00:00 2001 From: ljpark Date: Thu, 3 Sep 2026 17:09:58 +0900 Subject: [PATCH 07/12] =?UTF-8?q?sn:=20appendApiSearchParams()=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inftwtResult/InftwtResultPopupPage.tsx | 18 +++-- .../PipeAnalysisDetailPopupPage.tsx | 24 ++++--- .../pipeAnalysis/PipeAnalysisPopupPage.tsx | 10 +-- .../rdiiResult/RdiiResultPopupPage.tsx | 18 +++-- .../001-fe-SFR-011-008-์นจ์ˆ˜์˜ˆ์ธกํ˜„ํ™ฉ-์ž‘์—…๊ณ„ํš.md | 8 +++ .../lib/fldng-predc-prst-excel.ts | 66 +++++++++++++++++++ .../ui/FldngPredcPrstPopupContent.tsx | 56 ++++++++++------ 7 files changed, 144 insertions(+), 56 deletions(-) create mode 100644 apps/sn/src/features/fldngPredcPrst/lib/fldng-predc-prst-excel.ts diff --git a/apps/sn/app/(map-layout)/map/components/inftwtResult/InftwtResultPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/inftwtResult/InftwtResultPopupPage.tsx index f6020437d..a0cf5765e 100644 --- a/apps/sn/app/(map-layout)/map/components/inftwtResult/InftwtResultPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/inftwtResult/InftwtResultPopupPage.tsx @@ -13,6 +13,7 @@ import { useAlertStore } from "@/shared/lib"; import { Button } from "@common/components"; import { getData } from "@/components/lib/api"; +import { appendApiSearchParams } from "@/shared/util/util"; import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpvSggPrcsFcltSelectBox"; import { formatDisplayData } from "@/components/lib/utils/formatDisplayValue"; import useStore from "@/store/useStore"; @@ -132,16 +133,13 @@ export default function InftwtResultPopupPage() { : "inftwtAnlsSn,desc", ); - if (nextFilters.aplcnYmFrom) - queryParams.set("aplcnYmFrom", nextFilters.aplcnYmFrom); - if (nextFilters.aplcnYmTo) - queryParams.set("aplcnYmTo", nextFilters.aplcnYmTo); - if (nextFilters.ctpvCd !== "0") - queryParams.set("ctpvCd", nextFilters.ctpvCd); - if (nextFilters.sggCd !== "0") - queryParams.set("sggCd", nextFilters.sggCd); - if (nextFilters.swtrmtFcltNm.trim()) - queryParams.set("swtrmtFcltNm", nextFilters.swtrmtFcltNm.trim()); + appendApiSearchParams(queryParams, { + aplcnYmFrom: nextFilters.aplcnYmFrom, + aplcnYmTo: nextFilters.aplcnYmTo, + ctpvCd: nextFilters.ctpvCd, + sggCd: nextFilters.sggCd, + swtrmtFcltNm: nextFilters.swtrmtFcltNm, + }); const response = await getData(`v1/inftwtAnls?${queryParams.toString()}`); const content = Array.isArray(response?.content) ? response.content : []; diff --git a/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisDetailPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisDetailPopupPage.tsx index 44a32f067..969f472d8 100644 --- a/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisDetailPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisDetailPopupPage.tsx @@ -6,6 +6,7 @@ import { Button, Input } from "@common/components"; import { StandardPopupLayout } from "@common/business_components"; import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel"; import { getData } from "@/components/lib/api"; +import { appendApiSearchParams } from "@/shared/util/util"; import LoadingOverlay from "@/components/ui/LoadingBar"; import useStore from "@/store/useStore"; import { useAlertStore } from "@/shared/lib"; @@ -131,19 +132,16 @@ export default function PipeAnalysisDetailPopupPage() { size: String(size), }); - if (nextFilters.ctpvCd !== "0") params.set("ctpvCd", nextFilters.ctpvCd); - if (nextFilters.sggCd !== "0") params.set("sggCd", nextFilters.sggCd); - if (nextFilters.swtrmtZoneNm !== "0") - params.set("swtrmtZoneNm", nextFilters.swtrmtZoneNm); - if (baseElapsedYearBucket) - params.set("elapsedYearBucket", baseElapsedYearBucket); - if (nextFilters.materialCode !== "0") - params.set("materialCode", nextFilters.materialCode); - if (nextFilters.useCd !== "0") params.set("useCd", nextFilters.useCd); - if (nextFilters.diameterFrom) - params.set("diameterFrom", nextFilters.diameterFrom); - if (nextFilters.diameterTo) - params.set("diameterTo", nextFilters.diameterTo); + appendApiSearchParams(params, { + ctpvCd: nextFilters.ctpvCd, + sggCd: nextFilters.sggCd, + swtrmtZoneNm: nextFilters.swtrmtZoneNm, + elapsedYearBucket: baseElapsedYearBucket, + materialCode: nextFilters.materialCode, + useCd: nextFilters.useCd, + diameterFrom: nextFilters.diameterFrom, + diameterTo: nextFilters.diameterTo, + }); if (scope.regionCode) params.set("scopeRegionCode", scope.regionCode); const response = await getData( diff --git a/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisPopupPage.tsx index 2e0791e92..725921045 100644 --- a/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisPopupPage.tsx @@ -14,6 +14,7 @@ import { StandardPopupLayout } from "@common/business_components"; import UiPageNations from "@common/business_components/ui/table/uiPageNations"; import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel"; import { getData } from "@/components/lib/api"; +import { appendApiSearchParams } from "@/shared/util/util"; import LoadingOverlay from "@/components/ui/LoadingBar"; import useStore from "@/store/useStore"; import { useAlertStore } from "@/shared/lib"; @@ -144,10 +145,11 @@ export default function PipeAnalysisPopupPage() { analysisType: nextFilters.analysisType, }); - if (nextFilters.ctpvCd !== "0") params.set("ctpvCd", nextFilters.ctpvCd); - if (nextFilters.sggCd !== "0") params.set("sggCd", nextFilters.sggCd); - if (nextFilters.swtrmtZoneNm !== "0") - params.set("swtrmtZoneNm", nextFilters.swtrmtZoneNm); + appendApiSearchParams(params, { + ctpvCd: nextFilters.ctpvCd, + sggCd: nextFilters.sggCd, + swtrmtZoneNm: nextFilters.swtrmtZoneNm, + }); if (nextFilters.scopeRegionCode) params.set("scopeRegionCode", nextFilters.scopeRegionCode); diff --git a/apps/sn/app/(map-layout)/map/components/rdiiResult/RdiiResultPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/rdiiResult/RdiiResultPopupPage.tsx index 8c2521ada..06641fed8 100644 --- a/apps/sn/app/(map-layout)/map/components/rdiiResult/RdiiResultPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/rdiiResult/RdiiResultPopupPage.tsx @@ -10,6 +10,7 @@ import { import { Button } from "@common/components"; import { getData } from "@/components/lib/api"; +import { appendApiSearchParams } from "@/shared/util/util"; import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel"; import { fetchAllPages } from "@/shared/lib/fetch-all-pages"; import { useAlertStore } from "@/shared/lib"; @@ -134,16 +135,13 @@ export default function RdiiResultPopupPage() { : "rainInfwtAnlsSn,desc", ); - if (nextFilters.aplcnYmFrom) - queryParams.set("aplcnYmFrom", nextFilters.aplcnYmFrom); - if (nextFilters.aplcnYmTo) - queryParams.set("aplcnYmTo", nextFilters.aplcnYmTo); - if (nextFilters.ctpvCd !== "0") - queryParams.set("ctpvCd", nextFilters.ctpvCd); - if (nextFilters.sggCd !== "0") - queryParams.set("sggCd", nextFilters.sggCd); - if (nextFilters.swtrmtFcltNm.trim()) - queryParams.set("swtrmtFcltNm", nextFilters.swtrmtFcltNm.trim()); + appendApiSearchParams(queryParams, { + aplcnYmFrom: nextFilters.aplcnYmFrom, + aplcnYmTo: nextFilters.aplcnYmTo, + ctpvCd: nextFilters.ctpvCd, + sggCd: nextFilters.sggCd, + swtrmtFcltNm: nextFilters.swtrmtFcltNm, + }); const response = await getData(`v1/rdiiAnls?${queryParams.toString()}`); const content = Array.isArray(response?.content) ? response.content : []; diff --git a/apps/sn/docs/์ž‘์—…๊ณ„ํš/SFR-011-008-์นจ์ˆ˜์˜ˆ์ธก/001-fe-SFR-011-008-์นจ์ˆ˜์˜ˆ์ธกํ˜„ํ™ฉ-์ž‘์—…๊ณ„ํš.md b/apps/sn/docs/์ž‘์—…๊ณ„ํš/SFR-011-008-์นจ์ˆ˜์˜ˆ์ธก/001-fe-SFR-011-008-์นจ์ˆ˜์˜ˆ์ธกํ˜„ํ™ฉ-์ž‘์—…๊ณ„ํš.md index 17410affd..ebf869639 100644 --- a/apps/sn/docs/์ž‘์—…๊ณ„ํš/SFR-011-008-์นจ์ˆ˜์˜ˆ์ธก/001-fe-SFR-011-008-์นจ์ˆ˜์˜ˆ์ธกํ˜„ํ™ฉ-์ž‘์—…๊ณ„ํš.md +++ b/apps/sn/docs/์ž‘์—…๊ณ„ํš/SFR-011-008-์นจ์ˆ˜์˜ˆ์ธก/001-fe-SFR-011-008-์นจ์ˆ˜์˜ˆ์ธกํ˜„ํ™ฉ-์ž‘์—…๊ณ„ํš.md @@ -69,3 +69,11 @@ - 2026-08-18 ๋ชฉ๋ก ๊ฐ•์ˆ˜๋Ÿ‰์„ ์†Œ์ˆ˜์  ํ•œ ์ž๋ฆฌ๋กœ, ํ—ค๋”๋ฅผ `์ดˆ๊ณผ๋นˆ๋„๋…„๋„`๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  ๊ฐ’์˜ `๋…„` ์ ‘๋ฏธ์‚ฌ๋ฅผ ์ œ๊ฑฐํ–ˆ๋‹ค. - ๊ธฐ์กด `.next` ์‚ฐ์ถœ๋ฌผ์„ ์žฌ์ƒ์„ฑํ•œ ๋’ค `pnpm --filter sn run build`๋ฅผ ์ตœ์ข… ํ†ต๊ณผํ–ˆ๋‹ค. - TypeScript ์ •์ ๊ฒ€์‚ฌ๋Š” ์ €์žฅ์†Œ ๊ธฐ์กด `ignoreDeprecations` ์„ค์ •๊ณผ ์‹คํ–‰ ์ค‘ ์‚ฐ์ถœ๋ฌผ ์ž ๊ธˆ ๋•Œ๋ฌธ์— ์™„๋ฃŒํ•˜์ง€ ๋ชปํ–ˆ๊ณ , Next ๋นŒ๋“œ๋Š” ์ตœ์ ํ™” ๋‹จ๊ณ„ ์žฅ๊ธฐ ๋Œ€๊ธฐ ํ›„ ์ค‘๋‹จํ–ˆ๋‹ค. + +## Excel ํ”„๋ก ํŠธ ์ƒ์„ฑ ์ „ํ™˜ ๊ฒฐ๊ณผ (2026-09-03) + +- ํ˜„์žฌ ์ ์šฉ ๊ฒ€์ƒ‰์กฐ๊ฑด์œผ๋กœ `/api/v1/fldng/predc/prst`๋ฅผ 500๊ฑด ๋‹จ์œ„ ์ „์ฒด ํŽ˜์ด์ง€ ์กฐํšŒํ•œ๋‹ค. +- ๊ณตํ†ต `fetchAllPages`๋กœ `totalElements`์™€ ์‹ค์ œ ์ˆ˜์ง‘ ๊ฑด์ˆ˜ ์ผ์น˜๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค. +- `fldng-predc-prst-excel.ts`๊ฐ€ ํ™”๋ฉด ์—ด ์ˆœ์„œ์˜ ๋‹จ์ผ ์‹œํŠธ `์นจ์ˆ˜์˜ˆ์ธกํ˜„ํ™ฉ.xlsx`๋ฅผ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ƒ์„ฑํ•œ๋‹ค. +- ์„œ๋ฒ„ `/prst/excel` Blob ํ˜ธ์ถœ๊ณผ `getBlobData`, `fileDownload` ์‚ฌ์šฉ์„ ์ œ๊ฑฐํ–ˆ๋‹ค. +- ๋…๋ฆฝ TypeScript `--noEmit` ๊ฒ€์ฆ์„ ํ†ต๊ณผํ–ˆ๋‹ค. diff --git a/apps/sn/src/features/fldngPredcPrst/lib/fldng-predc-prst-excel.ts b/apps/sn/src/features/fldngPredcPrst/lib/fldng-predc-prst-excel.ts new file mode 100644 index 000000000..4408e8e07 --- /dev/null +++ b/apps/sn/src/features/fldngPredcPrst/lib/fldng-predc-prst-excel.ts @@ -0,0 +1,66 @@ +import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel"; + +export type FloodPredictionExcelRow = { + ctpvNm: string; + sggNm: string; + analysisDateTime: string; + rainBeginningDateTime: string; + observedAccumulatedRainfall: number | null; + predictionHour: number | null; + predictedAccumulatedRainfall: number | null; + exceededFrequencyYear: number | null; + floodRiskGrade: string; +}; + +const HEADERS: Record = { + rowNo: "์ˆœ๋ฒˆ", + ctpvNm: "์‹œ๋„", + sggNm: "์‹œ๊ตฐ๊ตฌ", + analysisDateTime: "๋ถ„์„์‹œ๊ฐ", + rainBeginningDateTime: "๊ฐ•์šฐ ์‹œ์ž‘์‹œ๊ฐ", + observedAccumulatedRainfall: "๊ด€์ธก ๋ˆ„์ ๊ฐ•์ˆ˜๋Ÿ‰(mm)", + predictionHour: "์˜ˆ์ธก์‹œ๊ฐ„", + predictedAccumulatedRainfall: "์˜ˆ์ธก ๋ˆ„์ ๊ฐ•์ˆ˜๋Ÿ‰(mm)", + exceededFrequencyYear: "์ดˆ๊ณผ๋นˆ๋„(๋…„)", + floodRiskGrade: "์นจ์ˆ˜์œ„ํ—˜๋„", +}; + +/** yyyyMMddHHmm ํ˜•์‹์˜ API ์‹œ๊ฐ์„ Excel ํ‘œ์‹œ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค. */ +const formatDateTime = (value: string) => + /^\d{12}$/.test(value) + ? `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)} ${value.slice(8, 10)}:${value.slice(10, 12)}` + : value || "-"; + +/** ์ ์šฉ ์กฐํšŒ์กฐ๊ฑด์˜ ์ „์ฒด ์นจ์ˆ˜์˜ˆ์ธกํ˜„ํ™ฉ์„ ๋ธŒ๋ผ์šฐ์ €์—์„œ ํ•œ ์‹œํŠธ XLSX๋กœ ์ƒ์„ฑํ•œ๋‹ค. */ +export const downloadFloodPredictionStatusExcel = async ( + rows: FloodPredictionExcelRow[], +) => { + const excelRows = rows.map((row, index) => ({ + rowNo: index + 1, + ctpvNm: row.ctpvNm || "-", + sggNm: row.sggNm || "-", + analysisDateTime: formatDateTime(row.analysisDateTime), + rainBeginningDateTime: formatDateTime(row.rainBeginningDateTime), + observedAccumulatedRainfall: + row.observedAccumulatedRainfall == null + ? "-" + : Number(row.observedAccumulatedRainfall.toFixed(1)), + predictionHour: + row.predictionHour == null ? "-" : `${row.predictionHour}์‹œ๊ฐ„ ํ›„`, + predictedAccumulatedRainfall: + row.predictedAccumulatedRainfall == null + ? "-" + : Number(row.predictedAccumulatedRainfall.toFixed(1)), + exceededFrequencyYear: + row.exceededFrequencyYear == null ? "-" : row.exceededFrequencyYear, + floodRiskGrade: row.floodRiskGrade || "-", + })); + + await downloadExcelWithKoreanHeaders({ + data: excelRows as unknown as JSON[], + fileName: "์นจ์ˆ˜์˜ˆ์ธกํ˜„ํ™ฉ.xlsx", + sheetNameArray: ["์นจ์ˆ˜์˜ˆ์ธกํ˜„ํ™ฉ"], + headerRange: "A1:J1", + headerMapping: HEADERS, + }); +}; diff --git a/apps/sn/src/features/fldngPredcPrst/ui/FldngPredcPrstPopupContent.tsx b/apps/sn/src/features/fldngPredcPrst/ui/FldngPredcPrstPopupContent.tsx index 93c588c75..b16991eb1 100644 --- a/apps/sn/src/features/fldngPredcPrst/ui/FldngPredcPrstPopupContent.tsx +++ b/apps/sn/src/features/fldngPredcPrst/ui/FldngPredcPrstPopupContent.tsx @@ -8,8 +8,8 @@ import { } from "@common/business_components"; import { Button } from "@common/components"; -import { getBlobData, getData, getRawJson } from "@/components/lib/api"; -import { fileDownload } from "@/components/lib/utils/fileDownload"; +import { getData, getRawJson } from "@/components/lib/api"; +import { appendApiSearchParams } from "@/shared/util/util"; import LoadingOverlay from "@/components/ui/LoadingBar"; import { windowPopupDefinitions } from "@/features/windowPopup/model/popupDefinitions"; import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types"; @@ -18,6 +18,8 @@ import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel"; import { useAlertStore } from "@/shared/lib"; import useStore from "@/store/useStore"; import { createPredictionAnalysisTimeRange } from "../model/analysisTimeRange"; +import { fetchAllPages } from "@/shared/lib/fetch-all-pages"; +import { downloadFloodPredictionStatusExcel } from "../lib/fldng-predc-prst-excel"; type ApiRow = { sggCd: string; @@ -55,6 +57,7 @@ type Filters = { }; const PAGE_SIZE = 10; +const EXCEL_DOWNLOAD_PAGE_SIZE = 500; const MAP_FREQUENCIES = new Set([30, 50, 80, 100]); const FREQUENCY_OPTIONS = [30, 50, 80, 100] as const; @@ -118,7 +121,7 @@ const buildInitialFilters = (): Filters => { rainBgngTo: "", analysisFrom: toDateTimeLocal(range.analysisFrom), analysisTo: toDateTimeLocal(range.analysisTo), - frequencyYear: "", + frequencyYear: "0", }; }; @@ -141,21 +144,21 @@ const formatNumber = (value: number | null, digits: number) => : Number(value).toFixed(digits); /** ํ™”๋ฉด ์กฐํšŒ์กฐ๊ฑด์„ ๋ชฉ๋กยทExcel API ์ฟผ๋ฆฌ๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค. */ -const buildQuery = (filters: Filters, page?: number) => { +const buildQuery = (filters: Filters, page?: number, size = PAGE_SIZE) => { const params = new URLSearchParams(); if (page !== undefined) { params.set("page", String(page - 1)); - params.set("size", String(PAGE_SIZE)); + params.set("size", String(size)); } - if (filters.ctpvCd !== "0") params.set("ctpvCd", filters.ctpvCd.slice(0, 2)); - if (filters.sggCd !== "0") params.set("sggCd", filters.sggCd.slice(0, 5)); - ( - ["rainBgngFrom", "rainBgngTo", "analysisFrom", "analysisTo"] as const - ).forEach((key) => { - const value = apiDate(filters[key]); - if (value) params.set(key, value); + appendApiSearchParams(params, { + ctpvCd: filters.ctpvCd === "0" ? "0" : filters.ctpvCd.slice(0, 2), + sggCd: filters.sggCd === "0" ? "0" : filters.sggCd.slice(0, 5), + rainBgngFrom: apiDate(filters.rainBgngFrom), + rainBgngTo: apiDate(filters.rainBgngTo), + analysisFrom: apiDate(filters.analysisFrom), + analysisTo: apiDate(filters.analysisTo), + frequencyYear: filters.frequencyYear, }); - if (filters.frequencyYear) params.set("frequencyYear", filters.frequencyYear); return params.toString(); }; @@ -176,7 +179,7 @@ const validateFilters = (filters: Filters) => { return "๋ถ„์„์‹œ๊ฐ์˜ ์‹œ์ž‘์€ ์ข…๋ฃŒ๋ณด๋‹ค ๋Šฆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."; } if ( - filters.frequencyYear && + filters.frequencyYear != "0" && !FREQUENCY_OPTIONS.includes( Number(filters.frequencyYear) as 30 | 50 | 80 | 100, ) @@ -389,10 +392,25 @@ export default function FldngPredcPrstPopupContent() { } setLoading(true); try { - const blob = await getBlobData( - `/sewerage-service/api/v1/fldng/predc/prst/excel?${buildQuery(appliedFilters)}`, - ); - fileDownload(blob, "์นจ์ˆ˜์˜ˆ์ธกํ˜„ํ™ฉ.xlsx"); + const result = await fetchAllPages< + { content: ApiRow[]; totalElements: number }, + ApiRow + >({ + batchSize: EXCEL_DOWNLOAD_PAGE_SIZE, + fetchPage: async (pageIndex, size) => { + const response = await getRawJson( + `/sewerage-service/api/v1/fldng/predc/prst?${buildQuery(appliedFilters, pageIndex + 1, size)}`, + ); + const payload = response?.data ?? response; + return { + content: Array.isArray(payload?.content) ? payload.content : [], + totalElements: Number(payload?.totalElements ?? 0), + }; + }, + getItems: (response) => response.content, + getTotal: (response) => response.totalElements, + }); + await downloadFloodPredictionStatusExcel(result.items); } catch (error) { console.error("์นจ์ˆ˜์˜ˆ์ธกํ˜„ํ™ฉ Excel ๋‹ค์šด๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜:", error); alert("์นจ์ˆ˜์˜ˆ์ธกํ˜„ํ™ฉ Excel ๋‹ค์šด๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); @@ -519,7 +537,7 @@ export default function FldngPredcPrstPopupContent() { })) } > - + {FREQUENCY_OPTIONS.map((frequency) => (
- - +
+ + +
{notice && ( diff --git a/apps/sn/src/features/obsltPpl/ui/ObsltPplListPage.tsx b/apps/sn/src/features/obsltPpl/ui/ObsltPplListPage.tsx index 214eca881..d27f76b49 100644 --- a/apps/sn/src/features/obsltPpl/ui/ObsltPplListPage.tsx +++ b/apps/sn/src/features/obsltPpl/ui/ObsltPplListPage.tsx @@ -619,17 +619,19 @@ export function ObsltPplListPage() { />
- - +
+ + +
From 10d3fc096a298a0068c9822a254143d302f82d69 Mon Sep 17 00:00:00 2001 From: ljpark Date: Fri, 4 Sep 2026 11:00:38 +0900 Subject: [PATCH 10/12] =?UTF-8?q?sn:=20=EC=8B=9C=EB=8F=84=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=EC=9D=BC=20=EB=95=8C,=20=EC=8B=9C=EA=B5=B0=EA=B5=AC?= =?UTF-8?q?=20=EB=AA=A9=EB=A1=9D=20=EB=B9=84=EC=9A=B0=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../map/components/agedPipe/AgedPipePopupPage.tsx | 2 +- .../map/components/fldngHstry/FldngHstryPopupPage.tsx | 2 +- .../map/components/floodDngrMap/FloodDngrMapPopupPage.tsx | 2 +- .../map/components/pipeAnalysis/PipeAnalysisPopupPage.tsx | 2 +- .../map/components/sbmrgDngrMap/SbmrgDngrMapPopupPage.tsx | 2 +- .../map/components/sinkhole/SinkholePopupPage.tsx | 2 +- .../map/components/waterRisk/WaterRiskPopupPage.tsx | 4 ++-- apps/sn/app/(with-base-layout)/oper/dredging/page.tsx | 2 +- .../sn/app/html/(map-layout)/aged-pipe/AgedPipeListModal.tsx | 2 +- .../fldngPredcPrst/ui/FldngPredcPrstPopupContent.tsx | 2 +- apps/sn/src/features/obsltPpl/lib/obslt-ppl-region.ts | 5 ++++- 11 files changed, 15 insertions(+), 12 deletions(-) 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/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/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/pipeAnalysis/PipeAnalysisPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisPopupPage.tsx index 725921045..e657c6f2d 100644 --- a/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisPopupPage.tsx @@ -388,7 +388,7 @@ export default function PipeAnalysisPopupPage() { const sggOptions = useMemo( () => draftFilters.ctpvCd === "0" - ? sgg + ? [] : sgg.filter((item) => String(item.val).startsWith(draftFilters.ctpvCd.slice(0, 2)), ), diff --git a/apps/sn/app/(map-layout)/map/components/sbmrgDngrMap/SbmrgDngrMapPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/sbmrgDngrMap/SbmrgDngrMapPopupPage.tsx index 4cb3987fe..a6e2190e4 100644 --- a/apps/sn/app/(map-layout)/map/components/sbmrgDngrMap/SbmrgDngrMapPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/sbmrgDngrMap/SbmrgDngrMapPopupPage.tsx @@ -85,7 +85,7 @@ export default function SbmrgDngrMapPopupPage() { 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/sinkhole/SinkholePopupPage.tsx b/apps/sn/app/(map-layout)/map/components/sinkhole/SinkholePopupPage.tsx index c837cefdf..c5268b167 100644 --- a/apps/sn/app/(map-layout)/map/components/sinkhole/SinkholePopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/sinkhole/SinkholePopupPage.tsx @@ -197,7 +197,7 @@ export default function SinkholePopupPage() { 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/waterRisk/WaterRiskPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/waterRisk/WaterRiskPopupPage.tsx index 2077532aa..6758738f5 100644 --- a/apps/sn/app/(map-layout)/map/components/waterRisk/WaterRiskPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/waterRisk/WaterRiskPopupPage.tsx @@ -366,7 +366,7 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) { areaOptions .filter( (option) => - draftResultFilters.ctpvCd === "ALL" || + draftResultFilters.ctpvCd !== "ALL" && option.ctpvCd === draftResultFilters.ctpvCd, ) .forEach((option) => { @@ -382,7 +382,7 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) { () => areaOptions.filter( (option) => - (draftResultFilters.ctpvCd === "ALL" || + (draftResultFilters.ctpvCd !== "ALL" && option.ctpvCd === draftResultFilters.ctpvCd) && (draftResultFilters.sggCd === "ALL" || option.sggCd === draftResultFilters.sggCd), diff --git a/apps/sn/app/(with-base-layout)/oper/dredging/page.tsx b/apps/sn/app/(with-base-layout)/oper/dredging/page.tsx index aa9e18396..7e041a273 100644 --- a/apps/sn/app/(with-base-layout)/oper/dredging/page.tsx +++ b/apps/sn/app/(with-base-layout)/oper/dredging/page.tsx @@ -361,7 +361,7 @@ const Page = () => { /** ์„ ํƒํ•œ ์‹œ๋„ ์ฝ”๋“œ์™€ ์•ž ๋‘ ์ž๋ฆฌ๊ฐ€ ๊ฐ™์€ ์‹œ๊ตฐ๊ตฌ๋งŒ ํ›„๋ณด๋กœ ์ œ๊ณตํ•œ๋‹ค. */ const availableSggOptions = useMemo(() => { - if (ctpvCd === "0") return sggRegionOptions; + if (ctpvCd === "0") return []; const ctpvPrefix = ctpvCd.slice(0, 2); return sggRegionOptions.filter((option) => String(option.val).startsWith(ctpvPrefix), diff --git a/apps/sn/app/html/(map-layout)/aged-pipe/AgedPipeListModal.tsx b/apps/sn/app/html/(map-layout)/aged-pipe/AgedPipeListModal.tsx index a5c90e184..45a04331b 100644 --- a/apps/sn/app/html/(map-layout)/aged-pipe/AgedPipeListModal.tsx +++ b/apps/sn/app/html/(map-layout)/aged-pipe/AgedPipeListModal.tsx @@ -41,7 +41,7 @@ export default function AgedPipeListModal({ const { ctpv, sgg } = useStore(); const lastClickRef = useRef<{ index: number; clickedAt: number } | null>(null); const sggOptions = filters.ctpvCd === "0" - ? sgg + ? [] : sgg.filter((item) => String(item.val).startsWith(filters.ctpvCd.slice(0, 2))); const handleRowClick = (index?: number) => { diff --git a/apps/sn/src/features/fldngPredcPrst/ui/FldngPredcPrstPopupContent.tsx b/apps/sn/src/features/fldngPredcPrst/ui/FldngPredcPrstPopupContent.tsx index b16991eb1..38abae35b 100644 --- a/apps/sn/src/features/fldngPredcPrst/ui/FldngPredcPrstPopupContent.tsx +++ b/apps/sn/src/features/fldngPredcPrst/ui/FldngPredcPrstPopupContent.tsx @@ -240,7 +240,7 @@ export default function FldngPredcPrstPopupContent() { const sggOptions = useMemo( () => filters.ctpvCd === "0" - ? sgg + ? [] : sgg.filter((item) => String(item.val).startsWith(filters.ctpvCd.slice(0, 2)), ), diff --git a/apps/sn/src/features/obsltPpl/lib/obslt-ppl-region.ts b/apps/sn/src/features/obsltPpl/lib/obslt-ppl-region.ts index 2fc38854e..46547c757 100644 --- a/apps/sn/src/features/obsltPpl/lib/obslt-ppl-region.ts +++ b/apps/sn/src/features/obsltPpl/lib/obslt-ppl-region.ts @@ -40,12 +40,15 @@ export const buildObsltPplRepresentativeSggOptions = ( sggOptions: RegionOption[], ctpvCd: string, ) => { + // ์‹œ๋„๊ฐ€ ์ „์ฒด(0)์ด๋ฉด ์‹œ๊ตฐ๊ตฌ๋ฅผ ์„ ํƒํ•  ์ˆ˜ ์—†๋„๋ก ๋ชฉ๋ก์„ ๋น„์šด๋‹ค. + if (ctpvCd === "0") return []; + const representativeMap = new Map(); sggOptions .filter((option) => { const optionCode = String(option.val ?? "").trim(); - return ctpvCd === "0" || optionCode.startsWith(ctpvCd.slice(0, 2)); + return optionCode.startsWith(ctpvCd.slice(0, 2)); }) .forEach((option) => { const representativeName = normalizeObsltPplSigunName(option.text); From 9067e645b30d1f11951f019fa4f92d3988512ba0 Mon Sep 17 00:00:00 2001 From: ljpark Date: Fri, 4 Sep 2026 14:33:25 +0900 Subject: [PATCH 11/12] =?UTF-8?q?sn:=20=EC=A0=95=EB=B3=B4=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EA=B8=B0=EB=B3=B8=EC=A0=95=EB=B3=B4=20=EA=B5=AC?= =?UTF-8?q?=EB=B6=84=EB=9E=80=20=ED=99=94=EB=A9=B4=EC=9D=B4=20=EC=A4=84?= =?UTF-8?q?=EC=97=88=EC=9D=84=20=EB=95=8C,=20=EB=94=94=EC=9E=90=EC=9D=B8?= =?UTF-8?q?=20=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/(map-layout)/map/searchModal/basicTab.tsx | 2 +- apps/sn/app/(map-layout)/map/searchModal/fldngWtlvMap.tsx | 4 ++-- apps/sn/app/(map-layout)/map/searchModal/stofcbr.tsx | 4 ++-- apps/sn/app/(map-layout)/map/searchModal/swageMhl.tsx | 4 ++-- apps/sn/app/(map-layout)/map/searchModal/swagePmstn.tsx | 4 ++-- apps/sn/app/(map-layout)/map/searchModal/swagePpl.tsx | 4 ++-- .../sn/app/(map-layout)/map/searchModal/swagePrcsFclt.tsx | 4 ++-- .../sn/app/(map-layout)/map/searchModal/swagePrcsZone.tsx | 4 ++-- .../sn/app/html/(map-layout)/map/searchModal/basicTab.tsx | 8 ++++---- .../src/ui/standard_data_table/index.tsx | 2 +- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/apps/sn/app/(map-layout)/map/searchModal/basicTab.tsx b/apps/sn/app/(map-layout)/map/searchModal/basicTab.tsx index 769c33036..0d1ebe18d 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/basicTab.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/basicTab.tsx @@ -55,7 +55,7 @@ const BasicTab: React.FC = () => { selectContentClass="z-[101]" />
-
+
{basicType === "tb_ms_swage_ppl" && } {/* ํ•˜์ˆ˜๊ด€๋กœ */} {basicType === "tb_il_swage_prcs_fclt" && }{" "} {/** ํ•˜์ˆ˜์ฒ˜๋ฆฌ์‹œ์„ค */} diff --git a/apps/sn/app/(map-layout)/map/searchModal/fldngWtlvMap.tsx b/apps/sn/app/(map-layout)/map/searchModal/fldngWtlvMap.tsx index 35d08b98f..47248ab07 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/fldngWtlvMap.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/fldngWtlvMap.tsx @@ -23,8 +23,8 @@ const FldngWtlvMap: React.FC = () => { const { handleSearch, handleResetSearch } = useSearchModal(); return ( -
-
+
+
{ const { handleSearch, handleResetSearch } = useSearchModal(); return ( -
-
+
+
{/* ์‹œ๋„, ์‹œ๊ตฐ๊ตฌ */} { const { handleSearch, handleResetSearch } = useSearchModal(); return ( -
-
+
+
{ const { handleSearch, handleResetSearch } = useSearchModal(); return ( -
-
+
+
{/* ์‹œ๋„, ์‹œ๊ตฐ๊ตฌ */} { const { handleSearch, handleResetSearch } = useSearchModal(); return ( -
-
+
+
{ const { handleSearch, handleResetSearch } = useSearchModal(); return ( -
-
+
+
{ const { handleSearch, handleResetSearch } = useSearchModal(); return ( -
-
+
+
{ const { basicType, setBasicType } = useSearchModalStore(); return ( -
+
{/* ๊ตฌ๋ถ„ */} -
+
{ />
- {/* ์กฐํšŒ ์กฐ๊ฑด */} + {/* ์กฐํšŒ ์กฐ๊ฑด: ๊ตฌ๋ถ„ ์„ ํƒ๊ณผ ๊ฐ™์€ ํ–‰์— ๋ฐฐ์น˜ */} {basicType === "tb_ms_swage_ppl" && ()} {/* ํ•˜์ˆ˜๊ด€๋กœ */} {basicType === "tb_il_swage_prcs_fclt" && ()} {/* ํ•˜์ˆ˜์ฒ˜๋ฆฌ์‹œ์„ค */} {basicType === "tb_ms_swage_prcs_zone" && ()} {/* ํ•˜์ˆ˜์ฒ˜๋ฆฌ๊ตฌ์—ญ */} @@ -63,4 +63,4 @@ const BasicTab: React.FC = () => { ); }; -export default React.memo(BasicTab); \ No newline at end of file +export default React.memo(BasicTab); diff --git a/packages/common/business_components/src/ui/standard_data_table/index.tsx b/packages/common/business_components/src/ui/standard_data_table/index.tsx index 0753d8f23..f044b851c 100644 --- a/packages/common/business_components/src/ui/standard_data_table/index.tsx +++ b/packages/common/business_components/src/ui/standard_data_table/index.tsx @@ -277,7 +277,7 @@ function StandardDataTable({
From aa5a71ae6818ea240dc3147b1c6c76eb271cc79a Mon Sep 17 00:00:00 2001 From: ljpark Date: Fri, 4 Sep 2026 14:52:11 +0900 Subject: [PATCH 12/12] =?UTF-8?q?sn:=20=EC=B9=A8=EC=9E=85=EC=88=98,=20RDII?= =?UTF-8?q?=20=EC=A0=9C=EB=AA=A9=EB=9E=80=20=EC=A1=B0=EA=B8=88=20=EB=84=93?= =?UTF-8?q?=EA=B2=8C=20=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../popupComponent/chart/infilChart.tsx | 2 +- .../popupComponent/chart/rdiiChart.tsx | 2 +- .../(with-base-layout)/ii/irsetting/page.tsx | 1 + .../app/(with-base-layout)/ii/rdii/page.tsx | 1 + apps/sn/app/components/ui/fileUploadForm.tsx | 8 +++---- .../src/ui/standard_form/index.tsx | 24 ++++++++++++------- 6 files changed, 24 insertions(+), 14 deletions(-) diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx index c75fcdb5e..515d6ad8a 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx @@ -244,7 +244,7 @@ const InfilChart = ({ const renderResultPanel = () => (
- +
{selectedAplcnYm diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx index 5c9b09683..0a5abc436 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx @@ -218,7 +218,7 @@ const RdiiChart = ({ const renderResultPanel = () => (
- +
{selectedAplcnYm diff --git a/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx b/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx index 927e9581a..4e7f7a237 100644 --- a/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx +++ b/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx @@ -485,6 +485,7 @@ const IrSetting = () => { key="irsetting-layout-key" title="๋ถ„์„ ์กฐ๊ฑด ๋ฐ ์ž๋ฃŒ" columns={2} + wideLabels > {/* ์—ฐ์›” ์„ ํƒ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ์ˆ˜์ • ํ•„์š” */} diff --git a/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx b/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx index d0b72ef21..e5757f572 100644 --- a/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx +++ b/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx @@ -518,6 +518,7 @@ export default function RTKForm() { key="form-section" title="๋ถ„์„ ์กฐ๊ฑด ๋ฐ ์ž๋ฃŒ" columns={2} + wideLabels > diff --git a/apps/sn/app/components/ui/fileUploadForm.tsx b/apps/sn/app/components/ui/fileUploadForm.tsx index 8af347679..0277bc836 100644 --- a/apps/sn/app/components/ui/fileUploadForm.tsx +++ b/apps/sn/app/components/ui/fileUploadForm.tsx @@ -168,16 +168,16 @@ const FileUploadForm = ({ } return ( -
+
{" "} {/* ์ปดํฌ๋„ŒํŠธ ๋ฃจํŠธ div */} -
+
{" "} {/* ์นด๋“œ ์Šคํƒ€์ผ๋ง */} -
+
{" "} {/* FileUploader์™€ Button์„ ์ •๋ ฌํ•˜๋Š” ๋ฉ”์ธ flex ์ปจํ…Œ์ด๋„ˆ */} -
+
{" "} {/* FileUploader์˜ ๋„ˆ๋น„์™€ ์˜ค๋ฅธ์ชฝ ์—ฌ๋ฐฑ์„ ์ œ์–ดํ•˜๋Š” ์ปจํ…Œ์ด๋„ˆ */} {title} -
- {children} -
+ +
+ {children} +
+
); } @@ -78,9 +84,11 @@ function StandardFormField({ children, className = "", }: StandardFormFieldProps) { + const wideLabels = useContext(StandardFormWideLabelsContext); + return (
-
+
{children} {error ? (