Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev
iipbs_sn / build (push) Waiting to run

This commit is contained in:
yschoi
2025-05-27 16:37:27 +09:00
9 changed files with 602 additions and 225 deletions
+10 -5
View File
@@ -9,8 +9,13 @@ export const postData = async (url: string, data: object): Promise<any> => {
body: JSON.stringify(data),
};
const res = await fetch(`${BASE_URL}/${url}`, options);
const resData = await res.json();
return resData;
if (res.ok) {
const text = await res.text();
if (text) {
return JSON.parse(text); // JSON 데이터가 있으면 파싱하여 반환
}
return true; // 응답 본문이 비어 있으면 true 반환
}
};
export const postMutiPart = async (url: string, data: FormData): Promise<any> => {
@@ -55,7 +60,7 @@ export const deleteData = async (url: string, data?: object): Promise<any> => {
},
body: data ? JSON.stringify(data) : undefined,
};
const res = await fetch(`${BASE_URL}/${url}`, options);
const resData = await res.json();
return resData;
const res = await fetch(url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`, options);
if(res.status === 204) return true
return res;
};
@@ -10,5 +10,5 @@ export const searchData = async<T extends Record<string, any>>(url: string, para
}, {} as Record<string, string>)
);
const data = await getData(`${url}?${params.toString()}`);
return data && data.content
return data && {data : data.content, total : data.totalElements}
}
+125 -86
View File
@@ -1,105 +1,144 @@
"use client"
import { SelectBox } from '@common/business_components/ui';
import { Button } from '@common/components/ui';
import React, { useState } from 'react'
import type { HttpResponse, Upload } from "tus-js-client";
import { FileUploader } from "@common/components";
import { Button } from '@common/components/ui';
import { handleUpload } from "@utils";
import { set } from 'react-hook-form';
import { useId, useState } from 'react';
import type { HttpResponse, Upload } from "tus-js-client";
const FileUploadForm = ({ title, onSubmit }: { title: string, onSubmit: (files: string, year: string, quarter: string, code:string, id:string) => void }) => {
const [atflCd, setAtflCd] = useState<string | null>(null);
const [atflId, setAtflId] = useState<string | null>(null);
const handleUploadResponse = (file: File) => async (res: HttpResponse) => {
const fileInfo = res.getHeader("X-File-Info");
if (!fileInfo) return;
const { atflCd, atflId } = JSON.parse(fileInfo) ?? {};
const orgnlFileNm = file.name;
console.log('orgnlFileNm', orgnlFileNm, atflCd, atflId);
setAtflCd(atflCd);
setAtflId(atflId);
};
interface FileUploadFormProps {
onSubmit?: (files: string, year: string, quarter: string, code: string, id: string) => void;
onFileSelect?: (file: File | null, atflCd: string | null, atflId: string | null) => void; // 파일 선택 시 호출될 콜백 추가
showButton?: boolean; // 버튼 표시 여부 (기본값 true)
showPreview?: boolean; // 파일명 프리뷰 표시 여부
accept?: string; // 파일 형식 지정
fileIdx?: number; // 부모가 어떤 FileUploadForm인지 구분하기 위한 인덱스
}
const FileUploadForm = ({
onSubmit,
onFileSelect,
showButton = true,
showPreview = false,
accept = ".jpeg, .jpg, .png",
fileIdx // props에서 fileIdx 받기
}: FileUploadFormProps) => {
const [internalAtflCd, setInternalAtflCd] = useState<string | null>(null);
const [internalAtflId, setInternalAtflId] = useState<string | null>(null);
const [currentFile, setCurrentFile] = useState<File | null>(null); // 현재 선택된 파일을 저장할 상태
const [fileName, setFileName] = useState<string>('');
const [files, setFiles] = useState<FileList | null>(null);
const [upload, setUpload] = useState<Upload | null>(null);
const uniqueId = useId(); // 각 FileUploadForm 인스턴스에 대한 고유 ID 생성
const quater = [{ text: '1분기', val: '1' }, { text: '2분기', val: '2' }, { text: '3분기', val: '3' }, { text: '4분기', val: '4' }];
// 현재 년도부터 10년 전까지의 년도 옵션 생성
const yearOptions = Array.from({ length: 10 }, (_, i) => {
const year = new Date().getFullYear() - i;
return { text: `${year}년`, val: year.toString() };
});
const quater = [{ text: '1분기', val: '1' }, { text: '2분기', val: '2' }, { text: '3분기', val: '3' }, { text: '4분기', val: '4' }]
const [files, setFiles] = useState<FileList | null>(null);
const [selectedYear, setSelectedYear] = useState<string>(yearOptions[0]?.val || '');
const [selectedQuarter, setSelectedQuarter] = useState<string>(quater[0]?.val || '');
const [upload, setUpload] = useState<Upload | null>(null);
const internalHandleUploadResponse = (file: File) => async (res: HttpResponse) => {
const fileInfo = res.getHeader("X-File-Info");
if (!fileInfo) {
if (onFileSelect) onFileSelect(file, null, null);
return;
}
const { atflCd: newAtflCd, atflId: newAtflId } = JSON.parse(fileInfo) ?? {};
const orgnlFileNm = file.name;
console.log(`FileUploadForm (idx: ${fileIdx ?? 'N/A'}) - orgnlFileNm:`, orgnlFileNm, 'New AtflCd:', newAtflCd, 'New AtflId:', newAtflId);
setFileName(orgnlFileNm);
setInternalAtflCd(newAtflCd);
setInternalAtflId(newAtflId);
if (onFileSelect) {
onFileSelect(file, newAtflCd, newAtflId);
}
};
return (
<div>
<div className='card-header'>
<h3 className="text-2xl font-medium">{title}</h3>
</div>
<div className="card p-6">
<div className="grid grid-col-2 items-center space-x-4">
<div className='flex'>
<SelectBox
className="w-50 mr-3 ml-4"
selectClass="rounded-lg bg-white"
label="년도"
selectArray={yearOptions}
onSelectValue={(value: string | number) => setSelectedYear(String(value))}
/><SelectBox
className="w-50"
selectClass="rounded-lg bg-white"
label="분기"
selectArray={quater}
onSelectValue={(value: string | number) => setSelectedQuarter(String(value))}
<div> {/* 컴포넌트 루트 div */}
<div className="card p-6"> {/* 카드 스타일링 */}
<div className='flex items-center gap-2'> {/* FileUploader와 Button을 정렬하는 메인 flex 컨테이너 */}
<div className="w-[300px] mr-4"> {/* FileUploader의 너비와 오른쪽 여백을 제어하는 컨테이너 */}
<FileUploader
id={uniqueId}
className="!h-auto !max-w-none flex-row items-center gap-2"
accept={accept}
onUpload={(e) => {
const selectedFiles = e.target.files;
if (!selectedFiles || selectedFiles.length === 0) {
setFiles(null);
setCurrentFile(null);
setInternalAtflCd(null);
setInternalAtflId(null);
if (onFileSelect) {
onFileSelect(null, null, null); // 파일 선택 취소 시 null 전달
}
return;
}
const firstFile = selectedFiles[0];
if (!firstFile) return; // 타입스크립트 undefined 가능성 제거
setFiles(selectedFiles);
setCurrentFile(firstFile);
if (onFileSelect) {
onFileSelect(firstFile, internalAtflCd, internalAtflId);
}
handleUpload(selectedFiles, {
handleUploadStart: setUpload,
handleSuccess: setUpload,
// handleResponse는 tus-js-client의 Upload 인스턴스 콜백과 연동 필요
// 여기서는 handleUpload 유틸리티가 응답을 직접 처리한다고 가정
handleResponse: internalHandleUploadResponse(firstFile),
});
}}
customPreview={
showPreview ? (
<div className="file-uploader-preview">
{files && files.length > 0
? fileName
: "파일을 선택하세요."}
</div>
) : null
}
/>
<div className='flex ml-5 items-center'>
<div className="w-[80px] mr-4">
<FileUploader
id="atflCd"
className="!h-auto !max-w-none flex-col !items-start"
accept=".jpeg,.jpg,.png"
onUpload={(e) => {
const selectedFiles = e.target.files;
if (!selectedFiles) return;
setFiles(selectedFiles);
if (selectedFiles[0]) {
handleUpload(selectedFiles, {
handleUploadStart: setUpload,
handleSuccess: setUpload,
handleResponse: handleUploadResponse(selectedFiles[0]),
});
}
}}
customPreview={<></>}
/>
</div>
<Button
className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700"
size="sm"
onClick={() => {
console.log(files?.[0]?.name)
if (!files) {
alert('파일을 선택해주세요.');
return;
}
if (!selectedYear) {
alert('등록년도를 선택해주세요.');
return;
}
if (!selectedQuarter) {
alert('분기를 선택해주세요.');
return;
}
const fileNm = files?.[0]?.name || '';
onSubmit(fileNm, selectedYear, selectedQuarter, atflCd || '', atflId || '');
}}
>
등록
</Button>
</div>
</div>
{showButton ? ( // showButton이 true일 때만 버튼 렌더링
<Button
className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700"
size="sm"
onClick={() => {
console.log(files?.[0]?.name)
if (!currentFile) {
alert('파일을 선택해주세요.');
return;
}
if (!selectedYear) {
alert('등록년도를 선택해주세요.');
return;
}
if (!selectedQuarter) {
alert('분기를 선택해주세요.');
return;
}
const fileNm = currentFile.name;
if (onSubmit) {
// 부모에게 전달할 때는 내부 상태인 internalAtflCd, internalAtflId 사용
onSubmit(fileNm, selectedYear, selectedQuarter, internalAtflCd || '', internalAtflId || '');
setFiles(null); // 파일 업로드 후 파일 목록 초기화
setFileName(''); // 파일명 초기화
}
}}
>
등록
</Button>
) : null}
</div>
</div>
</div>
@@ -0,0 +1,68 @@
"use client"
import { SelectBox } from '@common/business_components/ui';
import { Button } from '@common/components/ui';
import React, { useState } from 'react'
import type { HttpResponse, Upload } from "tus-js-client";
import { FileUploader } from "@common/components";
import { handleUpload } from "@utils";
import { set } from 'react-hook-form';
import FileUploadForm from './fileUploadForm';
const operFileUploadForm = ({ title, onSubmit }: { title: string, onSubmit: (files: string, year: string, quarter: string, code:string, id:string) => void }) => {
const [atflCd, setAtflCd] = useState<string | null>(null);
const [atflId, setAtflId] = useState<string | null>(null);
const handleUploadResponse = (file: File) => async (res: HttpResponse) => {
const fileInfo = res.getHeader("X-File-Info");
if (!fileInfo) return;
const { atflCd, atflId } = JSON.parse(fileInfo) ?? {};
const orgnlFileNm = file.name;
console.log('orgnlFileNm', orgnlFileNm, atflCd, atflId);
setAtflCd(atflCd);
setAtflId(atflId);
};
// 현재 년도부터 10년 전까지의 년도 옵션 생성
const yearOptions = Array.from({ length: 10 }, (_, i) => {
const year = new Date().getFullYear() - i;
return { text: `${year}년`, val: year.toString() };
});
const quater = [{ text: '1분기', val: '1' }, { text: '2분기', val: '2' }, { text: '3분기', val: '3' }, { text: '4분기', val: '4' }]
const [files, setFiles] = useState<FileList | null>(null);
const [selectedYear, setSelectedYear] = useState<string>(yearOptions[0]?.val || '');
const [selectedQuarter, setSelectedQuarter] = useState<string>(quater[0]?.val || '');
const [upload, setUpload] = useState<Upload | null>(null);
return (
<div>
<div className='card-header'>
<h3 className="text-2xl font-medium">{title}</h3>
</div>
<div className="card p-6">
<div className="grid grid-col-2 items-center space-x-4">
<div className='flex'>
<SelectBox
className="w-50 mr-3 ml-4"
selectClass="rounded-lg bg-white"
label="년도"
selectArray={yearOptions}
onSelectValue={(value: string | number) => setSelectedYear(String(value))}
/><SelectBox
className="w-50"
selectClass="rounded-lg bg-white"
label="분기"
selectArray={quater}
onSelectValue={(value: string | number) => setSelectedQuarter(String(value))}
/>
<FileUploadForm
onSubmit={onSubmit}
showButton={true}
showPreview={true}
accept={""} />
</div>
</div>
</div>
</div>
)
}
export default operFileUploadForm
+282 -84
View File
@@ -1,14 +1,15 @@
'use client'
import React, { useRef, useState } from 'react'
import { Button, DatePicker, Input } from '@common/components/ui'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import FormSection from '@/components/ui/formSection'
import BaseLayout from '../BaseLayout'
import { filePicked, FileUpload } from '@common/business_components/ui'
import { excelToflowRateData, excelToMatchingResultData, excelToTimePatternData } from '../../components/lib/utils/excelToJson'
import { postData } from '@/components/lib/api'
import FileUploadForm from '@/components/ui/fileUploadForm'
import FormSection from '@/components/ui/formSection'
import { filePicked } from '@common/business_components/ui'
import { Button, DatePicker, Input } from '@common/components/ui'
import { zodResolver } from '@hookform/resolvers/zod'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { excelToflowRateData, excelToMatchingResultData, excelToTimePatternData } from '../../components/lib/utils/excelToJson'
import BaseLayout from '../BaseLayout'
const formSchema = z.object({
managenm: z.string(),
@@ -19,7 +20,21 @@ const formSchema = z.object({
suyeongaPlace: z.string(),
pattern: z.string()
})
const region: { text: string, val: number }[] = [{ text: '양평', val: 1 }, { text: '강남', val: 2 }, { text: 'test', val: 3 }]
// 각 파일 업로드 폼의 데이터를 저장할 인터페이스 정의
interface UploadedFileInfo {
file: File | null;
atflCd: string | null;
atflId: string | null;
// 필요하다면 파일 이름 등 추가 정보 저장 가능
// fileName: string;
}
// 초기 파일 정보 상태
const initialFileInfo: UploadedFileInfo = { file: null, atflCd: null, atflId: null };
const region: { text: string, val: number }[] = [{ text: '양평', val: 1 }, { text: '강남', val: 2 }, { text: 'test', val: 3 }];
const IrSetting = () => {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
@@ -33,104 +48,287 @@ const IrSetting = () => {
pattern: ""
}
})
const [file1, setFile1] = useState<File | null>(null);
const [file2, setFile2] = useState<File | null>(null);
const [file3, setFile3] = useState<File | null>(null);
const [file4, setFile4] = useState<File | null>(null);
const [selectedDate, setSelectedDate] = useState(new Date())
const fileInputRef1 = useRef<HTMLInputElement>(null)
const fileInputRef2 = useRef<HTMLInputElement>(null)
const fileInputRef3 = useRef<HTMLInputElement>(null)
const fileInputRef4 = useRef<HTMLInputElement>(null)
// FileUploadForm에서 전달받은 파일 정보를 저장할 상태 배열
// 각 인덱스는 FileUploadForm의 fileIdx와 매칭됨
// 예: 0: 오수관망도, 1: 수용가-맨홀, 2: 오수량 시간패턴, 3: 유량자료
const [uploadedFilesData, setUploadedFilesData] = useState<UploadedFileInfo[]>([
initialFileInfo, // 0: 오수관망도
initialFileInfo, // 1: 수용가-맨홀 매칭결과
initialFileInfo, // 2: 오수량 시간패턴
initialFileInfo // 3: 유량자료 등록
]);
const [selectedDate, setSelectedDate] = useState(new Date());
const [input1, setInput1] = useState('');
const [input2, setInput2] = useState('');
const [select1, setSelect1] = useState('');
const [select2, setSelect2] = useState('');
const [select3, setSelect3] = useState('');
const [select4, setSelect4] = useState('');
const [select5, setSelect5] = useState('');
const [formResetKey, setFormResetKey] = useState(0); // FileUploadForm 리셋을 위한 key 상태
// 폼 초기화 함수
const resetAllForms = () => {
form.reset({ // react-hook-form 초기화
managenm: "",
region: "",
suyeonga: 0,
inputdate: new Date(),
rtkSource: "",
suyeongaPlace: "",
pattern: ""
});
setUploadedFilesData([ // 파일 업로드 상태 초기화
initialFileInfo,
initialFileInfo,
initialFileInfo,
initialFileInfo
]);
setSelectedDate(new Date()); // 날짜 선택 초기화
setInput1(''); // 입력 필드 초기화
setInput2(''); // 입력 필드 초기화
setSelect1(''); // 선택 필드 초기화
setSelect2('');
setSelect3('');
setSelect4('');
setSelect5('');
setFormResetKey(prevKey => prevKey + 1); // FileUploadForm 재마운트를 위해 key 변경
// FileUploadForm 내부의 파일명 표시 등을 초기화하려면
// FileUploadForm 컴포넌트 자체에 reset 로직 또는 key 변경을 통한 재마운트 전략이 필요할 수 있습니다.
// 현재 구조에서는 FileUploadForm의 내부 상태를 직접 초기화하기 어렵습니다.
// 가장 간단한 방법은 FileUploadForm에 key prop을 주고, 초기화 시 key를 변경하여 강제로 재렌더링하는 것입니다.
// 예: <FileUploadForm key={formResetKey} ... /> setFormResetKey(prev => prev + 1)
};
// FileUploadForm의 onFileSelect prop으로 전달될 콜백 함수
const handleFileSelection = (
index: number, // FileUploadForm의 fileIdx 값
selectedFile: File | null,
cd: string | null,
id: string | null
) => {
setUploadedFilesData(prevData => {
const newData = [...prevData];
newData[index] = {
file: selectedFile,
atflCd: cd,
atflId: id,
};
return newData;
});
console.log(`Page: File data received for form index ${index}`, { fileName: selectedFile?.name, cd, id });
};
const registFile = async (analysisData: any) => {
console.log("Submitting combined analysis data to backend:", analysisData);
// 추후 API 엔드포인트에 맞게 URL 수정 필요
const data = await postData('v1/inftwtAnls', analysisData);
if (data.ok) {
alert("분석 데이터가 성공적으로 제출되었습니다.");
resetAllForms(); // 성공 시 폼 초기화 함수 호출
} else {
alert("분석 데이터 제출 중 오류가 발생했습니다.");
// resetAllForms(); // 실패 시 폼 초기화 함수 호출
}
};
// 버튼 클릭 핸들러
const handleAnalysisClick = async () => {
const file1 = fileInputRef1.current?.files?.[0]
const file2 = fileInputRef2.current?.files?.[0]
const file3 = fileInputRef3.current?.files?.[0]
console.log(uploadedFilesData);
const file1 = uploadedFilesData[0]; // 오수관망도
const file2 = uploadedFilesData[1]; // 수용가-맨홀 매칭결과
const file3 = uploadedFilesData[2]; // 오수량 시간패턴
const file4 = uploadedFilesData[3]; // 유량자료
if (!file1 || !file2 || !file3) {
alert('모든 파일을 선택해주세요.')
return
console.log("오수관망도 정보:", file1?.file);
console.log("수용가-맨홀 매칭결과 정보:", file2);
console.log("오수량 시간패턴 정보:", file3);
console.log("유량자료 정보:", file4);
// 모든 필수 파일이 선택되었는지 확인 (UploadedFileInfo 객체와 그 안의 file 객체 모두 확인)
if (!file1?.file || !file2?.file || !file3?.file || !file4?.file) {
alert('모든 분석용 파일을 선택해주세요.');
return;
}
let json2: any = null;
let json3: any = null;
// filePickers 구성 시 uploadedFilesData의 file 객체 사용
const filePickers = [
// filePicked 유틸리티가 File 객체를 직접 받는지, ArrayBuffer 등을 받는지 확인 필요
// 여기서는 File 객체를 받고, 내부적으로 ArrayBuffer로 변환하여 parser에 전달한다고 가정
{ file: file2, parser: excelToMatchingResultData },
{ file: file3, parser: excelToTimePatternData },
{ file: file4, parser: excelToflowRateData }
// 오수관망도 파일은 파싱 대상이 아닐 수 있으므로, 필요에 따라 추가/제외
];
await filePicked(file2, 0).then((res) => {
if (res !== undefined) json2 = excelToMatchingResultData(res);
});
await filePicked(file3, 0).then((res) => {
if (res !== undefined) json3 = excelToTimePatternData(res);
});
try {
const parsedResults = await Promise.all(
filePickers.map(async ({ file, parser }) => {
if (!file || !file.file) return null; // fileInfo 객체 및 file 객체 null 체크
// filePicked 함수에 File 객체와 headerNum 전달
const jsonData = await filePicked(file.file, 0);
console.log(file1)
console.log(json2)
console.log(json3)
}
if (jsonData === undefined) {
console.warn(`Warning: filePicked returned undefined for ${file.file.name}`);
return {
fileName: file.file.name,
atflCd: file.atflCd,
atflId: file.atflId,
parsedData: null // 또는 적절한 기본값
};
}
// jsonData를 각 parser 함수로 처리
// parser 함수가 filePicked가 반환하는 JSON 형식을 직접 처리한다고 가정
const parsedDataBySpecificFunction = parser(jsonData);
// 버튼 클릭 핸들러
const handleUploadClick = async () => {
const file4 = fileInputRef4.current?.files?.[0];
return {
fileName: file.file.name,
atflCd: file.atflCd,
atflId: file.atflId,
parsedData: parsedDataBySpecificFunction
};
})
);
if (!file4) {
alert('파일을 선택해주세요.')
return
}
let json4: any = null;
await filePicked(file4, 0).then((res) => {
console.log(res);
if (res !== undefined) json4 = excelToflowRateData(res);
});
console.log(json4);
// 각 파싱 결과 접근
const matchingResultData = parsedResults.find(r => r && r.fileName === file2.file?.name)?.parsedData;
const timePatternData = parsedResults.find(r => r && r.fileName === file3.file?.name)?.parsedData;
const flowRateData = parsedResults.find(r => r && r.fileName === file4.file?.name)?.parsedData;
// json4가 존재하면 postData로 백엔드에 전송
if (json4) {
try {
const response = await postData('your-backend-endpoint', json4); // 'your-backend-endpoint'를 실제 API 경로로 변경
console.log('업로드 결과:', response);
alert('업로드가 완료되었습니다.');
} catch (error) {
console.error('업로드 실패:', error);
alert('업로드에 실패했습니다.');
}
// 새로운 JSON 데이터 생성
const combinedAnalysisData = {
enoCd: select1, // 환경청코드
ctpvCd: select2, // 시도코드
sggCd: select3, // 시군구코드
swageTmplntCd: select4, // 하수처리장코드
swtrmtZoneCd: select5, // 하수처리구역코드
aplcnYmd: selectedDate.toISOString(), // 적용연월
dtrwSwtcRt: input1, // 오수전환율
anlsInfo: input2, // 분석정보
atflCd: file1?.atflCd, // 첨부파일CD
atflId: file1?.atflId, // 첨부파일ID
tbSnRcptrMhlMpng: matchingResultData, // 수용가-맨홀 매칭결과
tbSnHrPttn: timePatternData, // 오수량 시간패턴
tbSnOilqData: flowRateData, // 유량자료
};
console.log("통합 분석 데이터 (JSON):", combinedAnalysisData);
// 수정된 registFile 함수를 호출하여 combinedAnalysisData를 전송합니다.
await registFile(combinedAnalysisData);
} catch (error) {
console.error("파일 파싱 중 오류 발생:", error);
alert("파일 처리 중 오류가 발생했습니다.");
return;
}
}
return (
<>
<BaseLayout titleName={{ mainTitle: '침입수 분석', firstCardTitle: '침입수 분석 자료 입력', secondCardTitle: '실측자료 등록' }} form={form} onSubmitEvent={() => { }} graphBtn={false} searchBox={{ title: '분석 대장지역 선택' }} >
<BaseLayout
titleName={{ mainTitle: '침입수 분석', firstCardTitle: '침입수 분석 자료 입력', secondCardTitle: '실측자료 등록' }}
form={form}
onSubmitEvent={() => { }}
graphBtn={false}
searchBox={{ title: '분석 대장지역 선택' }} >
<div className="grid gap-4">
<FormSection name='suyeongaPlace' form={form} titleNm='▷ 오수관망도'><div className="">
<Input id="fileNm" ref={fileInputRef1} type="file" />
</div></FormSection>
<FormSection name='suyeonga' form={form} titleNm='▷ 수용가-맨홀 매칭결과'><div className="w-[150px]">
<Input id="fileNm" ref={fileInputRef2} type="file" accept=".xls, .xlsx, .xlsm, .xltx" />
</div></FormSection>
<FormSection name='pattern' form={form} titleNm='▷ 오수량 시간패턴'><div className="w-[150px]">
<Input id="fileNm" ref={fileInputRef3} type="file" accept=".xls, .xlsx, .xlsm, .xltx" />
</div></FormSection>
<FormSection name='inputdate' form={form} titleNm='▷ 상수사용량 적용기간'>
<DatePicker selected={selectedDate} onSelect={(date: Date | undefined) => { if (date) setSelectedDate(date) }} mode={"single"} />
<FormSection name='suyeongaPlace' form={form} titleNm='▷ 오수관망도'>
<FileUploadForm
key={`file-upload-0-${formResetKey}`} // 폼 초기화를 위한 key
onFileSelect={(file, cd, id) => handleFileSelection(0, file, cd, id)}
showButton={false}
showPreview={true}
accept={""}
fileIdx={0} />
</FormSection>
<FormSection name='suyeonga' form={form} titleNm='▷ 수용가-맨홀 매칭결과'>
<FileUploadForm
key={`file-upload-1-${formResetKey}`} // 폼 초기화를 위한 key
onFileSelect={(file, cd, id) => handleFileSelection(1, file, cd, id)}
showButton={false}
showPreview={true}
accept={".xls, .xlsx, .xlsm, .xltx"}
fileIdx={1} />
{/* <Input id="fileNm" ref={fileInputRef1} type="file" accept=".xls, .xlsx, .xlsm, .xltx" /> */}
</FormSection>
<FormSection name='pattern' form={form} titleNm='▷ 오수량 시간패턴'>
<FileUploadForm
key={`file-upload-2-${formResetKey}`} // 폼 초기화를 위한 key
onFileSelect={(file, cd, id) => handleFileSelection(2, file, cd, id)}
showButton={false}
showPreview={true}
accept={".xls, .xlsx, .xlsm, .xltx"}
fileIdx={2} />
{/* <Input id="fileNm" ref={fileInputRef2} type="file" accept=".xls, .xlsx, .xlsm, .xltx" /> */}
</FormSection>
<FormSection
name='inputdate'
form={form}
titleNm='▷ 상수사용량 적용기간'>
<DatePicker
className="ml-5"
selected={selectedDate}
onSelect={(date: Date | undefined) => { if (date) setSelectedDate(date) }}
mode={"single"} />
</FormSection>
<FormSection
name='rtkSource'
form={form}
titleNm='▷ 오수전환율'>
<Input
value={input1}
onChange={(e) => {
const newValue = e.target.value;
if (newValue === "") {
setInput1(newValue);
return;
}
// 숫자와 '.'만 허용하고, '.'은 최대 하나인지 확인
const isValidChars = /^[0-9]*\.?[0-9]*$/.test(newValue);
if (!isValidChars) return; // 유효하지 않은 문자 또는 여러 개의 '.' 포함
// '.' 기호가 포함된 경우, 처음이나 끝에 오면 안 됨
if (newValue.includes('.') && newValue.startsWith('.')) return; // '.'이 처음에는 올 수 없음
setInput1(newValue);
}}
className="ml-5 h-[3.2rem] text-center text-xl w-[150px]"
placeholder="예: 0.75" />
</FormSection>
<FormSection
name='analysis information'
form={form}
titleNm='▷ 분석 정보'>
<Input
value={input2}
onChange={(e) => setInput2(e.target.value)}
className="ml-5 h-[3.2rem] text-center text-xl w-[150px]" />
</FormSection>
</div>
<div className="flex items-center gap-4">
<FormSection
name='region' form={form} titleNm='▷ 유량자료 등록'>
<FileUploadForm
key={`file-upload-3-${formResetKey}`} // 폼 초기화를 위한 key
onFileSelect={(file, cd, id) => handleFileSelection(3, file, cd, id)}
showButton={false}
showPreview={true}
accept={".xls, .xlsx, .xlsm, .xltx"}
fileIdx={3} />
{/* <Input id="fileNm" ref={fileInputRef3} type="file" accept=".xls, .xlsx, .xlsm, .xltx" /> */}
</FormSection>
<FormSection name='rtkSource' form={form} titleNm='▷ 오수전환율'><Input className="h-[3.2rem] text-center text-xl w-[150px]" /></FormSection>
<FormSection name='analysis information' form={form} titleNm='▷ 분석 정보'><Input className="h-[3.2rem] text-center text-xl w-[150px]" /></FormSection>
<div className="flex justify-center mt-1">
<Button type="submit" onClick={handleAnalysisClick}>침입수분석 실행</Button>
</div>
</div>
<div className="flex items-center gap-24">
<FormSection name='region' form={form} titleNm='▷ 유량자료 등록'>
<div className="w-[150px]">
<Input id="fileNm" ref={fileInputRef4} type="file" accept=".xls, .xlsx, .xlsm, .xltx" />
</div>
</FormSection>
<div className='mt-1'>
<Button type="submit" onClick={handleUploadClick}>자료 업로드</Button>
</div>
</div>
</BaseLayout>
</>
)
+23 -21
View File
@@ -2,30 +2,32 @@
import { SearchDiv, SearchFormLeft, SearchFormRight, UiTable } from '@common/business_components/ui'
import { Button, DatePicker, Input, Label } from '@common/components/ui'
import React, { useEffect, useState } from 'react'
import FileUploadForm from '../../components/ui/fileUploadForm'
import OperFileUploadForm from '../../components/ui/operFileUploadForm'
import ListDiv from '@common/business_components/ui/list_div'
import ContentLayout from '@/components/ui/contentLayout'
import { searchData } from '@/components/lib/common/apiHelpers'
import { tableHeadlist } from '..'
import { handleDownload } from '@/components/lib/utils/fileDownload'
import { formatDate } from '@/components/lib/utils/date'
import { deleteData, getData, postData } from '@/components/lib/api'
import { deleteData, postData } from '@/components/lib/api'
const Page = () => {
const [fromDate, setFromDate] = useState(new Date())
// 조회조건
const [fromDate, setFromDate] = useState(()=>{const date = new Date(); date.setDate(date.getDate()-7); return date})
const [toDate, setToDate] = useState(new Date())
const [manageYear, setManageYear] = useState('')
const [mngQu, setMngQu] = useState('')
const [usr, setUsr] = useState('')
const [pjNm, setpjNm] = useState('')
const [fileNm, setFileNm] = useState('')
const [atflId, setAtflId] = useState('')
const [atflCd, setAtflCd] = useState('')
const [sbmrgPrevntBizSn, setSbmrgPrevntBizSn] = useState('')
const [tableData, setTableData] = useState<any>([{}])
const [curPage, setCurPage] = useState(1)
// 파일업로드 관련 상태
const [fileNm, setFileNm] = useState('') //파일명
const [atflId, setAtflId] = useState('') //파일 ID
const [sbmrgPrevntBizSn, setSbmrgPrevntBizSn] = useState('') //파일 일련번호
const [tableData, setTableData] = useState<any>([{}]) //테이블 데이터
const [curPage, setCurPage] = useState(1) //현재 페이지
const [totalData, setTotalData] = useState(0) //총 데이터 수
const fetchData = async () => {
const data = await searchData('v1/sbmrgPrevnt', {
const {data, total} = await searchData('v1/sbmrgPrevnt', {
fromRegDt: formatDate(fromDate),
fromMngYr: manageYear,
toMngYr: mngQu,
@@ -36,6 +38,7 @@ const Page = () => {
size: '10'
})
setTableData(data)
setTotalData(total)
}
useEffect(() => {
setCurPage(1)
@@ -49,30 +52,29 @@ const Page = () => {
handleDownload(`/attach-service/api/v1/download/${atflId}`, fileNm)
}
const isDeleteFile = async () => {
if (fileNm === '' || fileNm === undefined) {
if (sbmrgPrevntBizSn === '' || sbmrgPrevntBizSn === undefined) {
alert('삭제할 파일을 선택해주세요')
return false
}
const data = await getData(`/attach-service/api/v1/atfl/attachments/${atflId}`)
const data = await deleteData(`/attach-service/api/v1/atfl/attachments/${atflId}`)
if (data) {
const delData = await deleteData(`v1/sbmrgPrevnt/${sbmrgPrevntBizSn}`)
if (delData) {
if (delData.status === 200) {
alert('삭제되었습니다.')
fetchData()
} else {
alert('삭제에 실패했습니다.')
}
}
}
const registFile = async (fileNm: string, year: string, quater: string, cd: string, id: string) => {
const registFile = async (fileName: string, year: string, quater: string, cd: string, id: string) => {
const data = await postData('v1/sbmrgPrevnt', {
atflNm: fileNm,
atflNm: fileName,
mngYr: year,
mngQu: quater,
atflCd: cd,
atflId: id
atflId: id,
sbmrgPrevntBizNm:fileName
})
if (data.ok) { setAtflId(data.atflId); setAtflCd(data.atflCd) }
if (data) {alert('파일이 등록되었습니다.'); fetchData() }
}
return (
<ContentLayout title='도시침수 예방사업'>
@@ -133,10 +135,10 @@ const Page = () => {
<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>
</div>
</div>
<UiTable headName='' tableData={tableData} headlist={tableHeadlist} pageSize={10} total={10} cellClick={(index) => { if (index != null) { const fileName = tableData[index]?.fileNm ?? ""; setFileNm(fileName); setSbmrgPrevntBizSn(tableData[index]?.sbmrgPrevntBizSn) } }} onPageChange={async (page:number)=>{setCurPage(page); await fetchData()}}></UiTable>
<UiTable headName='' tableData={tableData} headlist={tableHeadlist} pageSize={10} total={totalData} cellClick={(index) => { if (index != null) {const fileName = tableData[index]?.sbmrgPrevntBizNm ?? ""; setFileNm(fileName); const id = tableData[index]?.atflId ?? ''; setAtflId(id); setSbmrgPrevntBizSn(tableData[index]?.sbmrgPrevntBizSn);} }} onPageChange={async (page:number)=>{setCurPage(page); await fetchData()}}></UiTable>
</ListDiv>
<ListDiv className="mt-4">
<FileUploadForm title='도시침수 예방사업 사업추진현황 등록' onSubmit={async (file, year, quater, cd, id) => { await registFile(file, year, quater, cd, id) }} />
<OperFileUploadForm title='도시침수 예방사업 사업추진현황 등록' onSubmit={async (file, year, quater, cd, id) => { await registFile(file, year, quater, cd, id) }} />
</ListDiv>
</ContentLayout>
)
+11 -3
View File
@@ -1,9 +1,17 @@
export const tableHeadlist = [
{ id: 'sbmrgPrevntBizSn', title: '일련번호' },
{ id: 'fileNm', title: '파일명' },
{ id: 'sbmrgPrevntBizNm', title: '파일명' },
{ id: 'mngYr', title: '년도' },
{ id: 'mngQu', title: '분기' },
{ id: 'registDt', title: '등록 일자' },
{ id: 'registUsr', title: '등록자' }
{ id: 'regDt', title: '등록 일자' },
{ id: 'rgtr', title: '등록자' }
]
export const tableHeadlistOldPp = [
{ id: 'dcrpPplMtncBizSn', title: '일련번호' },
{ id: 'dcrpPplMtncBizNm', title: '파일명' },
{ id: 'mngYr', title: '년도' },
{ id: 'mngQu', title: '분기' },
{ id: 'regDt', title: '등록 일자' },
{ id: 'rgtr', title: '등록자' }
]
+20 -17
View File
@@ -2,16 +2,16 @@
import { SearchDiv, SearchFormLeft, SearchFormRight, UiTable } from '@common/business_components/ui'
import { Button, DatePicker, Input, Label } from '@common/components/ui'
import React, { useEffect, useState } from 'react'
import FileUploadForm from '../../components/ui/fileUploadForm'
import OperFileUploadForm from '../../components/ui/operFileUploadForm'
import ListDiv from '@common/business_components/ui/list_div'
import ContentLayout from '@/components/ui/contentLayout'
import { tableHeadlist } from '..'
import { tableHeadlistOldPp } from '..'
import { handleDownload } from '@/components/lib/utils/fileDownload'
import { searchData } from '@/components/lib/common/apiHelpers'
import { formatDate } from '@/components/lib/utils/date'
import { deleteData, getData, postData } from '@/components/lib/api'
const Page = () => {
const [fromDate, setFromDate] = useState<Date>(new Date());
const [fromDate, setFromDate] = useState<Date>(()=>{const date = new Date(); date.setDate(date.getDate()-7); return date});
const [toDate, setToDate] = useState<Date>(new Date());
const [manageYear, setManageYear] = useState('')
const [mngQu, setMngQu] = useState('')
@@ -19,23 +19,25 @@ const Page = () => {
const [pjNm, setpjNm] = useState('')
const [fileNm, setFileNm] = useState('')
const [atflId, setAtflId] = useState('')
const [atflCd, setAtflCd] = useState('')
const [sbmrgPrevntBizSn, setSbmrgPrevntBizSn] = useState('')
const [dcrpPplMtncBizSn, setDcrpPplMtncBizSn] = useState('')
const [tableData, setTableData] = useState<any>([{}])
const [curPage, setCurPage] = useState(1)
const [totalData, setTotalData] = useState(0) //총 데이터 수
// 데이터 조회
const fetchData = async () => {
const data = await searchData('v1/dcrpPpl', {
const {data, total} = await searchData('v1/dcrpPpl', {
fromRegDt: formatDate(fromDate),
fromMngYr: manageYear,
toMngYr: mngQu,
toRegDt: formatDate(toDate),
atflNm: pjNm,
dcrpPplMtncBizNm: pjNm,
rgtrNm: usr,
page: curPage - 1,
size: '10'
})
setTableData(data)
setTotalData(total)
console.log(data, total)
}
useEffect(() => {
setCurPage(1)
@@ -47,19 +49,19 @@ const Page = () => {
alert('파일을 선택해주세요')
return false
}
handleDownload(`/attach-service/api/v1/download/${atflId}}`, fileNm)
handleDownload(`/attach-service/api/v1/download/${atflId}`, fileNm)
return true
}
// 파일 삭제
const isDeleteFile = async () => {
if (fileNm === '' || fileNm === undefined) {
if (dcrpPplMtncBizSn === '' || dcrpPplMtncBizSn === undefined) {
alert('삭제할 파일을 선택해주세요')
return false
}
const data = await getData(`/attach-service/api/v1/atfl/attachments/${atflId}`)
const data = await deleteData(`/attach-service/api/v1/atfl/attachments/${atflId}`)
if (data) {
const delData = await deleteData(`v1/sbmrgPrevnt/${sbmrgPrevntBizSn}`)
if (delData) {
const delData = await deleteData(`v1/dcrpPpl/${dcrpPplMtncBizSn}`)
if (delData.status == 200) {
alert('삭제되었습니다.')
fetchData()
} else {
@@ -74,9 +76,10 @@ const Page = () => {
mngYr: year,
mngQu: quater,
atflCd: cd,
atflId: id
atflId: id,
dcrpPplMtncBizNm: fileNm
})
if (data.ok) { setAtflId(data.atflId); setAtflCd(data.atflCd) }
if (data) {alert('파일이 등록되었습니다.');fetchData() }
}
return (
<ContentLayout title='노후 하수관로 정비사업'>
@@ -119,7 +122,7 @@ const Page = () => {
<Input
className='w-[100px]'
value={pjNm}
onChange={(e) => setpjNm(e.target.value)}
onChange={(e) => {setpjNm(e.target.value)}}
/>
</div>
</div>
@@ -137,10 +140,10 @@ const Page = () => {
<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>
</div>
</div>
<UiTable headName='' tableData={tableData} headlist={tableHeadlist} pageSize={10} total={20} children={undefined} cellClick={(index) => { if (index != null) { const fileName = tableData[index]?.fileNm ?? ""; setFileNm(fileName); setSbmrgPrevntBizSn(tableData[index]?.sbmrgPrevntBizSn) } }} onPageChange={async (page:number)=>{setCurPage(page); await fetchData()}}></UiTable>
<UiTable headName='' tableData={tableData} headlist={tableHeadlistOldPp} pageSize={10} total={totalData} children={undefined} cellClick={(index) => { if (index != null) { const fileName = tableData[index]?.dcrpPplMtncBizNm ?? ""; setFileNm(fileName); const id = tableData[index]?.atflId ?? ''; setAtflId(id); setDcrpPplMtncBizSn(tableData[index]?.dcrpPplMtncBizSn) } }} onPageChange={async (page:number)=>{setCurPage(page); await fetchData()}}></UiTable>
</ListDiv>
<ListDiv className="mt-4">
<FileUploadForm title='노후 하수관로 정비사업 주진현황 등록' onSubmit={async (file, year, quater, cd, id) => { await registFile(file, year, quater, cd, id) }} />
<OperFileUploadForm title='노후 하수관로 정비사업 주진현황 등록' onSubmit={async (file, year, quater, cd, id) => { await registFile(file, year, quater, cd, id) }} />
</ListDiv>
</ContentLayout>
)
@@ -14,6 +14,9 @@ import CircleStyle from 'ol/style/Circle';
import VectorLayer from 'ol/layer/Vector';
import { Point } from 'ol/geom';
import { boundingExtent } from 'ol/extent';
import TileWMTS from 'ol/source/WMTS';
import WMTS from 'ol/tilegrid/WMTS';
import { get as getProjection } from 'ol/proj';
import olSourceImageWMS from 'ol/source/ImageWMS';
import { GeoJSON } from 'ol/format';
import GisOlEditLayer from './gis.edit';
@@ -32,20 +35,26 @@ GisApp.GEOSERVER_URL = '/geoserver/hasudo/ows';
GisApp.GEOSERVER_WMS_URL = '/geoserver/hasudo/wms';
//맵 정보
GisApp.EMAP_URL = '/emap/';
GisApp.EMAP_SATELLITE_URL = '/emap/satellite/';
GisApp.VWORLD_URL = 'https://xdworld.vworld.kr/2d/Base/service/{z}/{x}/{y}.png';
GisApp.VWORLD_SATELLITE_URL = 'https://xdworld.vworld.kr/2d/Satellite/service/{z}/{x}/{y}.jpeg';
GisApp.GOOGLE_API_KEY = 'AIzaSyCf1sgeELFalfPuqlykRKBMj6X9u_DQH6M';
GisApp.GOOGLE_ROADMAP_URL = `https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}&key=${GisApp.GOOGLE_API_KEY}`;
GisApp.GOOGLE_SATELLITE_URL = `https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}&key=${GisApp.GOOGLE_API_KEY}`;
GisApp.GOOGLE_HYBRID_URL = `https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}&key=${GisApp.GOOGLE_API_KEY}`;
GisApp.GOOGLE_3D_URL = `https://tile.googleapis.com/v1/3dtiles/root.json?key=${GisApp.GOOGLE_API_KEY}`;
// TODO: NGII 정보
GisApp.NGII_API_KEY = '305CC242F2920F46BB93BCC13A52DD8A'; // NGII에서 발급받은 API 키
GisApp.NGII_WMTS_URL = `http://map.ngii.go.kr/openapi/wmts_ngiiMap.js?apikey=${GisApp.NGII_API_KEY}`;
GisApp.EMAP_URL = '/emap/';
GisApp.EMAP_SATELLITE_URL = '/emap/satellite/';
GisApp.VWORLD_URL = 'https://xdworld.vworld.kr/2d/Base/service/{z}/{x}/{y}.png';
GisApp.VWORLD_SATELLITE_URL = 'https://xdworld.vworld.kr/2d/Satellite/service/{z}/{x}/{y}.jpeg';
GisApp.OPENSTREET_URL = '//tile.openstreetmap.org/{z}/{x}/{y}.png';
GisApp.BASE_MAP_TYPES = [];
GisApp.FROM_PROJ = 'EPSG:4326';
GisApp.TO_PROJ = 'EPSG:3857';
// GisApp.TO_PROJ = 'EPSG:3857';
GisApp.TO_PROJ = 'EPSG:5179'; //ngii 좌표계
GisApp.TO_DATA_PROJ = 'EPSG:4326';
GisApp.CONTEXT_LAYER_PATH = '/images/map/layericon/';
GisApp.layerCode = {};
@@ -86,6 +95,7 @@ GisApp.core.prototype = {
minZoom: 5,
maxZoom: 22,
},
options: {
/*1: { zoom: 16, minZoom: 10, maxZoom: 22, centerX: 35.3227711, centerY: 129.1836202, dataProj: 'EPSG:5186' },
4: { zoom: 16, minZoom: 10, maxZoom: 22, centerX: 35.0140499, centerY: 126.7110757, dataProj: 'EPSG:5186' },
@@ -109,11 +119,15 @@ GisApp.core.prototype = {
GisApp.Module.core = this;
GisApp.Module.layer = new GisOlLayer(this.defaults);
GisApp.Module.edit = new GisOlEditLayer(this.defaults);
// TODO: vworld layer 정보 (getNGIIbaseMap)활성화 하면 ngii base map으로 변경됨
this.baseMiniMapLayers['base'] = this.getBaseMap('minimap-base');
this.baseLayers['base'] = this.getBaseMap('base');
this.baseLayers['satellite'] = this.getBaseMap('satellite');
this.baseLayers['3d'] = this.getBaseMap('3d');
// this.baseLayers['3d'] = this.getNGIIBaseMap('3d');
// this.baseMiniMapLayers['base'] = this.getNGIIBaseMap('minimap-base');
// this.baseLayers['base'] = this.getNGIIBaseMap('base');
// this.baseLayers['satellite'] = this.getNGIIBaseMap('satellite');
},
layerType: {
point: 'POINT',
@@ -126,6 +140,46 @@ GisApp.core.prototype = {
this.setCenter(Number(this.defaults.centerX), Number(this.defaults.centerY));
this.setZoom(15);
},
// NGII Base Map
getNGIIBaseMap: function (_id) {
const layers = {
base: 'korean_map', // 일반지도
satellite: 'satellite_map', // 위성지도
night: 'night_map', // 야간지도
};
// 선택된 레이어
const layerName = layers[_id] || layers.base;
// EPSG:5179 좌표계 설정
const projection = getProjection('EPSG:5179');
const extent = [705680.0000, 1349270.0000,1388291.0000, 2581448.0000]; // NGII 좌표계 범위
const resolutions = [
2088.96, 1044.48, 522.24, 261.12, 130.56, 65.28, 32.64, 16.32, 8.16, 4.08, 2.04, 1.02, 0.51, 0.255,
];
const tileGrid = new WMTS({
extent: extent,
resolutions: resolutions,
matrixIds: resolutions.map((_, index) => index.toString()), // WMTS matrix IDs
});
// WMTS 소스 생성
const wmtsSource = new TileWMTS({
url: GisApp.NGII_WMTS_URL,
layer: layerName,
matrixSet: 'EPSG:5179',
format: 'image/png',
projection: projection,
tileGrid: tileGrid,
style: 'default',
crossOrigin: 'anonymous',
});
// TileLayer 생성
return new TileLayer({
source: wmtsSource,
visible: true,
zIndex: 0,
});
},
getBaseMap: function (_id) {
let url = GisApp.VWORLD_URL;
if (_id === 'satellite') {
@@ -182,7 +236,7 @@ GisApp.core.prototype = {
setCenter: function (x, y, transYn) {
if (!x || !y) return;
let view = this.map.getView();
if (!transYn) {
// 값이 없으면
let center = olProj.transform([Number(y), Number(x)], GisApp.FROM_PROJ, GisApp.TO_PROJ);
@@ -553,7 +607,7 @@ GisApp.core.prototype = {
*/
convertLayerInfosToLayerCode: function (array, key, parentName) {
let initialValue = {};
for (let i = 0; i < array.length; i++) {
let item = {};
Object.assign(item, array[i]);