[as] feat: 보완 요청 사항 적용
- 실무협의서 내 보완요청 버튼 추가 - 대시보드 수정
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user