diff --git a/apps/sn/app/(map-layout)/map/components/gisEdit/GisEditPipeSessionPopup.tsx b/apps/sn/app/(map-layout)/map/components/gisEdit/GisEditPipeSessionPopup.tsx
index 01e462338..c41e61ab6 100644
--- a/apps/sn/app/(map-layout)/map/components/gisEdit/GisEditPipeSessionPopup.tsx
+++ b/apps/sn/app/(map-layout)/map/components/gisEdit/GisEditPipeSessionPopup.tsx
@@ -238,11 +238,14 @@ export default function GisEditPipeSessionPopup({
const fcltMngNo = String(change.featureRef?.fcltMngNo ?? "").trim();
const mngInstCd = String(change.featureRef?.mngInstCd ?? "").trim();
if (!fcltMngNo || !mngInstCd) return;
+ if (/^NEW-/i.test(fcltMngNo) || /^NEW-/i.test(mngInstCd)) return;
let isMounted = true;
const fetchPipeInputInfo = async () => {
try {
- const result = await getData(`v1/fcltppl/${fcltMngNo}/${mngInstCd}`);
+ const safeFcltMngNo = encodeURIComponent(fcltMngNo);
+ const safeMngInstCd = encodeURIComponent(mngInstCd);
+ const result = await getData(`v1/fcltppl/${safeFcltMngNo}/${safeMngInstCd}`);
if (!isMounted || !result) return;
setInputInfoTableData((previous) => ({
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 07fc557e1..1ec81db2e 100644
--- a/apps/sn/app/(map-layout)/map/components/gisEdit/GisEditToolbar.tsx
+++ b/apps/sn/app/(map-layout)/map/components/gisEdit/GisEditToolbar.tsx
@@ -15,6 +15,7 @@ import type { GisEditTool } from "./gisEditTypes";
type Props = {
isEditMode: boolean;
activeTool: GisEditTool | null;
+ isSnapEnabled: boolean;
canUseTools: boolean;
canSave: boolean;
isSaving?: boolean;
@@ -23,6 +24,7 @@ type Props = {
allowedTools?: GisEditTool[];
onOpenSession?: () => void;
onClose?: () => void;
+ onToggleSnap: () => void;
onSelectTool: (tool: GisEditTool) => void;
onSave: () => void;
};
@@ -41,6 +43,7 @@ const TOOL_BUTTONS: Array<{
export default function GisEditToolbar({
isEditMode,
activeTool,
+ isSnapEnabled,
canUseTools,
canSave,
isSaving = false,
@@ -49,6 +52,7 @@ export default function GisEditToolbar({
allowedTools = [],
onOpenSession,
onClose,
+ onToggleSnap,
onSelectTool,
onSave,
}: Props) {
@@ -57,70 +61,109 @@ export default function GisEditToolbar({
return (
-
-
-
하수관로 편집
+
+
+ 하수관로 편집
+
+
+
+ {TOOL_BUTTONS.map(({ tool, label, icon: Icon }) => {
+ const isActive = activeTool === tool;
+ const isToolEnabled =
+ canUseTools && allowedTools.includes(tool);
+ if (tool === "VERTEX_EDIT") {
+ return (
+
+
+
+
+ );
+ }
+ return (
+
+ );
+ })}
+
- {TOOL_BUTTONS.map(({ tool, label, icon: Icon }) => {
- const isActive = activeTool === tool;
- const isToolEnabled =
- canUseTools && allowedTools.includes(tool);
- return (
-
- );
- })}
-
+
+
-
+
-
+
+
diff --git a/apps/sn/app/(map-layout)/map/components/gisEdit/gisEditTypes.ts b/apps/sn/app/(map-layout)/map/components/gisEdit/gisEditTypes.ts
index 4c627942c..930e48c07 100644
--- a/apps/sn/app/(map-layout)/map/components/gisEdit/gisEditTypes.ts
+++ b/apps/sn/app/(map-layout)/map/components/gisEdit/gisEditTypes.ts
@@ -103,6 +103,7 @@ export type GisEditSessionSnapshot = {
isEditMode: boolean;
selectedLayerName: string;
activeTool: GisEditTool | null;
+ isSnapEnabled: boolean;
changes: GisEditChange[];
lastSavedPayload: GisEditSavePayload | null;
canUndo: boolean;
diff --git a/apps/sn/app/(map-layout)/map/components/gisEdit/useGisEdit.ts b/apps/sn/app/(map-layout)/map/components/gisEdit/useGisEdit.ts
index 47e91c0b9..fd8e726c3 100644
--- a/apps/sn/app/(map-layout)/map/components/gisEdit/useGisEdit.ts
+++ b/apps/sn/app/(map-layout)/map/components/gisEdit/useGisEdit.ts
@@ -487,6 +487,8 @@ export const useGisEdit = (treeData: any[]) => {
const [selectedLayerId, setSelectedLayerId] = useState("tb_ms_swage_ppl");
// 1차 구현에서는 activeTool과 지도 선택 이벤트를 조합해 변경분을 로컬 세션에 누적한다.
const [activeTool, setActiveTool] = useState
(null);
+ // QGIS 유사 스냅핑 on/off는 MOVE, VERTEX_EDIT 상호작용에서 함께 참조한다.
+ const [isSnapEnabled, setIsSnapEnabled] = useState(false);
// 저장 전까지의 변경분은 FE 로컬 편집 세션으로만 유지한다.
const [changes, setChanges] = useState([]);
const [statusMessage, setStatusMessage] = useState(
@@ -560,6 +562,7 @@ export const useGisEdit = (treeData: any[]) => {
setActiveTool(null);
setPendingInteraction(null);
setFocusedChangeId(null);
+ setIsSnapEnabled(false);
latestSelectionRef.current = null;
setStatusMessage(
next
@@ -570,6 +573,18 @@ export const useGisEdit = (treeData: any[]) => {
});
}, []);
+ const toggleSnapEnabled = useCallback(() => {
+ setIsSnapEnabled((previous) => {
+ const next = !previous;
+ setStatusMessage(
+ next
+ ? "스냅핑을 켰습니다. 이동과 꼭지점편집 중 기존 하수관로 꼭지점/선분에 맞춰집니다."
+ : "스냅핑을 껐습니다. 이동과 꼭지점편집이 자유 좌표 기준으로 동작합니다.",
+ );
+ return next;
+ });
+ }, []);
+
const selectLayer = useCallback((layerId: string) => {
setSelectedLayerId(layerId);
setActiveTool(null);
@@ -612,10 +627,10 @@ export const useGisEdit = (treeData: any[]) => {
if (tool === "ADD") {
const addGuideMessage =
selectedLayer.geometryType === "Point"
- ? `${selectedLayer.layerName} 신규 위치를 지도에서 클릭하세요.`
+ ? `${selectedLayer.layerName} 신규 위치를 지도에서 클릭하세요.${isSnapEnabled ? " 스냅핑이 켜져 있으면 기존 관로 꼭지점/선분에 맞춰집니다." : ""}`
: selectedLayer.geometryType === "LineString"
- ? `${selectedLayer.layerName} 선형을 지도에서 연속 클릭으로 입력한 뒤 더블클릭으로 마무리하세요.`
- : `${selectedLayer.layerName} 영역을 지도에서 연속 클릭으로 입력한 뒤 더블클릭으로 마무리하세요.`;
+ ? `${selectedLayer.layerName} 선형을 지도에서 연속 클릭으로 입력한 뒤 더블클릭으로 마무리하세요.${isSnapEnabled ? " 스냅핑이 켜져 있으면 기존 관로 꼭지점/선분에 맞춰집니다." : ""}`
+ : `${selectedLayer.layerName} 영역을 지도에서 연속 클릭으로 입력한 뒤 더블클릭으로 마무리하세요.${isSnapEnabled ? " 스냅핑이 켜져 있으면 기존 관로 꼭지점/선분에 맞춰집니다." : ""}`;
setStatusMessage(addGuideMessage);
return;
}
@@ -627,13 +642,13 @@ export const useGisEdit = (treeData: any[]) => {
: "삭제";
const vertexGuideMessage =
tool === "VERTEX_EDIT"
- ? `지도에서 ${selectedLayer.layerName} 대상 객체를 클릭하면 실꼭지점은 주황색, 새 점 삽입용 midpoint는 흰색으로 표시됩니다.`
+ ? `지도에서 ${selectedLayer.layerName} 대상 객체를 클릭하면 실꼭지점은 주황색, 새 점 삽입용 midpoint는 흰색으로 표시됩니다.${isSnapEnabled ? " 스냅핑이 켜져 있으면 기존 관로 선분과 꼭지점에 맞춰집니다." : ""}`
: null;
setStatusMessage(
vertexGuideMessage ??
- `지도에서 ${selectedLayer.layerName} 대상 객체를 클릭하면 ${toolLabel} 변경분을 편집 세션에 추가합니다.`,
+ `지도에서 ${selectedLayer.layerName} 대상 객체를 클릭하면 ${toolLabel} 변경분을 편집 세션에 추가합니다.${tool === "MOVE" && isSnapEnabled ? " 스냅핑이 켜져 있으면 이동 중 기존 관로 꼭지점/선분에 맞춰집니다." : ""}`,
);
- }, [appendChange, selectedLayer]);
+ }, [appendChange, isSnapEnabled, selectedLayer]);
const deactivateTool = useCallback(() => {
setActiveTool(null);
@@ -1052,6 +1067,7 @@ export const useGisEdit = (treeData: any[]) => {
setActiveTool(null);
setPendingInteraction(null);
setFocusedChangeId(null);
+ setIsSnapEnabled(false);
latestSelectionRef.current = null;
setStatusMessage("편집 세션을 초기화했습니다.");
}, []);
@@ -1063,6 +1079,7 @@ export const useGisEdit = (treeData: any[]) => {
setActiveTool(null);
setPendingInteraction(null);
setFocusedChangeId(null);
+ setIsSnapEnabled(false);
latestSelectionRef.current = null;
setStatusMessage("하수관로 편집을 종료하고 세션 변경분을 초기화했습니다.");
}, []);
@@ -1150,6 +1167,7 @@ export const useGisEdit = (treeData: any[]) => {
selectedLayer,
selectedLayerId,
activeTool,
+ isSnapEnabled,
changes,
statusMessage,
canUseTools,
@@ -1161,6 +1179,7 @@ export const useGisEdit = (treeData: any[]) => {
"현재 구현은 FE 로컬 편집 세션, 관로 기본정보 draft, BE 저장 API(`/api/v1/map/gisEditSessions/save`) 최종본 저장까지 포함합니다.",
canUndo: actionableChanges.length > 0,
toggleEditMode,
+ toggleSnapEnabled,
selectLayer,
activateTool,
deactivateTool,
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 5dd35eb37..d9a182009 100644
--- a/apps/sn/app/(map-layout)/map/components/sinkhole/SinkholeLayerSettingPanel.tsx
+++ b/apps/sn/app/(map-layout)/map/components/sinkhole/SinkholeLayerSettingPanel.tsx
@@ -33,7 +33,7 @@ export default function SinkholeLayerSettingPanel({
};
return (
-
+