Files
frontend/apps/sn/app/(map-layout)/map/components/analysis-menu/analysis-popup-menu.tsx
T

249 lines
8.3 KiB
TypeScript

/**
* 화면명: 분석도구 메뉴
* 지도에서 실행할 분석·현황 팝업 메뉴를 펼치고 선택한다.
*/
"use client";
import { type ComponentType, type ReactNode,useEffect, useState } from "react";
import {
ChartColumnBig,
ChevronUp,
PanelsTopLeft,
Search,
TriangleAlert,
Waves,
} from "lucide-react";
import { createPredictionAnalysisTimeRange } from "@/features/fldng-predc-prst/model/analysis-time-range";
import {
windowPopupDefinitions,
type WindowPopupKey,
} from "@/features/window-popup/model/popup-definitions";
import { getRawJson } from "@/shared/api/sewerage-api";
type Props = {
onOpenPopup: (popupKey: WindowPopupKey) => Window | null;
onOpenInfoSearch: () => void;
isInfoSearchActive?: boolean;
initialActiveKey?: WindowPopupKey | null;
};
type MenuIconProps = {
isActive: boolean;
};
const InftwtMenuIcon = () => (
<svg
viewBox="0 0 24 24"
className="h-[15px] w-[15px] shrink-0 text-current"
aria-hidden="true"
fill="none"
>
<path
d="M12 2.8C9.5 6.5 6.7 9.8 6.7 13.3A5.3 5.3 0 0 0 12 18.6a5.3 5.3 0 0 0 5.3-5.3c0-3.5-2.8-6.8-5.3-10.5Z"
fill="currentColor"
fillOpacity="0.14"
stroke="currentColor"
strokeWidth="1.8"
strokeLinejoin="round"
/>
<path
d="M9.5 13.4c0-1.2.8-2.6 1.9-4.2"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
opacity="0.9"
/>
</svg>
);
const ANALYSIS_POPUP_ITEMS: Array<{
key: WindowPopupKey | "infoSearch";
label: string;
icon?: ComponentType<{ className?: string }>;
renderIcon?: (props: MenuIconProps) => ReactNode;
}> = [
{
key: "infoSearch",
label: windowPopupDefinitions.mapSearch.title,
icon: Search,
},
{
key: "pipeAnalysisPopup",
label: windowPopupDefinitions.pipeAnalysisPopup.title,
icon: ChartColumnBig,
},
{
key: "fldngPredcPrst",
label: windowPopupDefinitions.fldngPredcPrst.title,
icon: Waves,
},
{
key: "agedPipeStatusByCtpvPopup",
label: windowPopupDefinitions.agedPipeStatusByCtpvPopup.title,
icon: ChartColumnBig,
},
{
key: "flowRiskPopup",
label: windowPopupDefinitions.flowRiskPopup.title,
icon: TriangleAlert,
},
{
key: "inftwtResultPopup",
label: windowPopupDefinitions.inftwtResultPopup.title,
renderIcon: InftwtMenuIcon,
},
{
key: "rdiiResultPopup",
label: windowPopupDefinitions.rdiiResultPopup.title,
icon: ChartColumnBig,
},
];
export default function AnalysisPopupMenu({
onOpenPopup,
onOpenInfoSearch,
isInfoSearchActive = false,
initialActiveKey = null,
}: Props) {
const [isOpen, setIsOpen] = useState(true);
const [activeKey, setActiveKey] = useState<
WindowPopupKey | "infoSearch" | null
>(initialActiveKey);
const [seriousPredictionCount, setSeriousPredictionCount] = useState(0);
// 침수예측현황 팝업의 기본 분석시각 범위와 동일한 범위로 메뉴 건수를 조회한다.
useEffect(() => {
const range = createPredictionAnalysisTimeRange();
const params = new URLSearchParams({
analysisFrom: range.analysisFrom,
analysisTo: range.analysisTo,
});
let cancelled = false;
void getRawJson(
`/sewerage-service/api/v1/fldng/predc/prst/count?${params.toString()}`,
)
.then((response) => {
if (cancelled) return;
const payload = response?.data ?? response;
const count = Number(payload);
setSeriousPredictionCount(Number.isFinite(count) ? count : 0);
})
.catch(() => {
if (!cancelled) setSeriousPredictionCount(0);
});
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
if (!isInfoSearchActive && activeKey === "infoSearch") {
setActiveKey(null);
}
}, [activeKey, isInfoSearchActive]);
useEffect(() => {
if (!initialActiveKey) return;
setActiveKey((currentKey) => currentKey ?? initialActiveKey);
}, [initialActiveKey]);
return (
<div className="pointer-events-none absolute right-[68px] top-[16px] z-[45] flex flex-col items-end gap-[8px]">
<button
type="button"
className={`pointer-events-auto flex h-[40px] w-[40px] shrink-0 items-center justify-center overflow-hidden rounded-[3px] p-0 shadow-[0_1px_2px_rgba(60,64,67,0.3),0_1px_3px_1px_rgba(60,64,67,0.15)] ${
isOpen
? "bg-primary text-primary-foreground"
: "bg-background text-label hover:bg-primary hover:text-primary-foreground"
}`}
onClick={() => setIsOpen((value) => !value)}
title={isOpen ? "분석 메뉴 숨기기" : "분석도구"}
aria-label={isOpen ? "분석 메뉴 숨기기" : "분석 메뉴 보이기"}
aria-expanded={isOpen}
aria-controls="analysis-popup-menu"
>
<PanelsTopLeft className="h-[20px] w-[20px]" aria-hidden="true" />
</button>
{isOpen ? (
<div
id="analysis-popup-menu"
className="pointer-events-auto flex w-max min-w-[154px] flex-col overflow-hidden rounded-[6px] bg-background shadow-[0_1px_2px_rgba(60,64,67,0.3),0_1px_3px_1px_rgba(60,64,67,0.15)]"
>
<div className="flex h-[36px] shrink-0 items-center justify-between gap-2 border-b border-border bg-background pl-[10px] pr-[4px] text-[13px] font-bold text-foreground">
<div className="flex items-center gap-2">
<PanelsTopLeft className="h-4 w-4 text-primary" />
<span>분석도구</span>
</div>
<button
type="button"
className="flex h-[28px] w-[28px] shrink-0 items-center justify-center rounded-[4px] text-label hover:bg-accent hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary"
onClick={() => setIsOpen(false)}
title="분석 메뉴 숨기기"
aria-label="분석 메뉴 숨기기"
>
<ChevronUp className="h-4 w-4" aria-hidden="true" />
</button>
</div>
<div className="flex flex-col gap-[3px] p-[6px]">
{ANALYSIS_POPUP_ITEMS.map((item) => {
const Icon = item.icon;
const isActive = activeKey === item.key;
const label =
item.key === "fldngPredcPrst"
? `침수예측현황(${seriousPredictionCount}건)`
: item.label;
return (
<button
key={item.key}
type="button"
className={`relative flex h-[33px] items-center gap-[7px] rounded-[5px] border px-[8px] pl-[10px] text-left text-[12px] font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary ${
isActive
? "border-primary bg-secondary font-bold text-primary before:absolute before:bottom-[7px] before:left-0 before:top-[7px] before:w-[3px] before:rounded-r-[2px] before:bg-primary"
: "border-transparent bg-background text-foreground hover:border-border hover:bg-accent hover:text-primary"
}`}
onClick={() => {
setActiveKey(item.key);
if (item.key === "infoSearch") {
onOpenInfoSearch();
return;
}
const popupWindow = onOpenPopup(item.key);
if (!popupWindow) {
setActiveKey(null);
return;
}
const closeWatcher = window.setInterval(() => {
if (!popupWindow.closed) return;
window.clearInterval(closeWatcher);
setActiveKey((currentKey) =>
currentKey === item.key ? null : currentKey,
);
}, 300);
}}
>
{item.renderIcon ? item.renderIcon({ isActive }) : null}
{Icon ? (
<Icon className="h-[15px] w-[15px] shrink-0 text-current" />
) : null}
<span className="whitespace-nowrap" title={label}>
{label}
</span>
</button>
);
})}
</div>
</div>
) : null}
</div>
);
}