feat(sn): CS 기준 UI 통일 및 지도 팝업 표준화

This commit is contained in:
2026-08-20 14:57:20 +09:00
parent c9e3ff0d91
commit 0cbbc3b934
167 changed files with 21191 additions and 14161 deletions
+159 -118
View File
@@ -1,6 +1,6 @@
/**
* ➡️ 경로 : I/I 분석 모든 페이지
*
*
* 📄 Base Layout
* 분석 페이지 기본 레이아웃 컴포넌트
*
@@ -11,58 +11,57 @@
* - 양식 파일 다운로드 링크 제공
* - 폼 제출 및 이벤트 핸들링
*/
'use client'
import { DateMonthPicker, Form, SelectBox } from '@common/business_components/ui'
import ListDiv from '@common/business_components/ui/list_div'
import { Button, Label } from '@common/components/ui'
import { useEffect, type ReactNode } from 'react'
import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox'
import { confirmToast } from '@/components/ui/toast'
import { toast, Toaster } from "react-hot-toast";
"use client";
import { DateMonthPicker, Form } from "@common/business_components/ui";
import { Button, Label } from "@common/components/ui";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import type { ReactNode } from "react";
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpvSggPrcsFcltSelectBox";
type TitleName = {
mainTitle: string
firstCardTitle: string
secondCardTitle?: string
}
mainTitle: string;
firstCardTitle: string;
secondCardTitle?: string;
};
type SelectBox = {
labelNm?: Label
searchChk?: boolean
labelNm?: Label;
searchChk?: boolean;
title?: string;
onSearchClick?: () => Promise<void>;
}
onResetClick?: () => void | Promise<void>;
};
type Label = {
first: string
second: string
third: string
fourth: string
}
first: string;
second: string;
third: string;
fourth: string;
};
// API로부터 받아올 데이터 타입을 정의 (SelectBox에 사용될 형태)
interface ApiSelectOption {
text: string;
val: string;
}
type BaseLayout = {
children?: ReactNode[]
titleName: TitleName
form: any
onSubmitEvent: Function
graphBtn?: boolean
analysisBtn?: string
resultViewBtn?: string
firstDivBtn?: boolean
selectBox?: SelectBox
selectionComponent?: ReactNode
children?: ReactNode[];
titleName: TitleName;
form: any;
onSubmitEvent: Function;
graphBtn?: boolean;
analysisBtn?: string;
resultViewBtn?: string;
firstDivBtn?: boolean;
selectBox?: SelectBox;
selectionComponent?: ReactNode;
showDatePicker?: boolean;
ctpvCd: string;
sggCd: string;
sggName?: string;
sigungu?: ApiSelectOption
sigungu?: ApiSelectOption;
selectedSwagePrcsFclt?: string;
onSwagePrcsFcltChange?: (value: string) => void;
aplcnYmFrom?: string
aplcnYmFrom?: string;
setAplcnYmFrom?: (aplcnYmFrom: string) => void;
aplcnYmTo?: string
aplcnYmTo?: string;
setAplcnYmTo?: (aplcnYmTo: string) => void;
setCtpvCd: (ctpv: string) => void;
setSggCd: (sgg: string) => void;
@@ -72,12 +71,12 @@ type BaseLayout = {
setIsLoading?: (isLoading: boolean) => void;
onExcelDownload?: () => void;
onDeleteFile?: () => void;
onAnalysis?: () => void;
onAnalysis?: () => void | Promise<void>;
onResultView?: () => void;
selectedRowData?: any;
isIr?: boolean;
isRdii?: boolean;
}
};
const BaseLayout = ({
children,
@@ -113,14 +112,8 @@ const BaseLayout = ({
isIr = false,
isRdii = false,
}: BaseLayout) => {
// 컴포넌트 언마운트 시 confirmToast 정리
useEffect(() => {
return () => {
// 컴포넌트가 언마운트될 때 global-confirm-toast ID를 가진 Toast 제거
toast.dismiss("global-confirm-toast");
};
}, []);
// 분석 실행 전 CS와 동일한 전역 확인 대화상자를 호출한다.
const { confirm } = useConfirm();
// 파일 다운로드
const isChckFile = () => {
@@ -129,8 +122,8 @@ const BaseLayout = ({
onExcelDownload();
return;
}
return true
}
return true;
};
// 파일 삭제
const isDeleteFile = async () => {
// Excel 다운로드 함수가 있으면 Excel 다운로드 실행
@@ -138,64 +131,75 @@ const BaseLayout = ({
onDeleteFile();
return;
}
}
};
const handleConfirm = () => {
// 기존 confirmToast가 있다면 먼저 제거
toast.dismiss("global-confirm-toast");
confirmToast(
const handleConfirm = async () => {
const shouldRunAnalysis = await confirm(
"분석에는 2분 내외의 시간이 소요될 수 있습니다.\n분석을 진행하시겠습니까?",
onAnalysis ?? (() => { }),
"text-xl font-medium text-gray-800"
);
if (!shouldRunAnalysis) return;
await onAnalysis?.();
};
return (
<>
<Form {...form} className="min-h-screen flex flex-col">
<ListDiv>
<Form {...form} className="flex min-h-screen flex-col">
<section className="card card-border">
{/* searchBox.title이 있으면 h3 태그로 제목을 표시합니다. */}
{selectBox?.title && (
<div className="card-header">
<h3 className="text-2xl font-medium">{selectBox.title}</h3>
<h3 className="text-2xl font-medium text-label">
{selectBox.title}
</h3>
</div>
)}
<div className="ml-2 mt-2 flex items-center w-full">
<div className="flex w-full flex-wrap items-center gap-3 p-[1.2rem]">
{showDatePicker && ( // showDatePicker 값에 따라 DatePicker 렌더링
<>
<Label className="mr-4 w-20 text-xl font-medium text-gray-600 whitespace-nowrap">상수사용기간</Label>
<Label className="whitespace-nowrap text-label">
상수사용기간
</Label>
<DateMonthPicker
field={{
value: aplcnYmFrom || (() => { // aplcnYm이 없으면 현재 연월을 기본값으로 설정
const now = new Date();
const year = now.getFullYear();
// const month = String(now.getMonth() + 1).padStart(2, '0');
// return `${year}${month}`;
return `${year}01`;
})(), // yyyyMM 형식
value:
aplcnYmFrom ||
(() => {
// aplcnYm이 없으면 현재 연월을 기본값으로 설정
const now = new Date();
const year = now.getFullYear();
// const month = String(now.getMonth() + 1).padStart(2, '0');
// return `${year}${month}`;
return `${year}01`;
})(), // yyyyMM 형식
onChange: (value: string) => {
setAplcnYmFrom?.(value); // string 값을 직접 전달
}
},
}}
selectClass="ml-8 w-[100px]"
startYear={1900} // 시작 연도 설정
selectClass="w-[10rem]"
startYear={1900} // 시작 연도 설정
/>
<span className="font-semibold text-xl mx-4">~</span>
<span className="text-muted-foreground">~</span>
<DateMonthPicker
field={{
value: aplcnYmTo || (() => { // aplcnYm이 없으면 현재 연월을 기본값으로 설정
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
return `${year}${month}`;
})(), // yyyyMM 형식
value:
aplcnYmTo ||
(() => {
// aplcnYm이 없으면 현재 연월을 기본값으로 설정
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(
2,
"0",
);
return `${year}${month}`;
})(), // yyyyMM 형식
onChange: (value: string) => {
setAplcnYmTo?.(value); // string 값을 직접 전달
}
},
}}
selectClass="w-[100px] mr-8"
startYear={1900} // 시작 연도 설정
selectClass="w-[10rem]"
startYear={1900} // 시작 연도 설정
/>
</>
)}
@@ -207,7 +211,7 @@ const BaseLayout = ({
sggName={sggName}
onSidoChange={(sidoId: string) => {
setCtpvCd(sidoId);
setSggCd('0');
setSggCd("0");
}}
onSigunguChange={setSggCd}
setSggName={setSggName}
@@ -219,86 +223,123 @@ const BaseLayout = ({
/>
)}
{selectBox?.searchChk && (
<div className='flex flex-1 justify-end mr-6'>
<Button size="sm" onClick={() => selectBox.onSearchClick?.()}>조회</Button>
<div className="ml-auto flex items-center gap-2">
<Button
type="button"
onClick={() => selectBox.onSearchClick?.()}
>
조회
</Button>
{selectBox.onResetClick ? (
<Button
type="button"
color="white"
onClick={() => selectBox.onResetClick?.()}
>
초기화
</Button>
) : null}
</div>
)}
</div>
</ListDiv>
<ListDiv>
</section>
<section className="card card-border">
<div className="card-header">
<h3 className="text-2xl font-medium">{titleName.firstCardTitle}</h3>
<div>
<h3 className="text-2xl font-medium text-label">
{titleName.firstCardTitle}
</h3>
<div className="flex flex-wrap items-center gap-2">
{graphBtn && (
<Button className="px-4 py-2 !mr-[1.2rem] bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size="sm" onClick={isChckFile}>파일 다운로드</Button>
)}
{/* 그래프 있을 시 */}
{graphBtn && (
<Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size="sm" onClick={isDeleteFile}>삭제</Button>
<Button type="button" onClick={isChckFile}>
엑셀다운로드
</Button>
)}
{/* 침입수 분석 */}
{isIr && (
<Button
size="sm"
className="mx-4 py-2 text-white rounded-sm text-lg font-medium green"
>
<a href="/samples/ir_setting_sample.zip" download="침입수 분석 업로드 파일 양식.zip">양식 받기</a>
<Button type="button">
<a
href="/samples/ir_setting_sample.zip"
download="침입수 분석 업로드 파일 양식.zip"
>
양식다운로드
</a>
</Button>
)}
{/* RDII 분석 */}
{isRdii && (
<Button
size="sm"
className="mx-4 py-2 text-white rounded-sm text-lg font-medium green"
>
<a href="/samples/rdii_sample.zip" download="RDII 분석 업로드 파일 양식.zip">양식 받기</a>
<Button type="button">
<a
href="/samples/rdii_sample.zip"
download="RDII 분석 업로드 파일 양식.zip"
>
양식다운로드
</a>
</Button>
)}
{/* 분석 버튼 */}
{analysisBtn && (
<Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size="sm" onClick={handleConfirm}>{analysisBtn}</Button>
<Button type="button" onClick={handleConfirm}>
{analysisBtn}
</Button>
)}
{resultViewBtn && (
<Button
className="px-4 py-2 bg-slate-600 text-white rounded-sm text-sm font-medium hover:bg-slate-700 !mr-[1.2rem]"
size="sm"
type="button"
color="white"
onClick={onResultView}
>
{resultViewBtn}
</Button>
)}
{graphBtn && (
<Button
type="button"
color="red"
onClick={isDeleteFile}
>
삭제
</Button>
)}
</div>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmitEvent)} className="flex-grow flex flex-col">
<div className="card !m-[1.2rem] flex-grow flex flex-col">
<form
onSubmit={form.handleSubmit(onSubmitEvent)}
className="flex flex-grow flex-col"
>
<div className="min-w-0 flex-grow p-[1.2rem]">
{/* 폼 정보 입력 칸 */}
{children?.[0]}
</div>
</form>
</Form>
</ListDiv>
</section>
{children?.[1] != null && (
<ListDiv>
<section className="card card-border">
<div className="card-header flex items-center">
<h3 className="text-2xl font-medium">{titleName?.secondCardTitle}</h3>
<div className='flex flex-1 justify-end mr-8'>
<h3 className="text-2xl font-medium text-label">
{titleName?.secondCardTitle}
</h3>
<div className="ml-auto flex items-center gap-2">
{graphBtn && (
<Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size="sm">파일 다운로드</Button>
<Button type="button">
엑셀다운로드
</Button>
)}
{/* 그래프 있을 시 */}
{graphBtn && (
<Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size="sm">삭제</Button>
<Button type="button" color="red">
삭제
</Button>
)}
</div>
</div>
<div className="card !m-[1.2rem]">{children?.[1]}</div>
</ListDiv>
<div className="p-[1.2rem]">{children?.[1]}</div>
</section>
)}
</Form>
<Toaster position="top-center"></Toaster>
</>
)
}
);
};
export default BaseLayout
export default BaseLayout;