[as] feat: 보완 요청 사항 적용

- 실무협의서 내 보완요청 버튼 추가
- 대시보드 수정
This commit is contained in:
2026-10-02 11:37:45 +09:00
parent 4167107a91
commit c5a0e378fd
10 changed files with 91 additions and 20 deletions
+1
View File
@@ -1,6 +1,7 @@
export { dashboardService } from "./api/dashboard-service";
export { dashboardBaseKey, useGetMainDashboard } from "./api/queries";
export type {
BasinDocPrgrsResponseDto,
DashboardMainResponseDto,
DashboardMainSearchDto,
DashboardService,
@@ -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;
}
@@ -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<HTMLDivElement>(null);
const svgLayerRef = useRef<HTMLDivElement>(null);
const labelsLayerRef = useRef<HTMLDivElement>(null);
const popupRef = useRef<HTMLDivElement>(null);
const [popupContent, setPopupContent] = useState<MapPopupContent>(INITIAL_POPUP_CONTENT);
const [activeBasin, setActiveBasin] = useState<ActiveBasin>(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);
}, []);
@@ -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 {
@@ -13,7 +13,7 @@ export function DashboardPage() {
return (
<div className="mt-4 flex gap-4 px-[1.8rem] pb-6">
<div className="hidden flex-[0.5] sm:block">
<NationalAsMap />
<NationalAsMap basinDocPrgrsList={data?.basinDocPrgrsList} />
</div>
<div className="flex min-h-0 flex-1 flex-col gap-4">
<DocPrgrsSttsCards items={data?.docPrgrsSttsList} isPending={isPending} isError={isError} />
@@ -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 (
@@ -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<ActiveDialog>(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) && (
<Button
color="gray"
onClick={() => setActiveDialog("dmndSplmnt")}
onClick={() => setIsDmndSplmntDialogOpen(true)}
disabled={isBusy}
>
보완 요청
@@ -802,8 +807,8 @@ function FloatingActionButtons({
<SplmntDmndDialog
splmntArtclUpCd="SPLMNTARTCL"
open={activeDialog === "dmndSplmnt"}
onOpenChange={() => setActiveDialog(null)}
open={isDmndSplmntDialogOpen}
onOpenChange={() => setIsDmndSplmntDialogOpen(false)}
isSubmitting={isBusy}
onConfirm={handleDmndSplmntConfirm}
/>
@@ -839,6 +844,9 @@ function FloatingActionButtons({
? false
: null
}
onDmndSplmnt={
canDmndSplmntInPwrkCnsltnDoc ? () => setIsDmndSplmntDialogOpen(true) : undefined
}
onClose={() => setActiveDialog(null)}
/>
@@ -58,6 +58,7 @@ interface PwrkCnsltnDocMngDialogProps {
atrzSeComCdList?: string[];
prgrsSttsComCd?: string;
canSave?: boolean | null;
onDmndSplmnt?: () => void;
onClose: () => void;
}
@@ -78,6 +79,7 @@ function PwrkCnsltnDocMngDialog({
atrzSeComCdList,
prgrsSttsComCd,
canSave: canSaveProp,
onDmndSplmnt,
onClose,
}: PwrkCnsltnDocMngDialogProps) {
const queryClient = useQueryClient();
@@ -309,6 +311,7 @@ function PwrkCnsltnDocMngDialog({
canSave={canSave}
onSaveDocument={handleSaveDocument}
onDownload={handleDownload}
onDmndSplmnt={onDmndSplmnt}
/>
</div>
{pjtcoAjmtPage?.content?.length ? (
@@ -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({
<Fullscreen className="!size-5" />
전체화면
</Button>
{onDmndSplmnt && (
<Button type="button" color="white" size="sm" disabled={isBusy} onClick={onDmndSplmnt}>
<FilePen className="!size-5" />
보완요청
</Button>
)}
</div>
);
}
@@ -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}
/>
<iframe
@@ -316,6 +318,7 @@ interface HangulGiangiProps {
documentFileName: string;
onSaveDocument: (file: Blob) => Promise<void>;
onDownload: () => Promise<void>;
onDmndSplmnt?: () => void;
canSave?: boolean;
bridgeUrl?: string;
ctrlBaseUrl?: string;