sn: GIS편집,GIS관리, 통수위험 정보카드 X버튼

This commit is contained in:
2026-10-16 17:36:31 +09:00
parent fe501965bf
commit 7911cff96a
6 changed files with 130 additions and 50 deletions
@@ -10,9 +10,7 @@ import {
ChartColumnBig,
ChevronUp,
PanelsTopLeft,
PencilRuler,
Search,
Settings2,
TriangleAlert,
Waves,
} from "lucide-react";
@@ -28,8 +26,6 @@ import { getRawJson } from "@/shared/api/sewerage-api";
type Props = {
onOpenPopup: (popupKey: WindowPopupKey) => Window | null;
onOpenInfoSearch: () => void;
onToggleGisEdit?: () => void;
isGisEditActive?: boolean;
isInfoSearchActive?: boolean;
initialActiveKey?: WindowPopupKey | null;
};
@@ -89,16 +85,6 @@ const ANALYSIS_POPUP_ITEMS: Array<{
label: windowPopupDefinitions.agedPipeStatusByCtpvPopup.title,
icon: ChartColumnBig,
},
{
key: "gisEditPopup",
label: windowPopupDefinitions.gisEditPopup.title,
icon: PencilRuler,
},
{
key: "gisManagementPopup",
label: windowPopupDefinitions.gisManagementPopup.title,
icon: Settings2,
},
{
key: "flowRiskPopup",
label: windowPopupDefinitions.flowRiskPopup.title,
@@ -119,8 +105,6 @@ const ANALYSIS_POPUP_ITEMS: Array<{
export default function AnalysisPopupMenu({
onOpenPopup,
onOpenInfoSearch,
onToggleGisEdit,
isGisEditActive = false,
isInfoSearchActive = false,
initialActiveKey = null,
}: Props) {
@@ -155,12 +139,6 @@ export default function AnalysisPopupMenu({
};
}, []);
useEffect(() => {
if (!isGisEditActive && activeKey === "gisEditPopup") {
setActiveKey(null);
}
}, [activeKey, isGisEditActive]);
useEffect(() => {
if (!isInfoSearchActive && activeKey === "infoSearch") {
setActiveKey(null);
@@ -214,10 +192,7 @@ export default function AnalysisPopupMenu({
<div className="flex flex-col gap-[3px] p-[6px]">
{ANALYSIS_POPUP_ITEMS.map((item) => {
const Icon = item.icon;
const isActive =
item.key === "gisEditPopup"
? isGisEditActive
: activeKey === item.key;
const isActive = activeKey === item.key;
const label =
item.key === "fldngPredcPrst"
? `침수예측현황(${seriousPredictionCount}건)`
@@ -233,12 +208,6 @@ export default function AnalysisPopupMenu({
: "border-transparent bg-background text-foreground hover:border-border hover:bg-accent hover:text-primary"
}`}
onClick={() => {
if (item.key === "gisEditPopup" && onToggleGisEdit) {
setActiveKey(item.key);
onToggleGisEdit();
return;
}
setActiveKey(item.key);
if (item.key === "infoSearch") {
@@ -34,7 +34,7 @@ import type {
export const useGisEdit = (treeData: any[]) => {
const editableLayers = useMemo(() => collectEditableLayers(treeData), [treeData]);
// GIS 편집 모드 on/off는 우측 분석 메뉴의 GIS 편집 버튼과 직접 연결된다.
// GIS 편집 모드 on/off는 지도 우측 GIS 편집 아이콘과 직접 연결된다.
const [isEditMode, setIsEditMode] = useState(false);
// 현재 선택 레이어를 기준으로 허용 도구와 저장 payload의 layerId를 결정한다.
const [selectedLayerId, setSelectedLayerId] = useState("tb_ms_swage_ppl");
@@ -0,0 +1,85 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { PencilRuler, Settings2 } from "lucide-react";
type Props = {
isGisEditActive: boolean;
onToggleGisEdit: () => void;
onOpenGisManagement: () => Window | null;
};
/** 기존 지도 도구와 같은 크기로 GIS 버튼의 기본·활성·키보드 포커스를 표시한다. */
const getButtonClass = (isActive: boolean) =>
`flex h-[40px] w-[40px] items-center justify-center p-0 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary ${
isActive
? "bg-primary text-primary-foreground"
: "bg-background text-label hover:bg-primary hover:text-primary-foreground"
}`;
/** 분석도구 메뉴와 독립적으로 GIS 편집 모드와 관리 팝업의 진입 버튼을 제공한다. */
export default function GisToolButtons({
isGisEditActive,
onToggleGisEdit,
onOpenGisManagement,
}: Props) {
// 관리 팝업이 닫히면 버튼의 활성 표시도 해제한다.
const [isGisManagementActive, setIsGisManagementActive] = useState(false);
// 렌더링과 무관한 감지 타이머를 보관해 재클릭·화면 이탈 시 중복 실행을 방지한다.
const closeWatcherRef = useRef<number | null>(null);
useEffect(() => () => {
if (closeWatcherRef.current !== null) {
window.clearInterval(closeWatcherRef.current);
}
}, []);
/** 기존 팝업 실행 함수를 호출하고 차단·닫힘 상태를 버튼에 반영한다. */
const handleOpenGisManagement = () => {
if (closeWatcherRef.current !== null) {
window.clearInterval(closeWatcherRef.current);
closeWatcherRef.current = null;
}
const popup = onOpenGisManagement();
setIsGisManagementActive(Boolean(popup && !popup.closed));
if (!popup || popup.closed) return;
closeWatcherRef.current = window.setInterval(() => {
if (!popup.closed) return;
if (closeWatcherRef.current !== null) {
window.clearInterval(closeWatcherRef.current);
closeWatcherRef.current = null;
}
setIsGisManagementActive(false);
}, 300);
};
return (
<div
role="group"
aria-label="GIS 도구"
className="absolute right-[16px] top-[222.67px] z-[45] flex w-[40px] flex-col overflow-hidden rounded-[3px] bg-background shadow-[0_1px_2px_rgba(60,64,67,0.3),0_1px_3px_1px_rgba(60,64,67,0.15)]"
>
<button
type="button"
title="GIS 편집"
aria-label="GIS 편집"
aria-pressed={isGisEditActive}
onClick={onToggleGisEdit}
className={`${getButtonClass(isGisEditActive)} border-b border-[#eeeeee]`}
>
<PencilRuler className="h-[20px] w-[20px]" aria-hidden="true" />
</button>
<button
type="button"
title="GIS 관리"
aria-label="GIS 관리"
aria-haspopup="dialog"
className={getButtonClass(isGisManagementActive)}
onClick={handleOpenGisManagement}
>
<Settings2 className="h-[20px] w-[20px]" aria-hidden="true" />
</button>
</div>
);
}
@@ -127,7 +127,7 @@ export function useWaterRiskLayer({
const [pixelX = 0, pixelY = 0] =
map.getPixelFromCoordinate?.(waterRiskInfoAnchor) ?? [];
const cardWidth = 244;
const cardHeight = 180;
const cardHeight = 190;
const left = pixelX + 22;
const top = pixelY - cardHeight / 2;
@@ -148,6 +148,12 @@ export function useWaterRiskLayer({
setWaterRiskInfoCardStyle({ left, top });
}, [waterRiskInfoAnchor]);
/** 정보 카드만 닫고 지도 결과·범례는 유지하며, 다음 결과 선택 시 다시 표시한다. */
const dismissWaterRiskInfoCard = useCallback(() => {
setWaterRiskInfoAnchor(null);
setWaterRiskInfoCardStyle(null);
}, []);
const clearWaterRiskPopupCloseWatcher = useCallback(() => {
if (waterRiskPopupCloseWatcherRef.current !== null) {
window.clearInterval(waterRiskPopupCloseWatcherRef.current);
@@ -470,6 +476,7 @@ export function useWaterRiskLayer({
waterRiskInfoCardStyle,
selectedWaterRiskMapView,
selectedWaterRiskDisplayStatus,
dismissWaterRiskInfoCard,
watchPopup,
};
}
@@ -1,6 +1,6 @@
"use client";
/** 지도 route에만 적용하는 기존 전역 컨트롤 CSS. 선택자와 스타일 값은 유지한다. */
/** 지도 route의 기존 배치를 유지하고 SN 컨트롤의 쌓임 순서를 맞춘다. */
export default function MapControlStyles() {
return (
<style jsx global>{`
@@ -8,6 +8,7 @@ export default function MapControlStyles() {
top: 16px;
right: 120px;
bottom: auto;
z-index: 45;
}
.sn-map-control-layout .map-type-tabs,
@@ -28,6 +29,7 @@ export default function MapControlStyles() {
top: 16px;
right: 16px;
bottom: auto;
z-index: 45;
}
.sn-map-control-layout .tool-bar-vector-icons ul li div {
@@ -63,7 +63,7 @@ import {
normalizeRainObservationLayerInteraction,
getFlowDirectionMinZoom,
} from "@/features/map-layers";
import { ListTree } from "lucide-react";
import { ListTree, X } from "lucide-react";
import {
Fragment,
useCallback,
@@ -184,6 +184,8 @@ import {
} from "../water-depth/water-depth";
import { buildWaterRiskStatusLabel } from "../components/water-risk/water-risk-types";
import GisToolButtons from "../components/gis-tools/gis-tool-buttons";
const AnalysisPopupMenu = dynamic(
() => import("../components/analysis-menu/analysis-popup-menu"),
{ ssr: false },
@@ -296,6 +298,7 @@ const MapPageClient = () => {
waterRiskInfoCardStyle,
selectedWaterRiskMapView,
selectedWaterRiskDisplayStatus,
dismissWaterRiskInfoCard,
watchPopup: watchWaterRiskPopup,
} = useWaterRiskLayer({
gisMaps,
@@ -3128,24 +3131,35 @@ const MapPageClient = () => {
style={waterRiskInfoCardStyle}
className="absolute z-[44] w-[244px] overflow-hidden rounded-[12px] border border-border bg-background shadow-blur"
>
<div className="flex items-center justify-between bg-secondary px-[9px] py-[7px]">
<div>
<div className="bg-secondary px-[9px] py-[7px]">
<div className="flex items-center justify-between">
<div className="text-[10px] font-semibold text-muted-foreground">
중점관리지역
</div>
<div className="mt-[2px] text-[13px] font-bold text-foreground">
<button
type="button"
className="flex h-[20px] w-[20px] items-center justify-center rounded-[3px] text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
onClick={dismissWaterRiskInfoCard}
aria-label="통수위험 정보 카드 닫기"
title="닫기"
>
<X className="h-[14px] w-[14px]" aria-hidden="true" />
</button>
</div>
<div className="mt-[2px] flex items-center justify-between gap-2">
<div className="min-w-0 text-[13px] font-bold text-foreground">
{selectedWaterRiskResultRow.areaName}
</div>
<span
className={`inline-flex min-w-[48px] shrink-0 items-center justify-center rounded-full px-[7px] py-[5px] text-[10px] font-bold ${
selectedWaterRiskDisplayStatus === "DANGER"
? "bg-[#ff1c15] text-white"
: "bg-[#e8f6f4] text-[#0f766e]"
}`}
>
{buildWaterRiskStatusLabel(selectedWaterRiskDisplayStatus)}
</span>
</div>
<span
className={`inline-flex min-w-[48px] items-center justify-center rounded-full px-[7px] py-[5px] text-[10px] font-bold ${
selectedWaterRiskDisplayStatus === "DANGER"
? "bg-[#ff1c15] text-white"
: "bg-[#e8f6f4] text-[#0f766e]"
}`}
>
{buildWaterRiskStatusLabel(selectedWaterRiskDisplayStatus)}
</span>
</div>
<div className="bg-background px-[9px] py-[6px] text-[9px] text-muted-foreground">
<div>GIS 매핑 관로 중 Capacity 0.8 이상이 1개라도 있으면</div>
@@ -3236,10 +3250,13 @@ const MapPageClient = () => {
initialActiveKey={initialAnalysisPopupKey}
onOpenPopup={openAnalysisPopup}
onOpenInfoSearch={() => setIsSearchModalOpen(true)}
onToggleGisEdit={handleToggleGisEdit}
isGisEditActive={gisEdit.isEditMode}
isInfoSearchActive={isSearchModalOpen}
/>
<GisToolButtons
isGisEditActive={gisEdit.isEditMode}
onToggleGisEdit={handleToggleGisEdit}
onOpenGisManagement={() => openAnalysisPopup("gisManagementPopup")}
/>
{isGisEditSessionInlineVisible ? (
<div
data-testid="gis-edit-session-inline"