Merge branch 'iipbs/sn/ii' into iipbs/sn/dev

This commit is contained in:
swcho
2025-07-03 20:46:34 +09:00
11 changed files with 470 additions and 143 deletions
@@ -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<ApiSelectOption[]>([]);
const [treatmentFacilityOptions, setTreatmentFacilityOptions] = useState<ApiSelectOption[]>([]);
@@ -216,13 +220,37 @@ const BaseLayout = ({
{showDatePicker && ( // showDatePicker 값에 따라 DatePicker 렌더링
<>
<Label className="ml-4 w-20 text-xl text-gray-600 whitespace-nowrap">분석기간</Label>
<DatePicker
mode={"single"}
<DateMonthPicker
field={{
value: aplcnYm ?? new Date(),
onChange: setAplcnYm ?? ((date: Date | undefined) => 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} // 시작 연도 설정
/>
<span className="font-semibold text-xl mx-2">~</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 형식
onChange: (value: string) => {
setAplcnYmTo?.(value); // string 값을 직접 전달
}
}}
selectClass="ml-5 w-[100px]"
startYear={1900} // 시작 연도 설정
/>
</>
)}
<GovSidoSigunguSearchForm
@@ -2,7 +2,7 @@
import { postData } from '@/components/lib/api'
import FileUploadForm from '@/components/ui/fileUploadForm'
import FormSection from '@/components/ui/formSection'
import { DatePicker, filePicked } from '@common/business_components/ui'
import { DateMonthPicker, filePicked } from '@common/business_components/ui'
import ListDiv from '@common/business_components/ui/list_div'
import { Input } from '@common/components/ui'
import { zodResolver } from '@hookform/resolvers/zod'
@@ -59,7 +59,13 @@ const IrSetting = () => {
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 = () => {
<FormSection
name='inputdate'
form={form}
titleNm='▷ 상수사용량 적용기간'>
<DatePicker
mode={"single"}
titleNm='▷ 상수사용량 적용월'>
{/* 연월 선택 라이브러리로 수정 필요 */}
<DateMonthPicker
field={{
value: selectedDate, // 현재 페이지의 selectedDate 상태 사용
onChange: (date: Date | undefined) => {
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} // 시작 연도 설정
/>
</FormSection>
<FormSection
+65 -46
View File
@@ -4,7 +4,7 @@ import { postData } from '@/components/lib/api'
import FileUploadForm from "@/components/ui/fileUploadForm"
import FormSection from "@/components/ui/formSection"
import BaseLayout from "../BaseLayout"
import { DatePicker, filePicked } from "@common/business_components/ui"
import { DateMonthPicker, filePicked } from "@common/business_components/ui"
import { Input } from "@common/components/ui"
import { zodResolver } from "@hookform/resolvers/zod"
import { Label } from '@radix-ui/react-label'
@@ -38,11 +38,11 @@ const formSchema = z.object({
})),
// osumap: z.instanceof(File).optional(), // 오수관망도
// matching: z.instanceof(File).optional(), // 수용가-맨홀 매칭결과
aplcnYm: z.string(), // 상수사용량 적용기간 (YYYYMM 형식)
aplcnYm: z.string(), // 상수사용량 적용월 (YYYYMM 형식)
rate: z.string(), // 오수전환율
// rain: z.instanceof(File).optional(), // 강우자료
analysisInfo: z.string().optional(), // 분석 정보
date: z.date(),
date: z.string(), // Date 객체에서 string(yyyyMM 형식)으로 변경
// date: z.coerce.date(),
})
// zod 스키마 타입 추론
@@ -72,7 +72,12 @@ const defaultFormValues: FormSchemaType = {
rate: "0.8", // 문자열로 기본값 설정
// rainFile: undefined,
analysisInfo: "test 분석정보",
date: new Date(),
date: (() => { // 현재 연월을 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<string | number>('0'); // 하수처리장 (swageTmplntCd)
const [select5, setSelect5] = useState<string | number>('0'); // 하수처리구역 (swtrmtZoneCd)
// form 인스턴스 생성
const form = useForm<FormSchemaType>({
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)
@@ -337,6 +341,35 @@ export default function RTKForm() {
console.log('Page changed:', page)
}
// 선택된 값을 처리하는 공통 함수
const handleSelectValueChange = (
val: string | number,
setter: (value: string | number) => void
) => {
// val이 0이 아닐 경우 .을 기준으로 슬라이싱해서 두번째 값을 설정
if (val && val !== '0' && val !== 0) {
const valString = String(val);
const parts = valString.split('.');
if (parts.length > 1 && parts[1] !== undefined) {
setter(parts[1]); // 두 번째 값 (인덱스 1)
} else {
setter(val); // .이 없으면 원래 값 사용
}
} else {
setter('0'); // val이 0이면 '0' 설정
}
};
// 하수처리장 변경 핸들러
const handleSwageTreatmentPlantChange = (val: string | number) => {
handleSelectValueChange(val, setSelect4);
};
// 하수처리구역 변경 핸들러
const handleSwageProcessingZoneChange = (val: string | number) => {
handleSelectValueChange(val, setSelect5);
};
return (
// <BaseLayout searchBox={{ title: '분석 대상지역 선택' }} titleName={{ mainTitle: 'RDII 분석', firstCardTitle: 'RDII 분석자료 입력', secondCardTitle: '실측자료 등록' }} form={form} onSubmitEvent={() => { }} graphBtn={false} >
<BaseLayout
@@ -353,9 +386,9 @@ export default function RTKForm() {
sigungu={select3}
setSigungu={setSelect3}
selectedSwageTreatmentPlant={select4}
onSwageTreatmentPlantChange={setSelect4}
onSwageTreatmentPlantChange={handleSwageTreatmentPlantChange}
selectedSwageProcessingZone={select5}
onSwageProcessingZoneChange={setSelect5}
onSwageProcessingZoneChange={handleSwageProcessingZoneChange}
isLoading={isLoadingResult}
onAnalysis={handleAnalysisClick}
>
@@ -447,30 +480,16 @@ export default function RTKForm() {
placeholder="예: 0.75"
/>
</FormSection>
<FormSection name='date' form={form} titleNm='○ 상수사용량 적용기간'>
{/* <DatePicker
selected={form.watch("date")}
onSelect={(date: Date | undefined) => {
if (date) {
form.setValue("date", date, { shouldValidate: true });
}
}}
mode={"single"}
className="ml-5"
/> */}
<DatePicker
mode={"single"} // mode는 DayPickerSingleProps에서 오므로 그대로 사용 가능
<FormSection name='date' form={form} titleNm='○ 상수사용량 적용월'>
<DateMonthPicker
field={{
value: form.watch("date"), // react-hook-form의 "date" 필드 값 사용
onChange: (date: Date | undefined) => {
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} // 시작 연도 설정
/>
</FormSection>
<FormSection name='rate' form={form} titleNm='○ 분석 정보'>
@@ -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<typeof formSchema>
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<ResultFormValues> = {
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}
@@ -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<typeof formSchema>
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<ResultFormValues> = {
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}
@@ -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 (
<div style={customStyles.legendContainer}>
@@ -72,6 +89,38 @@ const CustomLegend = (props: any) => {
))}
</div>
);
};*/
const CustomLegend = (props: any) => {
const { payload } = props;
if (!payload) return null;
return (
<div style={customStyles.legendContainer}>
{payload.map((entry: any, index: number) => (
<div
key={`legend-${index}`}
className="flex items-center"
style={{
minWidth: "90px",
margin: "2px 0",
}}
>
<div
style={{
width: "14px",
height: "14px",
backgroundColor: entry.color,
borderRadius: "3px",
marginRight: "8px",
minWidth: "14px",
minHeight: "14px",
}}
></div>
<span style={{ fontSize: "13px", whiteSpace: "nowrap" }}>{entry.value}</span>
</div>
))}
</div>
);
};
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 (
<>
@@ -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) => {
</div>
);
};
*/
const CustomLegend = (props: LegendProps) => {
const { payload } = props;
if (!payload) return null;
return (
<div style={customStyles.legendContainer}>
{payload.map((entry: LegendEntry, index: number) => (
<div
key={`legend-${index}`}
className="flex items-center"
style={{
minWidth: "90px",
margin: "2px 0",
}}
>
<div
style={{
width: "14px",
height: "14px",
backgroundColor: entry.fill,
borderRadius: "3px",
marginRight: "8px",
minWidth: "14px",
minHeight: "14px",
}}
></div>
<span style={{ fontSize: "13px", whiteSpace: "nowrap" }}>
{entry.payload?.label || entry.value}
</span>
</div>
))}
</div>
);
};
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 (
<Card className="flex flex-col">
<CardContent className="flex-1 pb-0">
@@ -130,9 +189,9 @@ export default function DoughnutChart(props: {
nameKey="label"
innerRadius={60}
/>
<CustomLegend />
</PieChart>
</ChartContainer>
<CustomLegend payload={legendData} />
</div>
</CardContent>
</Card>
@@ -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) => {
))}
</div>
);
};*/
// 커스텀 범례 (도넛차트와 동일하게)
const CustomLegend = (props: LegendProps) => {
const { payload } = props;
if (!payload) return null;
return (
<div style={customStyles.legendContainer}>
{payload.map((entry: LegendEntry, index: number) => (
<div
key={`legend-${index}`}
className="flex items-center"
style={{
minWidth: "90px",
margin: "2px 0",
}}
>
<div
style={{
width: "14px",
height: "14px",
backgroundColor: entry.color,
borderRadius: "3px",
marginRight: "8px",
minWidth: "14px",
minHeight: "14px",
}}
></div>
<span style={{ fontSize: "13px", whiteSpace: "nowrap" }}>{entry.value}</span>
</div>
))}
</div>
);
};
type DataType = {label: string; val: string};
@@ -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 = ({
<MonthPicker
currentMonth={parsedDate}
onMonthChange={handleSelect}
startYear={startYear}
/>
</PopoverContent>
</Popover>
@@ -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);
}}
>
<SelectTrigger className="w-[120px] h-7">
<SelectTrigger className="w-[140px] h-9 text-base">
<SelectValue placeholder="년도" />
</SelectTrigger>
<SelectContent>
@@ -112,7 +118,7 @@ const selectedYear = parseInt(yearFromProp);
onClick={() => onMonthChange(month, true)}
>
<time dateTime={format(month, "yyyy-MM-dd")}>
{format(month, 'MMM', { locale: ko })}
{format(month, 'MMM', { locale: ko })}
</time>
</button>
</div>
@@ -0,0 +1,20 @@
import React from 'react';
import { LoadingWithAnimation } from '@common/components/ui/loading';
interface LoadingOverlayProps {
isLoading: boolean;
}
const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ isLoading }) => {
if (!isLoading) return null;
return (
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
<div className="relative w-16 h-16">
<LoadingWithAnimation color="primary" />
</div>
</div>
);
};
export default LoadingOverlay;