diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/aplcnYms.ts b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/aplcnYms.ts index e8240aa5b..7f4acf616 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/aplcnYms.ts +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/aplcnYms.ts @@ -1,5 +1,7 @@ import {useState, useEffect} from 'react'; import { getData } from '@/components/lib/api'; +import moment from "moment"; + export type TInftwtAnls = { inftwtAnlsSn:number; aplcnYm: string; @@ -122,7 +124,7 @@ export const selectInftwtAnls = async(aplcnYm:String) => { // 침입수분석 }; export const getChartDataInftwtAnlsRslt = (arr: TInftwtAnlsRslt[]): { label: string; val: number }[] => { const options = arr.map(anlsRslt => ({ - label: anlsRslt.anlsTm.substring(4), + label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("DD HH:mm"), val: anlsRslt.predcOilq, })); return options; @@ -170,11 +172,13 @@ export const selectPplInftwtQty = async(inftwtAnlsSn:number, pplMngNo:string) => console.warn("inftwtAnlsSn or pplMngNo is not provided"); return []; } - const result = await getData(`v1/inftwtAnlsRslt/pplinftwtQty/${inftwtAnlsSn}/${pplMngNo}`); - if (result && result.numberOfElements > 0) { - console.log("selectPplInftwtQty", result.content); - return result.content; - } + const result = await getData(`v1/inftwtAnlsRslt/pplInftwtQty/${inftwtAnlsSn}/${pplMngNo}`); + if (result.length > 0) { + console.log("selectPplInftwtQty", result); + return result[0].avg_day_flow; + } else { + return 0; // 데이터가 없을 경우 0 반환 + } } catch (error) { console.error(error); } @@ -187,9 +191,9 @@ export const selectPplRdiiQty = async(rainInfwtAnlsSn:number, pplMngNo:string) = return []; } const result = await getData(`v1/rdiiAnlsRslt/pplRdiiQty/${rainInfwtAnlsSn}/${pplMngNo}`); - if (result && result.numberOfElements > 0) { - console.log("selectPplInftwtQty", result.content); - return result.content; + if (result?.sustaintime > 0) { + console.log("selectPplRdiiQty", result); + return result; } } catch (error) { console.error(error); @@ -219,14 +223,14 @@ export const selectRdiiAnls = async(aplcnYm:String) => { // 침입수분석 }; export const getChartDataRdiiAnlsRslt = (arr: TRdiiAnlsRslt[]): { label: string; val: number }[] => { const options = arr.map(anlsRslt => ({ - label: anlsRslt.anlsTm.substring(4), + label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("DD HH:mm"), val: anlsRslt.predcOilq, })); return options; }; export const getChartDataRdiiAnlsRsltMinq = (arr: TRdiiAnlsRslt[]): { label: string; val: number }[] => { const options = arr.map(anlsRslt => ({ - label: anlsRslt.anlsTm.substring(4), + label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("DD HH:mm"), val: anlsRslt.minq, })); return options; diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx index 97b0309e2..6f98705b1 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx @@ -3,6 +3,7 @@ import { CHART_FORM, type ChartConfig, MainChart, UiModal } from "@common/busine import { SelectBox } from '@common/business_components/ui'; import { Button, Input, Label } from '@common/components'; import React, { useEffect, useState } from 'react'; +import moment from "moment"; import type { TInftwtAnls, TOilqData } from './aplcnYms'; import * as fn from './aplcnYms'; @@ -16,19 +17,18 @@ type ModalOpenProp = { type DataType = {label: string; val: number}; const InfilChart = ({ btnTxt, isModalOpen, setIsModalOpen, closeModal, pplMngNo }: ModalOpenProp) => { - const [popupOpen, setPopupOpen] = React.useState(true); + const [popupOpen, setPopupOpen] = React.useState(false); // 조회조건팝업 const buttonRef = React.useRef(null); // 버튼 참조 const [popupPosition, setPopupPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 }); // 팝업 위치 - const [aplcnYm, setAplcnYm] = useState("0"); + const [aplcnYm, setAplcnYm] = useState(""); const [inftwtAnlss, setInftwtAnlss] = useState([]); const [inftwtAnlsInfos, setInftwtAnlsInfos] = useState([{ val: 0, text: '선택' }]); // 침입수분석정보 options const [inftwtAnls, setInftwtAnls] = useState(null); // 침입수분석정보 const [inftwtAnlsSn, setInftwtAnlsSn] = useState(0); // 침입수분석 정보 선택시 사용 const [chartData, setChartData] = useState<{ chartData1: DataType[]; chartData2: DataType[] }>({ - chartData1: [], chartData2: [{ label: "1", val: 15 }, - { label: "2", val: 35 }, - { label: "3", val: 55 },], + chartData1: [], chartData2: [], }); + const [inftwtQty, setInftwtQty] = useState(0); // 침입수량 const aplcnYms = fn.useAplcnYms('inftwtAnlsRslt'); // 상수사용월 데이터 가져오기 pplMngNo = "L"+pplMngNo; // "L13101003"; console.log("pplMngNo", pplMngNo); @@ -51,15 +51,14 @@ const InfilChart = ({ btnTxt, isModalOpen, setIsModalOpen, closeModal, pplMngNo useEffect(() => { if (isModalOpen === false) { - setPopupOpen(true); // 모달이 닫힐 때 조회조건팝업은 열어두기 + setPopupOpen(false); // 모달이 닫힐 때 조회조건팝업은 열어두기 setAplcnYm(""); // 상수사용월 초기화 setInftwtAnlss([]); // 침입수분석 초기화 setInftwtAnlsInfos([{ val: 0, text: '선택' }]); // 침입수분석정보 초기화 setInftwtAnls(null); // 침입수분석정보 초기화 setInftwtAnlsSn(0); // 침입수분석 일련번호 초기화 - setChartData({chartData1: [], chartData2: [{ label: "1", val: 15 }, - { label: "2", val: 35 }, - { label: "3", val: 55 },],}); // 차트 데이터 초기화 + setChartData({chartData1: [], chartData2: [],}); // 차트 데이터 초기화 + setInftwtQty(0); // 침입수량 초기화 } }, [isModalOpen]); @@ -119,31 +118,45 @@ const InfilChart = ({ btnTxt, isModalOpen, setIsModalOpen, closeModal, pplMngNo chartData2: [] // 유량 데이터 초기화 })); }); + fn.selectPplInftwtQty(inftwtAnlsSn, pplMngNo).then((data) => {// 침입수량 조회 + if (data > 0) { + setInftwtQty(data); + } + }); setPopupOpen(false); }; const renderBody = () => ( <> - {/* */} - {/* 분석 입력현황 조회 */} +
침입수분석결과
+
- - {/* 침입수량 분석결과 */} -
- 침입수량 분석결과 + {/* 상수사용량 적용연월 */} +
+ 상수사용량 적용연월
- 120 - m³/d + + {(aplcnYm)? moment(aplcnYm, "YYYYMM").format("YYYY-MM"):""} + +
+
+ {/* 침입수량 분석결과 */} +
+ 침입수량 분석결과 +
+ {inftwtQty} + m³/d
{/* 차트 */} -
- +
+
{popupOpen && ( @@ -243,15 +256,17 @@ const InfilChart = ({ btnTxt, isModalOpen, setIsModalOpen, closeModal, pplMngNo ) } diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx index 750a91dd6..96d07233f 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx @@ -3,20 +3,21 @@ import { CHART_FORM, type ChartConfig, MainChart, UiModal } from "@common/busine import { SelectBox } from '@common/business_components/ui'; import { Button, Input, Label } from '@common/components'; import React, { useEffect, useState } from 'react'; +import moment from "moment"; import type { TRdiiAnls, TRdiiAnlsRslt, TOilqData } from './aplcnYms'; -import { selectRdiiAnlsRslt, getChartDataRdiiAnlsRslt, getChartDataRdiiAnlsRsltMinq } from './aplcnYms'; -import { getOptionsRdiiAnlsInfo, selectRdiiAnls, useAplcnYms } from './aplcnYms'; +import * as fn from './aplcnYms'; type ModalOpenProp = { isModalOpen: boolean, setIsModalOpen: (open: boolean) => void, closeModal: () => void, btnTxt?: string, - pplMngNo?: string, // 관로관리번호 + pplMngNo: string, // 관로관리번호 } type DataType = {label: string; val: number}; + const RdiiChart = ({ btnTxt, isModalOpen, setIsModalOpen, closeModal, pplMngNo }: ModalOpenProp) => { - const [popupOpen, setPopupOpen] = React.useState(true); + const [popupOpen, setPopupOpen] = React.useState(false); // 조회조건팝업 const buttonRef = React.useRef(null); // 버튼 참조 const [popupPosition, setPopupPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 }); // 팝업 위치 const [aplcnYm, setAplcnYm] = useState(""); @@ -24,11 +25,12 @@ const RdiiChart = ({ btnTxt, isModalOpen, setIsModalOpen, closeModal, pplMngNo } const [rdiiAnlsInfos, setRdiiAnlsInfos] = useState([{ val: 0, text: '선택' }]); // 강우유입수분석정보 options const [rdiiAnls, setRdiiAnls] = useState(null); // 강우유입수분석정보 const [rainInfwtAnlsSn, setRainInfwtAnlsSn] = useState(0); // 강우유입수분석 정보 선택시 사용 - const [chartData, setChartData] = useState<{ chartData1: DataType[]; chartData2: DataType[] }>({ - chartData1: [], chartData2: [], + const [chartData, setChartData] = useState<{ chartData1: DataType[]; }>({ + chartData1: [], //chartData2: [], }); - - const aplcnYms = useAplcnYms('rdiiAnlsRslt'); // 상수사용월 데이터 가져오기 + const [rdiiQty, setRdiiQty] = useState(0); // 강우유입수량 + const [totalVolume, setTotalVolume] = useState(0); // 총체적 m³/mm + const aplcnYms = fn.useAplcnYms('rdiiAnlsRslt'); // 상수사용월 데이터 가져오기 pplMngNo = "L"+pplMngNo; // "L13101003"; console.log("pplMngNo", pplMngNo); const openPopup = () => { @@ -41,23 +43,24 @@ const RdiiChart = ({ btnTxt, isModalOpen, setIsModalOpen, closeModal, pplMngNo } } setPopupOpen(true); // 팝업 열기 }; - //console.log("btnTxt", btnTxt); const chartForm = CHART_FORM.LINE; const chartConfig = { - chartData1: { label: 'BSF', color: "orange", }, - chartData2: { label: '강우유입수예측유량', color: "red", }, + chartData1: { label: 'RDII(m³/h)', color: "red", }, + //chartData2: { label: '순오수량(m³/h)', color: "orange", }, } as ChartConfig; useEffect(() => { if (isModalOpen === false) { - setPopupOpen(true); // 모달이 닫힐 때 팝업은 + setPopupOpen(false); // 모달이 닫힐 때 팝업은 setAplcnYm(""); // 상수사용월 초기화 setRdiiAnlss([]); // 강우유입수분석 초기화 setRdiiAnlsInfos([{ val: 0, text: '선택' }]); // 강우유입수분석정보 초기화 setRdiiAnls(null); // 강우유입수분석정보 초기화 setRainInfwtAnlsSn(0); // 강우유입수분석 일련번호 초기화 - setChartData({chartData1: [], chartData2: []}); // 차트 데이터 초기화 + setChartData({chartData1: [], }); // 차트 데이터 초기화 + setRdiiQty(0); // 강우유입수량 초기화 + setTotalVolume(0); // 총체적 m³/mm 초기화 } }, [isModalOpen]); @@ -69,11 +72,11 @@ const RdiiChart = ({ btnTxt, isModalOpen, setIsModalOpen, closeModal, pplMngNo } setRdiiAnlsInfos([]); return; } - - selectRdiiAnls(aplcnYm) + console.log(moment(aplcnYm).format("YYYY-MM")); + fn.selectRdiiAnls(aplcnYm) .then((data) => { - setRdiiAnlss(data ?? []); - setRdiiAnlsInfos(getOptionsRdiiAnlsInfo(data)); + setRdiiAnlss(data ?? []); // 선택한 월의 분석성공한 RDII분석데이터[] + setRdiiAnlsInfos(fn.getOptionsRdiiAnlsInfo(data)); setRdiiAnls(null); // 오수전환율 이후 초기화 setRainInfwtAnlsSn(0); // 초기화 }) @@ -95,41 +98,57 @@ const RdiiChart = ({ btnTxt, isModalOpen, setIsModalOpen, closeModal, pplMngNo } }, [rainInfwtAnlsSn]); const closePopup = () => { // 팝업 닫기 - selectRdiiAnlsRslt(rainInfwtAnlsSn, pplMngNo).then((data) => {// 강우유입수분석 결과 조회 - const rsltData1 = getChartDataRdiiAnlsRsltMinq(data ?? []); - const rsltData2 = getChartDataRdiiAnlsRslt(data ?? []); + fn.selectRdiiAnlsRslt(rainInfwtAnlsSn, pplMngNo).then((data) => {// 강우유입수분석 결과 조회 + const rsltData1 = fn.getChartDataRdiiAnlsRslt(data ?? []); + //const rsltData2 = fn.getChartDataRdiiAnlsRsltMinq(data ?? []); setChartData(prev => ({ - chartData1: rsltData1, // BSF - chartData2: rsltData2 // 강우유입수분석결과 + chartData1: rsltData1, // 순오수량(m³/h) + // chartData2: rsltData2 // RDII(m³/h) 강우유입수분석결과 })); }); - + fn.selectPplRdiiQty(rainInfwtAnlsSn, pplMngNo).then((data) => {// 강우유입수량 조회 + setRdiiQty(Math.round(data.rdiih*24)); // cmd: m³/d 단위로 변환 + setTotalVolume(Math.round(data.rdiih * data.sustaintime)); // 총체적 m³/mm + }); setPopupOpen(false); }; - - const renderBody = () => ( <> - {/* 분석 입력현황 조회 */} +
RDII분석결과
+
- - {/* 강우유입수량 분석결과 */} -
- 강우유입수량 분석결과 + {/* 상수사용량 적용연월 */} +
+ 상수사용량 적용연월
- 120 + + {(aplcnYm)? moment(aplcnYm, "YYYYMM").format("YYYY-MM"):""} + +
+
+ + {/* 강우유입수량 분석결과 */} +
+ 강우유입수량 분석결과 +
+ {rdiiQty} m³/d
+
+ {totalVolume} + m³/mm +
{/* 차트 */} -
- +
+
{popupOpen && ( @@ -229,15 +248,17 @@ const RdiiChart = ({ btnTxt, isModalOpen, setIsModalOpen, closeModal, pplMngNo } ) } diff --git a/apps/sn/app/(with-base-layout)/ii/BaseLayout.tsx b/apps/sn/app/(with-base-layout)/ii/BaseLayout.tsx index f9596613f..86ae34f46 100644 --- a/apps/sn/app/(with-base-layout)/ii/BaseLayout.tsx +++ b/apps/sn/app/(with-base-layout)/ii/BaseLayout.tsx @@ -2,7 +2,7 @@ import { getData } from '@/components/lib/api' import ContentLayout from '@/components/ui/contentLayout' import { GovSidoSigunguSearchForm } from '@/components/ui/govSidoSigunguSearchForm' -import { DatePicker, Form, SearchFormRight, SelectBox } from '@common/business_components/ui' +import { DatePicker, DateMonthPicker, Form, SearchFormRight, SelectBox } from '@common/business_components/ui' import ListDiv from '@common/business_components/ui/list_div' import { SelectBox2 } from '@common/business_components/ui/select/index-2' import { Button, Label } from '@common/components/ui' @@ -51,8 +51,10 @@ type BaseLayout = { onSwageTreatmentPlantChange?: (value: string | number) => void; selectedSwageProcessingZone?: string | number; onSwageProcessingZoneChange?: (value: string | number) => void; - aplcnYm?: Date - setAplcnYm?: (aplcnYm: Date | undefined) => void; + aplcnYmFrom?: string + setAplcnYmFrom?: (aplcnYmFrom: string) => void; + aplcnYmTo?: string + setAplcnYmTo?: (aplcnYmTo: string) => void; isLoading?: boolean; // Excel 다운로드 관련 props 추가 onExcelDownload?: () => void; @@ -89,8 +91,10 @@ const BaseLayout = ({ onSwageTreatmentPlantChange, selectedSwageProcessingZone, onSwageProcessingZoneChange, - aplcnYm, - setAplcnYm, + aplcnYmFrom, + setAplcnYmFrom, + aplcnYmTo, + setAplcnYmTo, isLoading, // Excel 다운로드 관련 props 추가 onExcelDownload, @@ -99,7 +103,7 @@ const BaseLayout = ({ selectedRowData }: BaseLayout) => { - const defaultOption: ApiSelectOption = { val: '0', text: '선택' }; + const defaultOption: ApiSelectOption = { val: '0', text: '전체' }; const [processingZoneOptions, setProcessingZoneOptions] = useState([]); const [treatmentFacilityOptions, setTreatmentFacilityOptions] = useState([]); @@ -216,13 +220,37 @@ const BaseLayout = ({ {showDatePicker && ( // showDatePicker 값에 따라 DatePicker 렌더링 <> - console.log("DatePicker onChange:", date)) + value: aplcnYmFrom || (() => { // 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) => { + setAplcnYmFrom?.(value); // string 값을 직접 전달 + } }} - selectClass="ml-5 date-picker-trigger" /> + selectClass="ml-5 w-[100px]" + startYear={1900} // 시작 연도 설정 + /> + ~ + { // 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="ml-5 w-[100px]" + startYear={1900} // 시작 연도 설정 + /> )} { initialFileInfo // 3: 유량자료 등록 ]); - const [selectedDate, setSelectedDate] = useState(new Date()); + // selectedDate를 yyyyMM 문자열 형식으로 변경 + const [selectedDate, setSelectedDate] = useState(() => { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + return `${year}${month}`; + }); const [input1, setInput1] = useState(''); const [input2, setInput2] = useState(''); const [select1, setSelect1] = useState('0'); // 환경청코드 @@ -87,7 +93,11 @@ const IrSetting = () => { initialFileInfo, initialFileInfo ]); - setSelectedDate(new Date()); // 날짜 선택 초기화 + // selectedDate를 현재 연월로 초기화 + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + setSelectedDate(`${year}${month}`); setInput1(''); // 입력 필드 초기화 setInput2(''); // 입력 필드 초기화 setSelect1('0'); // 선택 필드 초기화 @@ -138,22 +148,6 @@ const IrSetting = () => { const handleAnalysisClick = async () => { setIsLoadingResult(true); // 로딩 상태 시작 - // 오수전환율 값 0 ~ 1 사이인지 검증 - if (input1 === '' || input1 === null || input1 === undefined) { - setIsLoadingResult(false); - console.error('오수전환율은 0에서 1 사이의 값으로 입력해야 합니다.'); - alert('오수전환율을 입력해주세요.'); - return; - } - - const numericInput1 = parseFloat(input1); - if (isNaN(numericInput1) || numericInput1 < 0 || numericInput1 > 1) { - setIsLoadingResult(false); - console.error('오수전환율은 0에서 1 사이의 값으로 입력해야 합니다.'); - alert('오수전환율은 0에서 1 사이의 값으로 입력해야 합니다.'); - return; - } - console.log(uploadedFilesData); const file1 = uploadedFilesData[0]; // 오수관망도 const file2 = uploadedFilesData[1]; // 수용가-맨홀 매칭결과 @@ -173,6 +167,22 @@ const IrSetting = () => { return; } + // 오수전환율 값 0 ~ 1 사이인지 검증 + if (input1 === '' || input1 === null || input1 === undefined) { + setIsLoadingResult(false); + console.error('오수전환율은 0에서 1 사이의 값으로 입력해야 합니다.'); + alert('오수전환율을 입력해주세요.'); + return; + } + + const numericInput1 = parseFloat(input1); + if (isNaN(numericInput1) || numericInput1 < 0 || numericInput1 > 1) { + setIsLoadingResult(false); + console.error('오수전환율은 0에서 1 사이의 값으로 입력해야 합니다.'); + alert('오수전환율은 0에서 1 사이의 값으로 입력해야 합니다.'); + return; + } + type MatchingResult = { anlsSn: number; mhlMngNo: string; rcptrNo: string; }; type TimePattern = { hrId: number; rt?: number | null | undefined; }; type FlowRate = { pplMngNo: string; mtrngDt: string; oilq?: number | null | undefined; }; @@ -236,7 +246,7 @@ const IrSetting = () => { swtrmtZoneCd: (select5 && select5 !== '0' && select5 !== 0) ? select5.toString().split('.').pop() || null // '.'으로 split해서 마지막 부분만 사용 : null, // 하수처리구역코드 (선택값) - aplcnYm: new Date(selectedDate).toISOString().substring(0, 7).replace("-", ""), // 적용연월 + aplcnYm: selectedDate, // 적용연월 drtwSwtcRt: parseFloat(input1) || null, // 오수전환율 (숫자로 변환) anlsInfo: input2, // 분석정보 atflCd: file1?.atflCd, // 첨부파일CD (오수관망도) @@ -316,19 +326,17 @@ const IrSetting = () => { - + {/* 연월 선택 라이브러리로 수정 필요 */} + { - if (date) { - setSelectedDate(date); - form.setValue('inputdate', date, { shouldValidate: true }); - } + value: selectedDate, // yyyyMM 형식 + onChange: (value: string) => { + setSelectedDate(value); } }} - selectClass="ml-5 date-picker-trigger" // date-picker.css의 스타일 적용 + selectClass="ml-5 w-[150px]" + startYear={1900} // 시작 연도 설정 /> { // 현재 연월을 yyyyMM 형식으로 설정 + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + return `${year}${month}`; + })(), }; // 각 파일 업로드 폼의 데이터를 저장할 인터페이스 정의 @@ -108,7 +113,6 @@ export default function RTKForm() { const [select4, setSelect4] = useState('0'); // 하수처리장 (swageTmplntCd) const [select5, setSelect5] = useState('0'); // 하수처리구역 (swtrmtZoneCd) - // form 인스턴스 생성 const form = useForm({ resolver: zodResolver(formSchema), @@ -163,23 +167,6 @@ export default function RTKForm() { // 침입수 분석 실행 버튼 클릭 핸들러 const handleAnalysisClick = async () => { setIsLoadingResult(true); // 로딩 상태 시작 - - // 오수전환율 값 0 ~ 1 사이인지 검증 - const rateValue = form.getValues().rate; - if (rateValue === '' || rateValue === null || rateValue === undefined) { - setIsLoadingResult(false); - console.error('오수전환율은 0에서 1 사이의 값으로 입력해야 합니다.'); - alert('오수전환율을 입력해주세요.'); - return; - } - - const numericInput1 = parseFloat(rateValue); - if (isNaN(numericInput1) || numericInput1 < 0 || numericInput1 > 1) { - setIsLoadingResult(false); - console.error('오수전환율은 0에서 1 사이의 값으로 입력해야 합니다.'); - alert('오수전환율은 0에서 1 사이의 값으로 입력해야 합니다.'); - return; - } const formData = form.getValues(); console.log(uploadedFilesData); @@ -198,6 +185,23 @@ export default function RTKForm() { return; } + // 오수전환율 값 0 ~ 1 사이인지 검증 + const rateValue = form.getValues().rate; + if (rateValue === '' || rateValue === null || rateValue === undefined) { + setIsLoadingResult(false); + console.error('오수전환율은 0에서 1 사이의 값으로 입력해야 합니다.'); + alert('오수전환율을 입력해주세요.'); + return; + } + + const numericInput1 = parseFloat(rateValue); + if (isNaN(numericInput1) || numericInput1 < 0 || numericInput1 > 1) { + setIsLoadingResult(false); + console.error('오수전환율은 0에서 1 사이의 값으로 입력해야 합니다.'); + alert('오수전환율은 0에서 1 사이의 값으로 입력해야 합니다.'); + return; + } + const filePickers = [ { file: file2, parser: excelToMatchingResultData }, { file: file3, parser: excelToRainData }, @@ -241,7 +245,7 @@ export default function RTKForm() { // 새로운 JSON 데이터 생성 const combinedAnalysisData = { enoCd: (select1 && select1 !== '0') ? select1.toString() : null, // 환경청코드 (선택값) - aplcnYm: new Date(formData.date).toISOString().substring(0, 7).replace("-", ""), // 적용연월 (YYYYMM 형식) + aplcnYm: formData.date, // 적용연월 (YYYYMM 형식) ctpvCd: (select2 && select2 !== '0') ? select2.toString() : null, // 시도코드 (선택값) sggCd: (select3.val && select3.val !== '0') ? select3.val.toString() : null, // 시군구코드 (선택값) swageTmplntCd: (select4 && select4 !== '0' && select4 !== 0) @@ -355,7 +359,7 @@ export default function RTKForm() { selectedSwageTreatmentPlant={select4} onSwageTreatmentPlantChange={setSelect4} selectedSwageProcessingZone={select5} - onSwageProcessingZoneChange={setSelect5} + onSwageProcessingZoneChange={setSelect4} isLoading={isLoadingResult} onAnalysis={handleAnalysisClick} > @@ -447,30 +451,16 @@ export default function RTKForm() { placeholder="예: 0.75" /> - - {/* { - if (date) { - form.setValue("date", date, { shouldValidate: true }); - } - }} - mode={"single"} - className="ml-5" - /> */} - + { - if (date) { - form.setValue("date", date, { shouldValidate: true }); - } + value: form.watch("date"), // form의 date 값을 직접 사용 + onChange: (value: string) => { + form.setValue("date", value, { shouldValidate: true }); // form의 date 필드에 직접 설정 } }} - // className을 직접 사용하거나, DatePicker 내부에서 selectClass를 처리하도록 수정 필요 - // 여기서는 DatePicker의 selectClass prop을 사용하고, date-picker.css의 스타일을 적용 - selectClass="ml-5 date-picker-trigger" + selectClass="ml-5 w-[200px]" + startYear={1900} // 시작 연도 설정 /> diff --git a/apps/sn/app/(with-base-layout)/ii/rdiiResult/page.tsx b/apps/sn/app/(with-base-layout)/ii/rdiiResult/page.tsx index bec7ffec6..12615c87c 100644 --- a/apps/sn/app/(with-base-layout)/ii/rdiiResult/page.tsx +++ b/apps/sn/app/(with-base-layout)/ii/rdiiResult/page.tsx @@ -18,12 +18,24 @@ const formSchema = z.object({ }).default({ val: '0', text: '전체' }), swageTmplntCd: z.string().default('0'), // 하수처리장 swtrmtZoneCd: z.string().default('0'), // 하수처리구역 - aplcnYm: z.date().default(new Date()), // 분석일자 (DatePicker와 연동) + aplcnYmFrom: z.string().default(() => { // Date에서 string(yyyyMM 형식)으로 변경 + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + return `${year}${month}`; + }), // 분석기간 시작연월 + aplcnYmTo: z.string().default(() => { // Date에서 string(yyyyMM 형식)으로 변경 + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + return `${year}${month}`; + }), // 분석기간 종료연월 }) type ResultFormValues = z.infer const tableColumn = [ + { id: 'rowNumber', title: '일련번호' }, { id: 'enoNm', title: '환경청' }, { id: 'ctpvNm', title: '시도' }, { id: 'sggNm', title: '시군구' }, @@ -35,7 +47,8 @@ const tableColumn = [ { id: 'cntbArea', title: 'RDII 기여면적' }, { id: 'anlsInfo', title: 'RDII 분석정보' }, { id: 'regDt', title: '분석일자' }, - { id: 'rgtrNm', title: '분석자' } + { id: 'rgtrNm', title: '분석자' }, + { id: 'scsYn', title: '분석성공여부' } ] const Result = () => { @@ -47,7 +60,18 @@ const Result = () => { sggCd: { val: '0', text: '전체' }, swageTmplntCd: '0', swtrmtZoneCd: '0', - aplcnYm: new Date(), + aplcnYmFrom: (() => { // 현재 연월을 yyyyMM 형식으로 설정 + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + return `${year}${month}`; + })(), + aplcnYmTo: (() => { // 현재 연월을 yyyyMM 형식으로 설정 + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + return `${year}${month}`; + })(), } }) @@ -67,21 +91,32 @@ const Result = () => { let inftwtAnlsSearchDto; + // swageTmplntCd 처리 로직 추가 + const processedSwageTmplntCd = formValues.swageTmplntCd && formValues.swageTmplntCd !== '0' + ? (() => { + const valString = String(formValues.swageTmplntCd); + const parts = valString.split('.'); + return parts.length > 1 ? parts[1] : valString; + })() + : null; + if (customFormValues) { inftwtAnlsSearchDto = { - "aplcnYm": formValues.aplcnYm, + "aplcnYmFrom": formValues.aplcnYmFrom, + "aplcnYmTo": formValues.aplcnYmTo, "enoCd": formValues.enoCd, "ctpvCd": formValues.ctpvCd, "sggCd": formValues.sggCd, - "swtrmtZoneCd": formValues.swtrmtZoneCd, + "swageTmplntCd": processedSwageTmplntCd, }; } else { inftwtAnlsSearchDto = { - "aplcnYm": formValues.aplcnYm ? formValues.aplcnYm.toISOString().substring(0, 7).replace("-", "") : null, + "aplcnYmFrom": formValues.aplcnYmFrom && formValues.aplcnYmFrom !== '' ? formValues.aplcnYmFrom : null, + "aplcnYmTo": formValues.aplcnYmTo && formValues.aplcnYmTo !== '' ? formValues.aplcnYmTo : null, "enoCd": formValues.enoCd === '0' ? null : formValues.enoCd, "ctpvCd": formValues.ctpvCd === '0' ? null : formValues.ctpvCd, "sggCd": formValues.sggCd?.val === '0' ? null : formValues.sggCd?.val, - "swtrmtZoneCd": formValues.swtrmtZoneCd === '0' ? null : formValues.swtrmtZoneCd, + "swageTmplntCd": processedSwageTmplntCd, }; } console.log("Search DTO:", inftwtAnlsSearchDto); @@ -101,10 +136,18 @@ const Result = () => { console.log("API: ", `v1/rdiiAnls${queryString ? `?${queryString}` : ''}`); try { const data = await getData(`v1/rdiiAnls${queryString ? `?${queryString}` : ''}`); - setTableData(data.content || []); - setTotalData(tableData.length); + // 일련번호를 추가한 데이터로 변환 + const dataWithRowNumber = (data.content || []).map((item: any, index: number) => ({ + ...item, + rowNumber: index + 1, // 1부터 시작하는 일련번호 추가 + scsYn: item.scsYn === null || item.scsYn === undefined ? 'N' : item.scsYn // scsYn이 null이면 'N'으로 변경 + })); + // setTableData(data.content || []); + // setTotalData(tableData.length); + setTableData(dataWithRowNumber); + setTotalData(dataWithRowNumber.length); console.log("response:", data); - console.log("response data:", data.content || []); + console.log("response data:", dataWithRowNumber); setIsLoadingResult(false); } catch (error) { setIsLoadingResult(false); @@ -198,11 +241,12 @@ const Result = () => { // 삭제 후 테이블 새로고침을 위해 재검색 const customFormValues: Partial = { - aplcnYm: selectedRowData.aplcnYm || null, + aplcnYmFrom: selectedRowData.aplcnYmFrom || null, + aplcnYmTo: selectedRowData.aplcnYmTo || null, enoCd: selectedRowData.enoCd || null, ctpvCd: selectedRowData.ctpvCd || null, sggCd: selectedRowData.sggCd || null, - swtrmtZoneCd: selectedRowData.swtrmtZoneCd || null, + swageTmplntCd: selectedRowData.swageTmplntCd || null, }; await handleSearchButtonClick(customFormValues); @@ -241,14 +285,10 @@ const Result = () => { onSwageTreatmentPlantChange={(val) => form.setValue("swageTmplntCd", String(val))} selectedSwageProcessingZone={form.watch("swtrmtZoneCd")} onSwageProcessingZoneChange={(val) => form.setValue("swtrmtZoneCd", String(val))} - aplcnYm={form.watch("aplcnYm")} - setAplcnYm={(val) => { - if (val instanceof Date) { - form.setValue("aplcnYm", val) - } else if (val) { - form.setValue("aplcnYm", new Date(val)) - } - }} + aplcnYmFrom={form.watch("aplcnYmFrom")} + setAplcnYmFrom={(val: string) => {form.setValue("aplcnYmFrom", val)}} + aplcnYmTo={form.watch("aplcnYmTo")} + setAplcnYmTo={(val: string) => {form.setValue("aplcnYmTo", val)}} isLoading={isLoadingResult} // Excel 다운로드 핸들러 추가 onExcelDownload={handleExcelDownload} diff --git a/apps/sn/app/(with-base-layout)/ii/result/page.tsx b/apps/sn/app/(with-base-layout)/ii/result/page.tsx index 988521eca..73bc6ecf2 100644 --- a/apps/sn/app/(with-base-layout)/ii/result/page.tsx +++ b/apps/sn/app/(with-base-layout)/ii/result/page.tsx @@ -1,6 +1,6 @@ 'use client' import { deleteData, getData } from '@/components/lib/api' -import { UiTable, downloadExcelWithKoreanHeaders } from '@common/business_components/ui' +import { UiTable, downloadExcel, downloadExcelWithKoreanHeaders } from '@common/business_components/ui' import { zodResolver } from '@hookform/resolvers/zod' import { useState } from 'react' import { useForm } from 'react-hook-form' @@ -18,12 +18,24 @@ const formSchema = z.object({ }).default({ val: '0', text: '전체' }), swageTmplntCd: z.string().default('0'), // 하수처리장 swtrmtZoneCd: z.string().default('0'), // 하수처리구역 - aplcnYm: z.date().default(new Date()), // 분석일자 (DatePicker와 연동) + aplcnYmFrom: z.string().default(() => { // Date에서 string(yyyyMM 형식)으로 변경 + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + return `${year}${month}`; + }), // 분석기간 시작연월 + aplcnYmTo: z.string().default(() => { // Date에서 string(yyyyMM 형식)으로 변경 + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + return `${year}${month}`; + }), // 분석기간 종료연월 }) type ResultFormValues = z.infer const tableColumn = [ + { id: 'rowNumber', title: '일련번호' }, { id: 'enoNm', title: '환경청' }, { id: 'ctpvNm', title: '시도' }, { id: 'sggNm', title: '시군구' }, @@ -33,7 +45,8 @@ const tableColumn = [ { id: 'drtwSwtcRt', title: '오수 전환율' }, { id: 'anlsInfo', title: '침입수 분석정보' }, { id: 'regDt', title: '분석일자' }, - { id: 'rgtrNm', title: '분석자' } + { id: 'rgtrNm', title: '분석자' }, + { id: 'scsYn', title: '분석성공여부' } ] const Result = () => { @@ -45,7 +58,18 @@ const Result = () => { sggCd: { val: '0', text: '전체' }, swageTmplntCd: '0', swtrmtZoneCd: '0', - aplcnYm: new Date(), + aplcnYmFrom: (() => { // 현재 연월을 yyyyMM 형식으로 설정 + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + return `${year}${month}`; + })(), + aplcnYmTo: (() => { // 현재 연월을 yyyyMM 형식으로 설정 + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + return `${year}${month}`; + })(), } }) @@ -65,21 +89,34 @@ const Result = () => { let inftwtAnlsSearchDto; + // swageTmplntCd 처리 로직 추가 + const processedSwageTmplntCd = formValues.swageTmplntCd && formValues.swageTmplntCd !== '0' + ? (() => { + const valString = String(formValues.swageTmplntCd); + const parts = valString.split('.'); + return parts.length > 1 ? parts[1] : valString; + })() + : null; + if (customFormValues) { inftwtAnlsSearchDto = { - "aplcnYm": formValues.aplcnYm, + "aplcnYmFrom": formValues.aplcnYmFrom, + "aplcnYmTo": formValues.aplcnYmTo, "enoCd": formValues.enoCd, "ctpvCd": formValues.ctpvCd, "sggCd": formValues.sggCd, - "swtrmtZoneCd": formValues.swtrmtZoneCd, + "swageTmplntCd": processedSwageTmplntCd, + // "swtrmtZoneCd": formValues.swtrmtZoneCd, }; } else { inftwtAnlsSearchDto = { - "aplcnYm": formValues.aplcnYm ? formValues.aplcnYm.toISOString().substring(0, 7).replace("-", "") : null, + "aplcnYmFrom": formValues.aplcnYmFrom && formValues.aplcnYmFrom !== '' ? formValues.aplcnYmFrom : null, + "aplcnYmTo": formValues.aplcnYmTo && formValues.aplcnYmTo !== '' ? formValues.aplcnYmTo : null, "enoCd": formValues.enoCd === '0' ? null : formValues.enoCd, "ctpvCd": formValues.ctpvCd === '0' ? null : formValues.ctpvCd, "sggCd": formValues.sggCd?.val === '0' ? null : formValues.sggCd?.val, - "swtrmtZoneCd": formValues.swtrmtZoneCd === '0' ? null : formValues.swtrmtZoneCd, + "swageTmplntCd": processedSwageTmplntCd, + // "swtrmtZoneCd": formValues.swtrmtZoneCd === '0' ? null : formValues.swtrmtZoneCd, }; } console.log("Search DTO:", inftwtAnlsSearchDto); @@ -99,10 +136,18 @@ const Result = () => { console.log("API: ", `v1/inftwtAnls${queryString ? `?${queryString}` : ''}`); try { const data = await getData(`v1/inftwtAnls${queryString ? `?${queryString}` : ''}`); - setTableData(data.content || []); - setTotalData(tableData.length); + // 일련번호를 추가한 데이터로 변환 + const dataWithRowNumber = (data.content || []).map((item: any, index: number) => ({ + ...item, + rowNumber: index + 1, // 1부터 시작하는 일련번호 추가 + scsYn: item.scsYn === null || item.scsYn === undefined ? 'N' : item.scsYn // scsYn이 null이면 'N'으로 변경 + })); + // setTableData(data.content || []); + // setTotalData(tableData.length); + setTableData(dataWithRowNumber); + setTotalData(dataWithRowNumber.length); console.log("response:", data); - console.log("response data:", data.content || []); + console.log("response data:", dataWithRowNumber); setIsLoadingResult(false); } catch (error) { setIsLoadingResult(false); @@ -198,11 +243,13 @@ const Result = () => { // 삭제 후 테이블 새로고침을 위해 재검색 const customFormValues: Partial = { - aplcnYm: selectedRowData.aplcnYm || null, + aplcnYmFrom: selectedRowData.aplcnYmFrom || null, + aplcnYmTo: selectedRowData.aplcnYmTo || null, enoCd: selectedRowData.enoCd || null, ctpvCd: selectedRowData.ctpvCd || null, sggCd: selectedRowData.sggCd || null, - swtrmtZoneCd: selectedRowData.swtrmtZoneCd || null, + swageTmplntCd: selectedRowData.swageTmplntCd || null, + // swtrmtZoneCd: selectedRowData.swtrmtZoneCd || null, }; await handleSearchButtonClick(customFormValues); @@ -241,14 +288,10 @@ const Result = () => { onSwageTreatmentPlantChange={(val) => form.setValue("swageTmplntCd", String(val))} selectedSwageProcessingZone={form.watch("swtrmtZoneCd")} onSwageProcessingZoneChange={(val) => form.setValue("swtrmtZoneCd", String(val))} - aplcnYm={form.watch("aplcnYm")} - setAplcnYm={(val) => { - if (val instanceof Date) { - form.setValue("aplcnYm", val) - } else if (val) { - form.setValue("aplcnYm", new Date(val)) - } - }} + aplcnYmFrom={form.watch("aplcnYmFrom")} + setAplcnYmFrom={(val: string) => {form.setValue("aplcnYmFrom", val)}} + aplcnYmTo={form.watch("aplcnYmTo")} + setAplcnYmTo={(val: string) => {form.setValue("aplcnYmTo", val)}} isLoading={isLoadingResult} // 로딩 상태 전달 // Excel 다운로드 핸들러 추가 onExcelDownload={handleExcelDownload} diff --git a/apps/sn/app/page.tsx b/apps/sn/app/page.tsx index 21d1c7b3f..d19bd5e79 100644 --- a/apps/sn/app/page.tsx +++ b/apps/sn/app/page.tsx @@ -1,51 +1,50 @@ -"use client"; import { Button } from "@common/components"; import Link from "next/link"; import { redirect } from "next/navigation"; export default function Home() { - //redirect("/map"); + redirect("/map"); - return ( -
-
-

하수관망도

-
- - - -
-
-
-

운영정보 관리

-
- - - - - - -
-
-
-

- I/I 분석(기초정보 설정) -

-
- - - - - - - - - - - - -
-
-
- ); + // return ( + //
+ //
+ //

하수관망도

+ //
+ // + // + // + //
+ //
+ //
+ //

운영정보 관리

+ //
+ // + // + // + // + // + // + //
+ //
+ //
+ //

+ // I/I 분석(기초정보 설정) + //

+ //
+ // + // + // + // + // + // + // + // + // + // + // + // + //
+ //
+ //
+ // ); } diff --git a/apps/sn/package.json b/apps/sn/package.json index fb8707352..95ec8f3e9 100644 --- a/apps/sn/package.json +++ b/apps/sn/package.json @@ -24,6 +24,7 @@ "chart.js": "^4.4.8", "dayjs": "^1.11.13", "js-cookie": "^3.0.5", + "moment": "^2.30.1", "next": "15.1.7", "react-chartjs-2": "^5.3.0", "react-dnd": "^16.0.1", diff --git a/packages/common/business_components/src/ui/chart/DoubleBar.tsx b/packages/common/business_components/src/ui/chart/DoubleBar.tsx index 9627485aa..b201b3bf9 100644 --- a/packages/common/business_components/src/ui/chart/DoubleBar.tsx +++ b/packages/common/business_components/src/ui/chart/DoubleBar.tsx @@ -7,6 +7,7 @@ import { } from "./lib/chart"; import type { CSSProperties } from "react"; +/* const customStyles = { legendContainer: { display: "flex", @@ -17,6 +18,22 @@ const customStyles = { marginTop: "10px", } as CSSProperties, }; +*/ + +const customStyles = { + legendContainer: { + display: "flex", + flexWrap: "wrap", + justifyContent: "center", + alignItems: "center", + gap: "12px", + fontSize: "13px", + fontWeight: "normal", + marginTop: "16px", + marginBottom: "8px", + minHeight: "32px", + } as CSSProperties, +}; type BarChartProps = { chartConfig: ChartConfig | undefined; @@ -60,7 +77,7 @@ const CustomTooltip = ({ }; // 커스텀 범례 (글씨 크기 키우기 + 스타일 추가) -const CustomLegend = (props: any) => { +/*const CustomLegend = (props: any) => { const { payload } = props; return (
@@ -72,6 +89,38 @@ const CustomLegend = (props: any) => { ))}
); +};*/ +const CustomLegend = (props: any) => { + const { payload } = props; + if (!payload) return null; + + return ( +
+ {payload.map((entry: any, index: number) => ( +
+
+ {entry.value} +
+ ))} +
+ ); }; export default function DoubleBarChart({chartConfig=undefined, chartData=[], chartTitle='차트', chartClassName='w-full h-full', graphSize=40, yFieldText='', showLegend=false} : BarChartProps) { @@ -83,7 +132,7 @@ export default function DoubleBarChart({chartConfig=undefined, chartData=[], cha .map(key => item[key]) ).flat()); }; - console.log(chartData); + const barMaxValue = getMaxValue(chartData, 'barVal'); return ( <> diff --git a/packages/common/business_components/src/ui/chart/DoughnutChart.tsx b/packages/common/business_components/src/ui/chart/DoughnutChart.tsx index 39dee3d9e..99395267a 100644 --- a/packages/common/business_components/src/ui/chart/DoughnutChart.tsx +++ b/packages/common/business_components/src/ui/chart/DoughnutChart.tsx @@ -48,6 +48,7 @@ type LegendProps = { }; /** 차트 기본 스타일 */ +/* const customStyles = { legendContainer: { display: "flex", @@ -58,6 +59,23 @@ const customStyles = { marginTop: "10px", } as CSSProperties, }; +*/ + +/** 차트 기본 스타일 (라인차트와 동일하게) */ +const customStyles = { + legendContainer: { + display: "flex", + flexWrap: "wrap", // 여러 줄로 자동 줄바꿈 + justifyContent: "center", + alignItems: "center", + gap: "12px", // 아이템 간 간격 + fontSize: "13px", + fontWeight: "normal", + marginTop: "16px", + marginBottom: "8px", + minHeight: "32px", // 범례 영역 높이 통일 + } as CSSProperties, +}; // 커스텀 툴팁 const CustomTooltip = ({ active, payload, label }: { @@ -82,6 +100,7 @@ const CustomTooltip = ({ active, payload, label }: { }; // 커스텀 범례 +/* const CustomLegend = (props: LegendProps) => { const { payload } = props; if (!payload) return null; @@ -97,12 +116,52 @@ const CustomLegend = (props: LegendProps) => {
); }; +*/ + +const CustomLegend = (props: LegendProps) => { + const { payload } = props; + if (!payload) return null; + + return ( +
+ {payload.map((entry: LegendEntry, index: number) => ( +
+
+ + {entry.payload?.label || entry.value} + +
+ ))} +
+ ); +}; export default function DoughnutChart(props: { chartConfig: any; chartData: ChartDataItem[] | undefined; chartTitle: string }) { + const legendData = props.chartData?.map(item => ({ + value: item.label, + fill: props.chartConfig?.[item.label]?.fill || 'blue', + })) ?? []; return ( @@ -130,9 +189,9 @@ export default function DoughnutChart(props: { nameKey="label" innerRadius={60} /> - +
diff --git a/packages/common/business_components/src/ui/chart/LineChart.tsx b/packages/common/business_components/src/ui/chart/LineChart.tsx index 4014702b6..28b4ceabf 100644 --- a/packages/common/business_components/src/ui/chart/LineChart.tsx +++ b/packages/common/business_components/src/ui/chart/LineChart.tsx @@ -18,6 +18,7 @@ type LineChartProps = { } /** 차트 기본 스타일 */ +/* const customStyles = { legendContainer: { display: "flex", @@ -28,6 +29,23 @@ const customStyles = { marginTop: "10px", } as CSSProperties, }; +*/ + +/** 차트 기본 스타일 */ +const customStyles = { + legendContainer: { + display: "flex", + flexWrap: "wrap", + justifyContent: "center", + alignItems: "center", + gap: "12px", + fontSize: "13px", + fontWeight: "normal", + marginTop: "16px", + marginBottom: "8px", + minHeight: "32px", + } as CSSProperties, +}; type LegendEntry = { value: string; @@ -60,7 +78,7 @@ const CustomTooltip = ({ active, payload, label }: { active?: boolean; payload?: }; // 커스텀 범례 (글씨 크기 키우기 + 스타일 추가) -const CustomLegend = (props: LegendProps) => { +/*const CustomLegend = (props: LegendProps) => { const { payload } = props; if (!payload) return null; @@ -74,6 +92,40 @@ const CustomLegend = (props: LegendProps) => { ))}
); +};*/ + +// 커스텀 범례 (도넛차트와 동일하게) +const CustomLegend = (props: LegendProps) => { + const { payload } = props; + if (!payload) return null; + + return ( +
+ {payload.map((entry: LegendEntry, index: number) => ( +
+
+ {entry.value} +
+ ))} +
+ ); }; type DataType = {label: string; val: string}; diff --git a/packages/common/business_components/src/ui/date_month_picker/index.tsx b/packages/common/business_components/src/ui/date_month_picker/index.tsx index d318b561f..1b97d9ce1 100644 --- a/packages/common/business_components/src/ui/date_month_picker/index.tsx +++ b/packages/common/business_components/src/ui/date_month_picker/index.tsx @@ -17,6 +17,7 @@ type MonthPickerProps = { field: { value: string; onChange: (date: string) => void }; // yyyyMM 문자열 selectClass?: string; readOnly?: boolean; + startYear?: number; // 시작 연도 props 추가 }; // yyyyMM → Date 객체 @@ -37,6 +38,7 @@ export const DateMonthPicker = ({ field, selectClass, readOnly = false, + startYear, }: MonthPickerProps) => { const [open, setOpen] = useState(false); @@ -75,6 +77,7 @@ export const DateMonthPicker = ({ diff --git a/packages/common/business_components/src/ui/date_month_picker/mothPicker.tsx b/packages/common/business_components/src/ui/date_month_picker/mothPicker.tsx index acf17fba4..2b64ab439 100644 --- a/packages/common/business_components/src/ui/date_month_picker/mothPicker.tsx +++ b/packages/common/business_components/src/ui/date_month_picker/mothPicker.tsx @@ -28,35 +28,41 @@ export type MonthPickerProps = { newMonth: Date | undefined, shouldClosePopover?: boolean ) => void; + startYear?: number; }; -export function MonthPicker({ currentMonth, onMonthChange }: MonthPickerProps) { +export function MonthPicker({ currentMonth, onMonthChange, startYear }: MonthPickerProps) { const today = startOfToday(); -const currentYear = today.getFullYear(); + const currentYear = today.getFullYear(); const yearFromProp = format(currentMonth, "yyyy"); -const selectedYear = parseInt(yearFromProp); + const selectedYear = parseInt(yearFromProp); // 현재 월의 시작 날짜 const fullYearStart = parse(yearFromProp, "yyyy", new Date()); const yearEnd = selectedYear === currentYear ? today : endOfYear(fullYearStart); - - - const yearRange = Array.from({ length: 21 }, (_, i) => currentYear - 20 + i); + + + // startYear가 없으면 기존과 동일하게 (현재년도 - 20년부터 현재년도까지) + // startYear가 있으면 해당 년도부터 현재년도까지, 내림차순으로 정렬 + const yearRange = startYear + ? Array.from({ length: currentYear - startYear + 1 }, (_, i) => currentYear - i) + : Array.from({ length: 21 }, (_, i) => currentYear - 20 + i); + const months = eachMonthOfInterval({ start: fullYearStart, end: yearEnd }); const getMonthButtonClass = (month: Date) => cn( "inline-flex h-9 w-16 items-center justify-center rounded-md p-0 text-sm font-normal ring-offset-white transition-colors hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 aria-selected:opacity-100 dark:ring-offset-slate-950 dark:hover:bg-slate-800 dark:hover:text-slate-50 dark:focus-visible:ring-slate-800", isEqual(month, currentMonth) && - "bg-slate-900 text-slate-50 hover:bg-slate-900 hover:text-slate-50 focus:bg-slate-900 focus:text-slate-50 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50 dark:hover:text-slate-900 dark:focus:bg-slate-50 dark:focus:text-slate-900", + "bg-slate-900 text-slate-50 hover:bg-slate-900 hover:text-slate-50 focus:bg-slate-900 focus:text-slate-50 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50 dark:hover:text-slate-900 dark:focus:bg-slate-50 dark:focus:text-slate-900", !isEqual(month, currentMonth) && - isEqual(month, getStartOfCurrentMonth()) && - "bg-slate-100 text-slate-900 dark:bg-slate-800 dark:text-slate-50" + isEqual(month, getStartOfCurrentMonth()) && + "bg-slate-100 text-slate-900 dark:bg-slate-800 dark:text-slate-50" ); return ( @@ -77,7 +83,7 @@ const selectedYear = parseInt(yearFromProp); onMonthChange(newDate, false); }} > - + @@ -112,7 +118,7 @@ const selectedYear = parseInt(yearFromProp); onClick={() => onMonthChange(month, true)} >
diff --git a/packages/common/business_components/src/ui/loadingBar/index.tsx b/packages/common/business_components/src/ui/loadingBar/index.tsx new file mode 100644 index 000000000..37991d1d2 --- /dev/null +++ b/packages/common/business_components/src/ui/loadingBar/index.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { LoadingWithAnimation } from '@common/components/ui/loading'; + +interface LoadingOverlayProps { + isLoading: boolean; +} + +const LoadingOverlay: React.FC = ({ isLoading }) => { + if (!isLoading) return null; + + return ( +
+
+ +
+
+ ); +}; + +export default LoadingOverlay; \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9c68525b..ab4cf5db0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -347,6 +347,9 @@ importers: js-cookie: specifier: ^3.0.5 version: 3.0.5 + moment: + specifier: ^2.30.1 + version: 2.30.1 next: specifier: 15.1.7 version: 15.1.7(react-dom@18.3.1)(react@18.3.1) @@ -2401,7 +2404,7 @@ packages: magic-string: 0.27.0 react-docgen-typescript: 2.2.2(typescript@5.6.3) typescript: 5.6.3 - vite: 6.2.0(@types/node@22.13.9) + vite: 6.2.0(@types/node@22.13.5) dev: true /@jridgewell/gen-mapping@0.3.8: @@ -2514,7 +2517,7 @@ packages: dnd-multi-backend: 8.1.2(dnd-core@16.0.1) framer-motion: 11.18.2(react-dom@18.3.1)(react@18.3.1) react: 18.3.1 - react-dnd: 16.0.1(@types/node@22.13.9)(@types/react@18.3.18)(react@18.3.1) + react-dnd: 16.0.1(@types/node@22.13.5)(@types/react@18.3.18)(react@18.3.1) react-dnd-html5-backend: 16.0.1 react-dnd-touch-backend: 16.0.1 react-dom: 18.3.1(react@18.3.1) @@ -3994,7 +3997,7 @@ packages: browser-assert: 1.2.1 storybook: 8.6.0(prettier@3.5.2) ts-dedent: 2.2.0 - vite: 6.2.0(@types/node@22.13.9) + vite: 6.2.0(@types/node@22.13.5) dev: true /@storybook/components@8.6.0(storybook@8.6.0): @@ -4126,7 +4129,7 @@ packages: resolve: 1.22.10 storybook: 8.6.0(prettier@3.5.2) tsconfig-paths: 4.2.0 - vite: 6.2.0(@types/node@22.13.9) + vite: 6.2.0(@types/node@22.13.5) transitivePeerDependencies: - rollup - supports-color @@ -4601,7 +4604,7 @@ packages: '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.9) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 6.2.0(@types/node@22.13.9) + vite: 6.2.0(@types/node@22.13.5) transitivePeerDependencies: - supports-color dev: true @@ -9787,7 +9790,7 @@ packages: debug: 4.4.0 globrex: 0.1.2 tsconfck: 3.1.5(typescript@5.6.3) - vite: 6.2.0(@types/node@22.13.9) + vite: 6.2.0(@types/node@22.13.5) transitivePeerDependencies: - supports-color - typescript