diff --git a/apps/as/src/entities/dashboard/index.ts b/apps/as/src/entities/dashboard/index.ts index 19d5b2c0b..46bd4250f 100644 --- a/apps/as/src/entities/dashboard/index.ts +++ b/apps/as/src/entities/dashboard/index.ts @@ -1,6 +1,7 @@ export { dashboardService } from "./api/dashboard-service"; export { dashboardBaseKey, useGetMainDashboard } from "./api/queries"; export type { + BasinDocPrgrsResponseDto, DashboardMainResponseDto, DashboardMainSearchDto, DashboardService, diff --git a/apps/as/src/entities/dashboard/model/dashboard-interface.ts b/apps/as/src/entities/dashboard/model/dashboard-interface.ts index fb592680e..abad209c5 100644 --- a/apps/as/src/entities/dashboard/model/dashboard-interface.ts +++ b/apps/as/src/entities/dashboard/model/dashboard-interface.ts @@ -18,6 +18,18 @@ export interface DocPrgrsSttsResponseDto { totCnt: number; } +/** 백엔드 BasinDocPrgrsResponseDto */ +export interface BasinDocPrgrsResponseDto { + basinKey: string; + basinNm: string; + docSeComCd: string; + docSeNm: string; + reqCnt: number; + prgrsCnt: number; + cmptnCnt: number; + totCnt: number; +} + /** 백엔드 NotiAsUserResponseDto */ export interface NotiAsUserResponseDto { notiId: number; @@ -48,6 +60,7 @@ export interface NotiAsStatResponseDto { /** 백엔드 DashboardMainResponseDto */ export interface DashboardMainResponseDto { docPrgrsSttsList: DocPrgrsSttsResponseDto[]; + basinDocPrgrsList: BasinDocPrgrsResponseDto[]; notiList: NotiAsUserResponseDto[]; notiStats: NotiAsStatResponseDto; } diff --git a/apps/as/src/pages/dashboard/lib/use-national-as-map.ts b/apps/as/src/pages/dashboard/lib/use-national-as-map.ts index 997c0154a..c46748ba2 100644 --- a/apps/as/src/pages/dashboard/lib/use-national-as-map.ts +++ b/apps/as/src/pages/dashboard/lib/use-national-as-map.ts @@ -1,7 +1,10 @@ import { type CSSProperties, useCallback, useEffect, useRef, useState } from "react"; +import type { BasinDocPrgrsResponseDto } from "@/entities/dashboard"; + import { BASIN_DATA, + BASIN_DOC_SE_SLOTS, BASIN_IDS, type BasinKey, MANUAL_LABEL_POS, @@ -15,8 +18,6 @@ const POPUP_EDGE_PADDING_PX = 12; const LABEL_OFFSET_Y_PX = 18; const DEFAULT_VIEWBOX = "0 0 100 100"; -export const PLACEHOLDER_POPUP_ITEMS = [{ header: "현황", data: "-" }] as const; - export interface MapPopupItem { header: string; data: string; @@ -27,10 +28,28 @@ export interface MapPopupContent { items: MapPopupItem[]; } -const INITIAL_POPUP_CONTENT: MapPopupContent = { - title: "", - items: [...PLACEHOLDER_POPUP_ITEMS], -}; +interface ActiveBasin { + key: string | undefined; + title: string; +} + +const INITIAL_ACTIVE_BASIN: ActiveBasin = { key: undefined, title: "" }; + +function buildPopupItems( + basinDocPrgrsList: BasinDocPrgrsResponseDto[] | undefined, + basinKey: string | undefined, +): MapPopupItem[] { + const itemByCd = new Map( + basinDocPrgrsList + ?.filter((item) => item.basinKey === basinKey) + .map((item) => [item.docSeComCd, item]), + ); + + return BASIN_DOC_SE_SLOTS.map((slot) => { + const item = itemByCd.get(slot.docSeComCd); + return { header: slot.label, data: item ? String(item.totCnt) : "-" }; + }); +} function isBasinKey(key: string | undefined): key is BasinKey { return Boolean(key && key in BASIN_DATA); @@ -172,22 +191,25 @@ function positionLabels( }); } -export function useNationalAsMap() { +export function useNationalAsMap(basinDocPrgrsList: BasinDocPrgrsResponseDto[] | undefined) { const stageRef = useRef(null); const svgLayerRef = useRef(null); const labelsLayerRef = useRef(null); const popupRef = useRef(null); - const [popupContent, setPopupContent] = useState(INITIAL_POPUP_CONTENT); + const [activeBasin, setActiveBasin] = useState(INITIAL_ACTIVE_BASIN); + const popupContent: MapPopupContent = { + title: activeBasin.title, + items: buildPopupItems(basinDocPrgrsList, activeBasin.key), + }; const showFor = useCallback((group: SVGGElement) => { const stage = stageRef.current; const popup = popupRef.current; if (!stage || !popup) return; - setPopupContent({ - title: resolveBasinName(group), - items: [...PLACEHOLDER_POPUP_ITEMS], - }); + const key = group.dataset.key; + const title = resolveBasinName(group); + setActiveBasin((prev) => (prev.key === key && prev.title === title ? prev : { key, title })); showPopupAtGroup(stage, popup, group); }, []); diff --git a/apps/as/src/pages/dashboard/model/map-data.ts b/apps/as/src/pages/dashboard/model/map-data.ts index 18f703161..594cdab03 100644 --- a/apps/as/src/pages/dashboard/model/map-data.ts +++ b/apps/as/src/pages/dashboard/model/map-data.ts @@ -19,6 +19,13 @@ export const BASIN_IDS = [ { key: "yeongsan", labels: ["영산강", "yeongsan"] }, ] as const; +export const BASIN_DOC_SE_SLOTS = [ + { docSeComCd: "FNRS", label: "재원협의" }, + { docSeComCd: "INSTL", label: "설치인가" }, + { docSeComCd: "CNFM", label: "설계자문" }, + { docSeComCd: "SEWER", label: "기본계획" }, +] as const; + export type BasinKey = keyof typeof BASIN_DATA; export interface ManualLabelPos { diff --git a/apps/as/src/pages/dashboard/ui/dashboard-page.tsx b/apps/as/src/pages/dashboard/ui/dashboard-page.tsx index 828a130f6..2093dfc48 100644 --- a/apps/as/src/pages/dashboard/ui/dashboard-page.tsx +++ b/apps/as/src/pages/dashboard/ui/dashboard-page.tsx @@ -13,7 +13,7 @@ export function DashboardPage() { return (
- +
diff --git a/apps/as/src/pages/dashboard/ui/national-as-map.tsx b/apps/as/src/pages/dashboard/ui/national-as-map.tsx index 096bbed89..82e93676c 100644 --- a/apps/as/src/pages/dashboard/ui/national-as-map.tsx +++ b/apps/as/src/pages/dashboard/ui/national-as-map.tsx @@ -2,13 +2,19 @@ import type { CSSProperties } from "react"; +import type { BasinDocPrgrsResponseDto } from "@/entities/dashboard"; + import { useNationalAsMap } from "../lib/use-national-as-map"; import styles from "../lib/national-as-map.module.css"; -export function NationalAsMap() { +interface NationalAsMapProps { + basinDocPrgrsList: BasinDocPrgrsResponseDto[] | undefined; +} + +export function NationalAsMap({ basinDocPrgrsList }: NationalAsMapProps) { const { stageRef, svgLayerRef, labelsLayerRef, popupRef, popupContent, popupGridStyle } = - useNationalAsMap(); + useNationalAsMap(basinDocPrgrsList); const popupColumnSpan = Math.max(1, popupContent.items.length); return ( diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx index 7d50b9dc8..cba9b1b19 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx @@ -97,7 +97,6 @@ type ActiveDialog = | "elaprTmplt" | "elaprDocPrgrs" | "rjct" - | "dmndSplmnt" | "fileSplmnt" | "elaprApproval" | "pwrkCnsltnDocMng" @@ -124,6 +123,7 @@ function FloatingActionButtons({ const alert = useAlertStore((state) => state.setMessage); const [isProcessing, setIsProcessing] = useState(false); const [activeDialog, setActiveDialog] = useState(null); + const [isDmndSplmntDialogOpen, setIsDmndSplmntDialogOpen] = useState(false); const docNo = data.detDto?.fnrsCnsltnDocNo ?? fnrsCnsltnDocNo; const isRegister = data.detDto?.rgtr === userInfo?.userUnqId; @@ -217,6 +217,10 @@ function FloatingActionButtons({ const canEdit = isRegister && actionButtons.some((button) => button === FNRS_CNSLTN_VIEW_BUTTON_ACTION.strg); + const canDmndSplmntInPwrkCnsltnDoc = + prgrsSttsComCd === "RK" && + atrzSeComCdList.includes("ATRZ02") && + actionButtons.some((button) => button === FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt); function handleGoToList() { router.push(getListHref("/fnrs-cnsltn")); @@ -533,6 +537,7 @@ function FloatingActionButtons({ } async function handleDmndSplmntConfirm(payload: SplmntDmndDialogConfirmPayload) { + setIsDmndSplmntDialogOpen(false); setActiveDialog(null); const requestPayload = buildSupplementReqPayload(data, payload, checklistFormState); await executeAction({ @@ -763,7 +768,7 @@ function FloatingActionButtons({ {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt) && (
{pjtcoAjmtPage?.content?.length ? ( diff --git a/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi-toolbar.tsx b/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi-toolbar.tsx index b5bd5db0d..b11182436 100644 --- a/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi-toolbar.tsx +++ b/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi-toolbar.tsx @@ -2,7 +2,7 @@ import { useId } from "react"; -import { Download, FolderOpen, Fullscreen, Save } from "lucide-react"; +import { Download, FilePen, FolderOpen, Fullscreen, Save } from "lucide-react"; import { Button, cn } from "@common/components"; @@ -15,6 +15,7 @@ interface HangulGiangiToolbarProps { onSave: () => void; onDownload: () => void; onFullscreen: () => void; + onDmndSplmnt?: () => void; } function HangulGiangiToolbar({ @@ -24,6 +25,7 @@ function HangulGiangiToolbar({ onSave, onDownload, onFullscreen, + onDmndSplmnt, }: HangulGiangiToolbarProps) { const openInputId = useId(); @@ -79,6 +81,12 @@ function HangulGiangiToolbar({ 전체화면 + {onDmndSplmnt && ( + + )}
); } diff --git a/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx b/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx index db9db7c32..5a382d640 100644 --- a/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx +++ b/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx @@ -21,6 +21,7 @@ function HangulGiangi({ documentFileName, onSaveDocument, onDownload, + onDmndSplmnt, canSave = true, bridgeUrl = DEFAULT_BRIDGE_URL, ctrlBaseUrl = WEBHYP_CTRL_URL, @@ -206,6 +207,7 @@ function HangulGiangi({ onSave={handleSave} onDownload={handleDownload} onFullscreen={handleFullscreen} + onDmndSplmnt={onDmndSplmnt} />