Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/wr/sttn

This commit is contained in:
unknown
2025-05-13 09:36:57 +09:00
236 changed files with 9368 additions and 2 deletions
+19
View File
@@ -0,0 +1,19 @@
name: iipbs_sn
on:
push:
branches: [iipbs/sn/dev]
jobs:
build:
runs-on: ubuntu-20.04
steps:
- uses: appleboy/ssh-action@master
with:
port: 11022
host: con.mind-one.co.kr
username: mindone
password: mindone!23
script: |
cd ./iipbs/front
sh ./frontStartSn.sh
+44
View File
@@ -0,0 +1,44 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
.next/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
#.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
pnpm-lock.yaml
+3
View File
@@ -0,0 +1,3 @@
{
"plugins": ["prettier-plugin-tailwindcss"]
}
+7
View File
@@ -0,0 +1,7 @@
'use client'
import { BaseLayout } from "@common/components";
export function ClientLayout({ children }: { children: React.ReactNode }) {
return <BaseLayout>{children}</BaseLayout>;
}
+61
View File
@@ -0,0 +1,61 @@
const BASE_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || '';
export const postData = async (url: string, data: object): Promise<any> => {
const options: RequestInit = {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
const res = await fetch(`${BASE_URL}/${url}`, options);
const resData = await res.json();
return resData;
};
export const postMutiPart = async (url: string, data: FormData): Promise<any> => {
const options: RequestInit = {
method: "POST",
body: data,
};
const res = await fetch(`${BASE_URL}/${url}`, options);
const resData = await res.json();
return resData;
};
export const putData = async (url: string, data: object): Promise<any> => {
const options: RequestInit = {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
const res = await fetch(`${BASE_URL}/${url}`, options);
const resData = await res.json();
return resData;
};
export const getData = async (url: string): Promise<any> => {
const res = await fetch(`${BASE_URL}/${url}`, {
method: "GET",
});
if (res.ok) {
const data = await res.json();
return data;
}
throw new Error('Failed to fetch data');
};
export const deleteData = async (url: string, data?: object): Promise<any> => {
const options: RequestInit = {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: data ? JSON.stringify(data) : undefined,
};
const res = await fetch(`${BASE_URL}/${url}`, options);
const resData = await res.json();
return resData;
};
@@ -0,0 +1,18 @@
import { ContentsDiv, MainDiv, Title } from '@common/business_components/ui'
import React, { type ReactNode } from 'react'
type LayoutNode = {
children : ReactNode
title : string
}
const ContentLayout = ({children, title} : LayoutNode) => {
return (
<MainDiv>
<Title title={title} />
<ContentsDiv>
{children}
</ContentsDiv>
</MainDiv>
)
}
export default ContentLayout
@@ -0,0 +1,64 @@
"use client"
import { FileUpload, SelectBox } from '@common/business_components/ui';
import { Button, Input } from '@common/components/ui';
import React, { useState } from 'react'
const FileUploadForm = ({ title }: { title: string }) => {
const [files, setFiles] = useState<File[] | null>(null);
const [selectedYear, setSelectedYear] = useState<string>('');
// 현재 년도부터 10년 전까지의 년도 옵션 생성
const yearOptions = Array.from({ length: 10 }, (_, i) => {
const year = new Date().getFullYear() - i;
return { text: `${year}년`, val: year.toString() };
});
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={yearOptions}
onSelectValue={(value: string | number) => setSelectedYear(String(value))}
/>
</div>
<div className='flex mt-5 items-center'>
<div className="w-[200px] mr-4">
<Input id="file" type="file" />
</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={() => {
if (!files?.[0] || !selectedYear) {
alert('파일과 등록년도를 선택해주세요.');
return;
}
// TODO: 파일 업로드 처리
console.log('Upload file:', files[0], 'Year:', selectedYear);
}}
>
등록
</Button>
</div>
</div>
</div>
</div>
)
}
export default FileUploadForm
+24
View File
@@ -0,0 +1,24 @@
import { FormField } from '@common/business_components/ui'
import React, { type ReactElement } from 'react'
interface FormSectionProps {
children: ReactElement
titleNm: string
form: any
name: string
}
const FormSection = ({ children, titleNm, form, name }: FormSectionProps) => {
return (
<div className="grid grid-cols-[130px_1fr] items-center">
<div className="text-xl font-medium">{titleNm}</div>
<FormField
control={form.control}
name={name}
render={({ field }) => children}
/>
</div>
)
}
export default FormSection
+31
View File
@@ -0,0 +1,31 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose, DialogOverlay } from '@common/components/ui'
import React from 'react'
interface ModalProps {
isModalOpen: boolean;
setIsModalOpen: (open: boolean) => void;
title?: string;
children: React.ReactNode;
closeModal: () => void;
}
const Modal: React.FC<ModalProps> = ({ isModalOpen, setIsModalOpen, title, children, closeModal }) => {
return (
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogOverlay className='z-[200]'/>
<DialogContent className="w-auto max-w-[1200px] h-auto max-h-[450px] overflow-auto z-[1000]">
{title ? <DialogHeader>
<DialogTitle>{title}</DialogTitle>
</DialogHeader> : null}
{children}
<DialogFooter>
<DialogClose>
<button className="px-4 py-2 bg-gray-600 text-white rounded" onClick={closeModal}>닫기</button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
export default Modal
+127
View File
@@ -0,0 +1,127 @@
import UiPageNations from "@common/business_components/ui/table/uiPageNations";
import { Table, TableBody, TableCell, TableRow } from "@common/components";
import { useEffect, useState } from "react";
type UiTableProps = {
headName: React.ReactNode | undefined;
tableData: { [key: string]: string | number | boolean | object }[];
headlist: UiHeadProps[];
pageSize: number;
total: number;
children: React.ReactNode | undefined;
cellClick: ((index: number | undefined, headId: string) => void) | undefined;
onPageChange: (page: number) => void;
className?: string;
resetPageNo?: boolean;
isEditable?: boolean;
};
type UiHeadProps = {
id: string;
title: string;
align?: string;
type?: string;
options?: string[];
};
export function VerticalTable({
tableData,
headlist,
pageSize,
total,
children,
headName,
cellClick,
onPageChange,
className = "",
resetPageNo = false,
isEditable = false,
}: UiTableProps) {
const [cur, setCur] = useState<number>(1);
const [formData, setFormData] = useState<{ [key: string]: string | number | boolean }>({});
useEffect(() => {
if (resetPageNo) {
setCur(1);
onPageChange(1);
}
}, [onPageChange, resetPageNo]);
useEffect(() => {
if (tableData.length > 0 && tableData[0]) {
setFormData(tableData[0] as { [key: string]: string | number | boolean });
}
}, [tableData]);
const handleInputChange = (id: string, value: string | number | boolean) => {
setFormData((prev) => ({
...prev,
[id]: value,
}));
};
const handlePageChange = (page: number) => {
setCur(page);
onPageChange(page);
};
return (
<>
{headName ? (
<div className="card-header">
<h3 className="text-2xl mx-3 font-medium text-label">{headName}</h3>
</div>
) : null}
<div className={`card !m-[1.2rem] !p-0r ${className}`}>
<Table variant="secondary">
<TableBody className="text-center">
{headlist?.map((head) => (
<TableRow key={head.id}>
{/* 컬럼 제목 */}
<TableCell className="font-bold bg-blue-100 text-xs">
{head.title}
</TableCell>
{/* 데이터 값 또는 입력 필드 */}
<TableCell className="text-xs">
{isEditable ? (
head.type === "select" ? (
<select
value={formData[head.id] ?? "-"}
onChange={(e) => handleInputChange(head.id, e.target.value)}
className="w-full border border-gray-300 rounded-sm px-2 py-1 text-xl border-radius-0"
>
{head.options?.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
) : head.type === "input" ? (
<input
type="text"
value={formData[head.id] ?? ""}
onChange={(e) => handleInputChange(head.id, e.target.value)}
className="w-full text-xl border-radius-0 border border-gray-300 rounded-sm px-2 py-1"
/>
) : (
<span>{formData[head.id] ?? "-"}</span>
)
) : (
<span>{formData[head.id] ?? "-"}</span>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{total > 0 && (
<UiPageNations
total={total}
cur={cur}
pageSize={pageSize}
onPageChange={handlePageChange}
/>
)}
</div>
</>
);
}
+81
View File
@@ -0,0 +1,81 @@
'use client'
import ContentLayout from '@/components/ui/contentLayout'
import { SearchDiv, SearchFormLeft, SelectBox, SearchFormRight, Form, UiTable } from '@common/business_components/ui'
import ListDiv from '@common/business_components/ui/list_div'
import { Button } from '@common/components/ui'
import React, { type ReactNode } from 'react'
type TitleName = {
mainTitle: string
firstCardTitle: string
secondCardTitle?: string
}
type SelectBox = {
labelNm?:Label
searchChk?:boolean
}
type Label= {
first: string
second: string
third:string
fourth:string
}
type BaseLayout = {
children?: ReactNode[]
titleName: TitleName
form: any
onSubmitEvent: Function
graphBtn?: boolean
firstDivBtn?:boolean
searchBox?:SelectBox
}
// TODO: 나중에 이벤트 추가되면 파라미터 추가 필요 ?
/**
*
* @param children
* @param titleName : 메인 타이틀, 첫번째 요소 타이틀, 두번째 요소 타이틀
* @param form : zod 스키마
* @param onSubmitEvent : 폼 이벤트 처리
* @param graphBtn : 하단에 그래프 버튼 유무 처리
* @returns
*/
const BaseLayout = ({ children,titleName, form, onSubmitEvent, graphBtn=false, searchBox }: BaseLayout) => {
return (
<ContentLayout title={titleName.mainTitle}>
<SearchDiv>
<SearchFormLeft>
<SelectBox className="col-span-2" label={searchBox?.labelNm?.first ?? ''} selectArray={[{ text: '한강 유역청', val: '1' }]} onSelectValue={(val) => { }} selectClass={undefined} />
<SelectBox label={searchBox?.labelNm?.second ?? ''} selectArray={[{ text: '경기도', val: '1' }]} className="col-span-2" onSelectValue={() => { }} selectClass='' />
<SelectBox label={searchBox?.labelNm?.third ?? ''} selectArray={[{ text: '양평군', val: '1' }]} className="col-span-3" onSelectValue={() => { }} selectClass='' />
<SelectBox label={searchBox?.labelNm?.fourth ?? ''} selectArray={[{ text: '양평하수처리장', val: '1' }]} className="col-span-3" onSelectValue={() => { }} selectClass='' />
</SearchFormLeft>
{searchBox?.searchChk ? <SearchFormRight><Button>조회</Button></SearchFormRight> : null}
</SearchDiv>
<ListDiv>
<div className="card-header">
<h3 className="text-2xl font-medium">{titleName.firstCardTitle}</h3>
<div>
{graphBtn ? <Button className="!mr-[1.2rem]">다운로드</Button> : null}
{/* 그래프 있을 시 */}
{graphBtn ? <Button className="!mr-[1.2rem]">삭제</Button> : null}
</div>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmitEvent)}>
<div className="card !m-[1.2rem]">
{/* 폼 정보 입력 칸 */}
{children?.[0]}
</div>
</form>
</Form>
</ListDiv>
{children?.[1] != null ? <ListDiv>
<div className="card-header flex items-center justify-between">
<h3 className="text-2xl font-medium">{titleName.secondCardTitle}</h3>
</div>
<div className="card !m-[1.2rem]">{children?.[1]}</div>
</ListDiv> : null}
</ContentLayout>
)
}
export default BaseLayout
+63
View File
@@ -0,0 +1,63 @@
'use client'
import React, { useState } from 'react'
import { FormField, SelectBox } from '@common/business_components/ui'
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'
const formSchema = z.object({
managenm: z.string(),
region: z.string(),
suyeonga: z.number(),
inputdate: z.coerce.date(),
rtkSource: z.string(),
suyeongaPlace: z.string()
})
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),
defaultValues: {
managenm: "",
region: "",
suyeonga: 0,
inputdate: new Date(),
rtkSource: "",
suyeongaPlace: ""
}
})
const [selectedDate, setSelectedDate] = useState(new Date())
return (
<BaseLayout titleName={{ mainTitle: '침입수 산정', firstCardTitle: '실측자료 등록', secondCardTitle: '침입수 분석자료 입력' }} form={form} onSubmitEvent={() => { }} graphBtn={false}>
<div className="grid grid-cols-2 gap-4">
<FormSection name='managenm' form={form} titleNm='○ 관로 관리번호'>
<DatePicker selected={selectedDate} onSelect={(date: Date | undefined) => { if (date) setSelectedDate(date);}} mode={"single"} />
</FormSection>
<FormSection name='region' form={form} titleNm='○ 파일위치'>
<div className="w-[150px]">
<Input id="fileNm" type="file" />
</div>
</FormSection>
</div>
<div className="grid gap-4">
<FormSection name='suyeongaPlace' form={form} titleNm='○ 오수관망도'><div className="w-[150px]">
<Input id="fileNm" type="file" />
</div></FormSection>
<FormSection name='suyeonga' form={form} titleNm='○ 수용가-맨홀 매칭결과'><div className="w-[150px]">
<Input id="fileNm" type="file" />
</div></FormSection>
<FormSection name='inputdate' form={form} titleNm='○ 실수사용량 적용기간'>
<DatePicker selected={selectedDate} onSelect={(date: Date | undefined) => { if (date) setSelectedDate(date) }} mode={"single"} />
</FormSection>
<FormSection name='rtkSource' form={form} titleNm='○ 오수전환율'><Input className="h-[3.2rem] text-center text-xl w-[150px]" /></FormSection>
<div className="flex justify-end mt-1">
<Button type="submit">침입수분석 실행</Button>
</div>
</div>
</BaseLayout>
)
}
export default IrSetting
+156
View File
@@ -0,0 +1,156 @@
"use client"
import { date, z } from "zod"
import { useForm, useFieldArray } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Input, Button, DatePicker } from "@common/components/ui"
import { UiTable } from "@common/business_components/ui"
import React, { useState } from "react"
import BaseLayout from "@/ii/BaseLayout"
import FormSection from "@/components/ui/formSection"
// TODO: 컴포넌트 호출 ~ 필요 코드 수정해야댐
// zod 스키마 정의
const formSchema = z.object({
region: z.string(),
rdiiArea: z.string(),
rtkSource: z.string(),
unitFactors: z.array(z.object({
response: z.string(),
R: z.string(),
T: z.string(),
K: z.string(),
})),
initialDepths: z.array(z.object({
response: z.string(),
Dmax: z.string(),
Drec: z.string(),
Do: z.string(),
})),
suyeonga: z.string(),
rate: z.string(),
date: z.coerce.date(),
rain: z.string(),
})
export default function RTKForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
region: "",
rdiiArea: "",
rtkSource: "",
unitFactors: [
{ response: "Short-Term", R: "", T: "", K: "" },
{ response: "Medium-Term", R: "", T: "", K: "" },
{ response: "Long-Term", R: "", T: "", K: "" },
],
initialDepths: [
{ response: "Short-Term", Dmax: "", Drec: "", Do: "" },
{ response: "Medium-Term", Dmax: "", Drec: "", Do: "" },
{ response: "Long-Term", Dmax: "", Drec: "", Do: "" },
],
}
})
const { fields: unitFactorFields } = useFieldArray({
control: form.control,
name: "unitFactors"
})
const { fields: initialDepthFields } = useFieldArray({
control: form.control,
name: "initialDepths"
})
const onSubmit = (data: z.infer<typeof formSchema>) => {
console.log("✅ 제출 데이터:", data)
}
// 단위도 계수 테이블 설정
const unitFactorHeadlist = [
{ id: 'response', title: 'Response' },
{ id: 'R', title: 'R' },
{ id: 'T', title: 'T' },
{ id: 'K', title: 'K' }
]
// 초기 차단 깊이 테이블 설정
const initialDepthHeadlist = [
{ id: 'response', title: 'Response' },
{ id: 'Dmax', title: 'Dmax' },
{ id: 'Drec', title: 'Drec' },
{ id: 'Do', title: 'Do' }
]
// 테이블 페이지 변경 핸들러
const handlePageChange = (page: number) => {
console.log('Page changed:', page)
}
const [selectedDate, setSelectedDate] = useState(new Date())
return (
<BaseLayout titleName={{ mainTitle: 'RDII 분석', firstCardTitle: 'RDII 분석자료 입력' }} form={form} onSubmitEvent={() => { }} graphBtn={false}>
{[<div className="space-y-[1.2rem]" key="form-section">
<div className="grid grid-cols-2 gap-4">
<FormSection name="rtkSource" form={form} titleNm="○ RTK계수 출처"><Input
className="h-10 text-center text-xl w-[150px]"
placeholder=""
/></FormSection>
<FormSection name="rdiiArea" form={form} titleNm="○ RDII 기여면적(ha)"><Input
className="h-10 text-center text-xl w-[150px]"
placeholder=""
/></FormSection></div>
<div className="grid grid-cols-2">
<div>
<div className="mb-[1.2rem] text-xl font-medium">○ 단위도 계수</div>
<UiTable
headName=""
className="w-[400px]"
tableData={unitFactorFields}
headlist={unitFactorHeadlist}
pageSize={10}
total={0}
onPageChange={handlePageChange}
cellClick={undefined}
>
{null}
</UiTable>
</div>
<div>
<div className="mb-[1.2rem] text-xl font-medium">○ 초기 차단 깊이</div>
<UiTable
headName=""
className="w-[400px]"
tableData={initialDepthFields}
headlist={initialDepthHeadlist}
pageSize={10}
total={0}
onPageChange={handlePageChange}
cellClick={undefined}
>
{null}
</UiTable>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<FormSection name="suyeonga" form={form} titleNm="○ 오수관망도"><div className="w-[150px]">
<Input id="fileNm" type="file" />
</div></FormSection>
<FormSection name="regopm" form={form} titleNm="○ 수용가-맨홀 매칭결과"><div className="w-[150px]">
<Input id="fileNm2" type="file" />
</div></FormSection>
<FormSection name='date' form={form} titleNm='○ 실수사용량 적용기간'>
<DatePicker selected={selectedDate} onSelect={(date: Date | undefined) => { if (date) setSelectedDate(date) }} mode={"single"} />
</FormSection>
<FormSection name='rate' form={form} titleNm='○ 오수전환율'><Input className="h-[3.2rem] text-center text-xl w-[150px]" /></FormSection>
<FormSection name="rain" form={form} titleNm="○ 강우자료"><div className="w-[150px]">
<Input id="fileNm3" type="file" />
</div></FormSection>
</div>
<div className="flex justify-end mt-1">
<Button type="submit">RDII분석 실행</Button>
</div>
</div>
]}
</BaseLayout >
)
}
+64
View File
@@ -0,0 +1,64 @@
'use client'
import React, { useState } from 'react'
import { FormField, SelectBox, UiTable } from '@common/business_components/ui'
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 { title } from 'process'
const formSchema = z.object({
managenm: z.string(),
region: z.string(),
suyeonga: z.number(),
inputdate: z.coerce.date(),
rtkSource: z.string(),
suyeongaPlace: z.string()
})
// TODO: 분석구분에 따라서 테이블 컬럼 변경필요
const tableColumn = [
{ id: 'no', title: '번호' },
{ id: 'hasu', title: '하수처리장' },
{ id: 'guyeok', title: '처리구역' },
{ id: 'sangsu', title: '상수 사용량' },
{ id: 'rate', title: '오수 전환율' },
{ id: 'water', title: '순오수량' },
{ id: 'measureWater', title: '측정하수량' },
{ id: 'ii', title: '침입수량' },
{ id: 'date', title: '분석일자' },
{ id: 'usr', title: '분석자' }
]
const tableInfoData = [{ 'no': '1', 'hasu': '양평하수처리장', 'guyeok': '양평', 'sangsu': '1000', 'rate': '0.5', 'water': '500', 'measureWater': '1000', 'ii': '500', 'date': '2023-10-01', 'usr': '홍길동' }]
const Result = () => {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
managenm: "",
region: "",
suyeonga: 0,
inputdate: new Date(),
rtkSource: "",
suyeongaPlace: ""
}
})
return (
<BaseLayout searchBox={{labelNm: {first: "분석기간",second: "침입수",third: "처리장",fourth: "처리구역",},searchChk: true,}} titleName={{ mainTitle: '분석결과', firstCardTitle: '분석이력' }} form={form} onSubmitEvent={() => { }} graphBtn={true}>
{[<div key='table1'>
<div className="mb-[1.2rem] text-xl font-medium">○ 침입수 산정결과 현황</div>
<UiTable
headName=''
headlist={tableColumn}
tableData={tableInfoData}
total={0}
pageSize={10}
children={undefined}
cellClick={(e) => { }}
onPageChange={(e) => { }}
/>
</div>]}
</BaseLayout>
)
}
export default Result
+37
View File
@@ -0,0 +1,37 @@
import './styles/base.css'
import './styles/components.css'
import type { Metadata } from "next";
import { cookies } from "next/headers";
import { StoreProvider } from './store'
import { ClientLayout } from './clientLayout'
export const metadata: Metadata = {
title: "디지털 하수관로",
description: "디지털 하수관로 시스템입니다",
keywords: ["하수관로"],
openGraph: {
title: "디지털 하수관로",
description: "디지털 하수관로 시스템입니다"
}
};
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const theme = (await cookies()).get("site-theme-key");
return (
<html lang="en" data-theme={theme?.value}>
<body>
<StoreProvider>
<ClientLayout>
{children}
</ClientLayout>
</StoreProvider>
</body>
</html>
)
}
+796
View File
@@ -0,0 +1,796 @@
import React from 'react';
import axios from 'axios';
import { UiModal } from '@common/business_components/ui/dialog_ui';
import { Button } from '@common/components';
import { UiTable2 } from '@common/business_components/ui/table/index-2';
import { CommonTableColumn } from "./table";
const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}`;
const TABS = [
{ key: 'basic', label: '기본정보' },
{ key: 'operation', label: '운영정보' },
{ key: 'report', label: '하수도정비기본계획보고서' },
];
const WORK_TYPES = [
{ key: 'maintenance', label: '보수 및 갱신' },
{ key: 'cleaning', label: '청소 및 준설' },
{ key: 'inspection', label: '점검 및 진단' },
];
const YEARS = Array.from({ length: 100 }, (_, i) => `${2025 - i}`);
// 하수처리구역 테이블 헤더 리스트
const swageZoneTableHeadlist = [
// { id: 'ctpvCd', title: '시도코드' },
{ id: 'ctpvNm', title: '시도명' },
// { id: 'sggCd', title: '시군구코드' },
{ id: 'sggNm', title: '시군구명' },
// { id: 'swtrmtZoneSn', title: '하수처리구역번호' },
{ id: 'swtrmtZoneNm', title: '하수처리구역명' },
{ id: 'swtrmtFcltCd', title: '하수처리시설번호' },
{ id: 'swtrmtFcltNm', title: '하수처리시설명' },
]
// 하수처리시설 테이블 헤더 리스트
const swageFcltTableHeadlist = [
{ id: 'swtrmtFcltCd', title: '하수처리시설코드' },
{ id: 'swtrmtFcltNm', title: '하수처리시설명' },
{ id: 'fcltCpct', title: '시설용량' },
{ id: 'rgnExpln', title: '지역설명' },
{ id: 'cmcnYmd', title: '준공일자' },
{ id: 'oprtngStrtYmd', title: '가동개시일자' },
{ id: 'clsgYmd', title: '폐쇄일자' },
{ id: 'laeNm', title: '지자체명' },
{ id: 'ctpvNm', title: '시도명' },
{ id: 'sggNm', title: '시군구명' },
{ id: 'sareaNm', title: '권역명' },
{ id: 'lrgeSareaNm', title: '대권역명' },
{ id: 'rvrNm', title: '하천명' },
{ id: 'addr', title: '주소' },
{ id: 'daddr', title: '상세주소' },
{ id: 'rdnmAddr', title: '도로명주소' },
{ id: 'dtlRdnmAddr', title: '상세도로명주소' },
{ id: 'dschgWtstNm', title: '방류수계명' },
{ id: 'pllnArea', title: '부지면적' },
{ id: 'mngRprtvBzentyNm', title: '관리대행업제명' },
{ id: 'swtrmtPpltnCnt', title: '하수처리인구수' },
{ id: 'prcsZoneAreaSum', title: '처리구역면적합계' },
{ id: 'geom', title: 'geometry' },
{ id: 'xcrd', title: 'X좌표' },
{ id: 'ycrd', title: 'Y좌표' },
{ id: 'rprsMsrcCn', title: '대표공법내용' },
{ id: 'sttyCrtrBod', title: '법정기준생물화학적산소요구량' },
{ id: 'sttyCrtrToc', title: '법정기준TOC' },
{ id: 'sttyCrtrSs', title: '법정기준SS' },
{ id: 'sttyCrtrTn', title: '법정기준총질소' },
{ id: 'sttyCrtrTph', title: '법정기준총인' },
{ id: 'sttyCrtrClbclCnt', title: '법정기준대장균수' },
{ id: 'bsnNm', title: '유역명' },
];
// '구분' 선택 목록
const BASIC_TYPES = [
{ key: 'SEWER_PIPE', label: '하수관로' },
{ key: 'tb_il_swage_prcs_fclt', label: '하수처리시설' },
{ key: 'tb_ms_swage_prcs_zone', label: '하수처리구역' },
{ key: 'MANHOLE', label: '하수맨홀' },
{ key: 'WASTEWATER_PUMP', label: '오수펌프' },
{ key: 'RAINWATER_PUMP', label: '빗물펌프' },
{ key: 'OUTFALL', label: '우수토실' },
{ key: 'RETENTION_FACILITY', label: '하수저류시설' },
{ key: 'PRIORITY_OUTFALL', label: '중점우수토실' },
];
const sidoList = [
{ id: 0, name: '선택' },
{ id: 26, name: '부산광역시' },
{ id: 28, name: '인천광역시' },
{ id: 30, name: '대전광역시' },
{ id: 41, name: '경기도' },
{ id: 42, name: '강원도' },
{ id: 44, name: '충청남도' },
{ id: 45, name: '전라북도' },
{ id: 46, name: '전라남도' },
{ id: 47, name: '경상북도' },
{ id: 48, name: '경상남도' },
];
// 시도별 시군구 리스트 매핑
const sigunguMap: Record<number, { id: number; name: string }[]> = {
26: [
{ id: 0, name: '선택' },
// ...부산산광역시의 시군구 추가
],
28: [
{ id: 0, name: '선택' },
// ...인천광역시의 시군구 추가
],
30: [
{ id: 0, name: '선택' },
// ...대전광역시의 시군구 추가
],
41: [
{ id: 0, name: '선택' },
{ id: 41820, name: '가평군' },
{ id: 41550, name: '안성시' },
{ id: 41830, name: '양평군' },
{ id: 41670, name: '여주시' },
{ id: 41500, name: '이천시' },
{ id: 41430, name: '의왕시' },
{ id: 41150, name: '의정부시' },
{ id: 41480, name: '파주시' },
{ id: 41650, name: '포천시' },
// ...경기도의 시군구 추가
],
42: [
{ id: 0, name: '선택' },
{ id: 42170, name: '동해시' },
{ id: 42110, name: '춘천시' },
{ id: 42720, name: '홍천군' },
// ...강원도의 시군구 추가
],
44: [
{ id: 0, name: '선택' },
{ id: 44770, name: '서천군' },
// ...충청남도의 시군구 추가
],
45: [
{ id: 0, name: '선택' },
{ id: 45130, name: '군산시' },
{ id: 45190, name: '남원시' },
{ id: 45730, name: '무주군' },
{ id: 45800, name: '부안군' },
{ id: 45770, name: '순창군' },
{ id: 45140, name: '익산시' },
{ id: 45750, name: '임실군' },
{ id: 45710, name: '완주군' },
{ id: 45110, name: '전주시' },
// ...전라북도의 시군구 추가
],
46: [
{ id: 0, name: '선택' },
{ id: 46810, name: "강진군" },
{ id: 46770, name: "고흥군" },
{ id: 46720, name: "곡성군" },
{ id: 46730, name: "구례군" },
{ id: 46230, name: "광양시" },
{ id: 46170, name: "나주시" },
{ id: 46110, name: "목포시" },
{ id: 46840, name: "무안군" },
{ id: 46150, name: "순천시" },
{ id: 46910, name: "신안군" },
{ id: 46130, name: "여수시" },
{ id: 46890, name: "완도군" },
// ...전라남도의 시군구 추가
],
47: [
{ id: 0, name: '선택' },
{ id: 47130, name: "경주시" },
{ id: 47830, name: "고령군" },
{ id: 47190, name: "구미시" },
{ id: 47720, name: "군위군" },
{ id: 47150, name: "김천시" },
{ id: 47280, name: "문경시" },
{ id: 47920, name: "봉화군" },
{ id: 47250, name: "상주시" },
{ id: 47770, name: "영덕군" },
{ id: 47760, name: "영양군" },
{ id: 47210, name: "영주시" },
{ id: 47900, name: "예천군" },
{ id: 47930, name: "울진군" },
{ id: 47730, name: "의성군" },
{ id: 47750, name: "청송군" },
{ id: 47850, name: "칠곡군" },
{ id: 47110, name: "포항시" },
// ...경상북도의 시군구 추가
],
48: [
{ id: 0, name: '선택' },
{ id: 48310, name: "거제시" },
{ id: 48250, name: "김해시" },
{ id: 48270, name: "밀양시" },
{ id: 48220, name: "통영시" },
// ...경상남도의 시군구 추가
],
// ...나머지 시도별 시군구 추가
};
const 처리구역List = ['선택', 'A', 'B'];
const 설치년도List = ['선택', ...YEARS];
// swageZoneTableHeadlist, tableData 타입 예시 (실제 타입에 맞게 수정)
type TableHead = {
key: string;
label: string;
};
type TableRow = Record<string, any>;
interface SearchModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
// filterParams 타입 정의 추가
type FilterParams = {
swtrmt_zone_nm?: string;
ctpv_cd?: number;
sgg_cd?: number;
swtrmt_fclt_nm?: string;
// 추가 필터 속성들...
};
// 해당 년/월의 마지막 일 구하는 함수
function getLastDayOfMonth(year: string, month: string) {
if (!year || !month) return 31;
const y = parseInt(year, 10);
const m = parseInt(month, 10);
if (isNaN(y) || isNaN(m)) return 31;
return new Date(y, m, 0).getDate();
}
// 날짜 조합 함수
const getDateString = (year: string, month: string, day: string, defaultDate: string) => {
if (!year && !month && !day) return defaultDate;
const y = year || defaultDate.slice(0, 4);
const m = month || defaultDate.slice(4, 6);
const d = day || defaultDate.slice(6, 8);
return `${y}${m}${d}`;
};
const SearchModal: React.FC<SearchModalProps> = ({ open, onOpenChange }) => {
const [activeTab, setActiveTab] = React.useState('basic');
const [workType, setWorkType] = React.useState(WORK_TYPES[0]?.key ?? '');
const [startYear, setStartYear] = React.useState('');
const [endYear, setEndYear] = React.useState('');
const [showTable, setShowTable] = React.useState(false);
const [curPage, setCurPage] = React.useState(1);
const [basicType, setBasicType] = React.useState(BASIC_TYPES[0]?.key ?? '');
const [하수처리구역명, set하수처리구역명] = React.useState('');
const [하수처리시설명, set하수처리시설명] = React.useState('');
const [시도, set시도] = React.useState(0);
const [시군구, set시군구] = React.useState(0);
const [처리구역, set처리구역] = React.useState('');
const [설치년도1, set설치년도1] = React.useState('');
const [설치년도2, set설치년도2] = React.useState('');
const [tableData, setTableData] = React.useState<any[]>([]);
const [시설용량최소, set시설용량최소] = React.useState('');
const [시설용량최대, set시설용량최대] = React.useState('');
// 날짜 상태 선언
const [가동개시_시작_년, set가동개시_시작_년] = React.useState('');
const [가동개시_시작_월, set가동개시_시작_월] = React.useState('');
const [가동개시_시작_일, set가동개시_시작_일] = React.useState('');
const [가동개시_종료_년, set가동개시_종료_년] = React.useState('');
const [가동개시_종료_월, set가동개시_종료_월] = React.useState('');
const [가동개시_종료_일, set가동개시_종료_일] = React.useState('');
// 오늘 날짜 구하기
const today = new Date();
const todayYear = today.getFullYear().toString();
const todayMonth = (today.getMonth() + 1).toString().padStart(2, '0');
const todayDate = today.getDate().toString().padStart(2, '0');
// UiTable2에 넘길 headlist를 BASIC_TYPES 값에 따라 분기 처리
let dynamicHeadlist = swageZoneTableHeadlist;
if (basicType === 'tb_ms_swage_prcs_zone') {
dynamicHeadlist = swageZoneTableHeadlist; // 하수처리구역
} else if (basicType === 'tb_il_swage_prcs_fclt') {
dynamicHeadlist = swageFcltTableHeadlist; // 하수처리시설
}
// 선택된 시도에 따라 시군구 리스트 가져오기
const selectedSigunguList = sigunguMap[시도] || [{ id: 0, name: '선택' }];
// 조회 버튼 클릭 시 1페이지로 이동하도록 수정
const handleSearch = () => {
setCurPage(1); // 페이지를 1로 초기화
fetchFilteredData();
setShowTable(true);
};
// 선택된 필터 값을 저장할 상태 추가
const [filterParams, setFilterParams] = React.useState<FilterParams>({});
// cellClick 함수 타입 지정
const handleCellClick = (row: TableRow, col: TableHead) => {
// 원하는 동작 구현
console.log(row, col);
};
const modalMinWidth = 'min-w-[900px]';
const modalMaxWidth = 'max-w-6xl';
// 모달 높이 동적 계산
const contentRef = React.useRef<HTMLDivElement>(null);
const [dynamicHeight, setDynamicHeight] = React.useState<number | undefined>(undefined);
// 구분(basicType)이 바뀔 때마다 queryParams, filterParams 초기화
React.useEffect(() => {
// filterParams 초기화
setFilterParams({});
set시설용량최소('');
set시설용량최대('');
set가동개시_시작_년('');
set가동개시_시작_월('');
set가동개시_시작_일('');
set가동개시_종료_년('');
set가동개시_종료_월('');
set가동개시_종료_일('');
set하수처리구역명('');
set하수처리시설명('');
set시도(0);
set시군구(0);
set처리구역('');
set설치년도1('');
set설치년도2('');
// queryParams는 fetchFilteredData 내에서 매번 새로 생성되므로 별도 상태 초기화는 필요 없음
}, [basicType]);
// 필터 값이 변경될 때 쿼리 파라미터 업데이트
React.useEffect(() => {
const params: FilterParams = {};
if (하수처리구역명) {
params.swtrmt_zone_nm = 하수처리구역명;
}
// 시도 선택 시 (0이 아닌 경우)
if (시도 !== 0) {
const selectedSido = sidoList.find(item => item.id === 시도);
if (selectedSido) {
params.ctpv_cd = selectedSido.id;
}
}
// 시군구 선택 시 (0이 아닌 경우)
if (시군구 !== 0) {
const selectedSigungu = (sigunguMap[시도] || []).find((item: { id: number; name: string }) => item.id === 시군구);
if (selectedSigungu) {
params.sgg_cd = selectedSigungu.id;
}
}
// 하수처리시설명이 있으면 추가
if (하수처리시설명) {
params.swtrmt_fclt_nm = 하수처리시설명;
}
setFilterParams(params);
}, [하수처리구역명, 시도, 시군구, 하수처리시설명]);
React.useEffect(() => {
if (contentRef.current) {
setDynamicHeight(contentRef.current.scrollHeight + 60); // 여유 padding
}
}, [activeTab, showTable, curPage, tableData.length]);
// 검색 API 호출 함수
const fetchFilteredData = () => {
const queryParams = new URLSearchParams({
lyrTypeNm: basicType
});
const cqlFilters: string[] = [];
// 구분별 분기 처리
if (basicType === 'tb_ms_swage_prcs_zone') {
// 하수처리구역
// 시도
if (filterParams.ctpv_cd) {
cqlFilters.push(`ctpv_cd='${filterParams.ctpv_cd}'`);
}
// 시군구
if (filterParams.sgg_cd) {
cqlFilters.push(`sgg_cd='${filterParams.sgg_cd}'`);
}
// 하수처리구역명
if (filterParams.swtrmt_zone_nm) {
cqlFilters.push(`swtrmt_zone_nm ILIKE '%${filterParams.swtrmt_zone_nm}%'`);
}
// 하수처리시설명
if (filterParams.swtrmt_fclt_nm) {
cqlFilters.push(`swtrmt_fclt_nm ILIKE '%${filterParams.swtrmt_fclt_nm}%'`);
}
} else if (basicType === 'tb_il_swage_prcs_fclt') {
// 하수처리시설
// 가동개시일자 범위 필터 추가
// 기본값: 19000101 ~ 오늘
const startDate = getDateString(가동개시_시작_년, 가동개시_시작_월, 가동개시_시작_일, '19000101');
const endDate = getDateString(가동개시_종료_년, 가동개시_종료_월, 가동개시_종료_일, `${todayYear}${todayMonth}${todayDate}`);
cqlFilters.push(`oprtng_strt_ymd >= '${startDate}' and oprtng_strt_ymd <= '${endDate}'`);
// 시설용량 범위(fclt_cpct) 필터 추가
if (시설용량최소 && 시설용량최대) cqlFilters.push(`fclt_cpct BETWEEN ${시설용량최소} AND ${시설용량최대}`);
else if (시설용량최소) cqlFilters.push(`fclt_cpct >= ${시설용량최소}`);
else if (시설용량최대) cqlFilters.push(`fclt_cpct <= ${시설용량최대}`);
// 하수처리시설명
if (filterParams.swtrmt_fclt_nm) {
cqlFilters.push(`swtrmt_fclt_nm ILIKE '%${filterParams.swtrmt_fclt_nm}%'`);
}
} else {
// 기타 구분(예: 하수관로, 맨홀 등) - 필요시 추가
// 예시: 이름으로 검색
if (filterParams.swtrmt_fclt_nm) {
cqlFilters.push(`swtrmt_fclt_nm ILIKE '%${filterParams.swtrmt_fclt_nm}%'`);
}
}
// 파라미터 이름 오타 주의! (Swagger와 동일하게)
if (cqlFilters.length > 0) {
queryParams.append('cqlFiter', cqlFilters.join(' and '));
}
console.log('API URL:', `${API_URL}/v1/map/features?${queryParams.toString()}`);
axios.get(`${API_URL}/v1/map/features?${queryParams.toString()}`)
.then(res => {
// 결과값을 표에 사용할 수 있도록 상태에 저장
console.log('필터링된 API 결과:', res.data.length);
console.log('필터링된 API 결과:', res.data);
setTableData(res.data || []); // res.data 구조에 따라 조정 필요
setShowTable(true);
})
.catch(err => {
console.error('API ERROR:', err);
setTableData([]); // 에러 시 빈 배열로 초기화
});
};
const renderBody = () => (
<>
{/* 탭 영역 */}
<div className="flex gap-2 mb-4">
{TABS.map(tab => (
<button
key={tab.key}
className={`px-4 py-2 rounded-t border border-b-0 text-base font-semibold
${activeTab === tab.key
? 'bg-[#bfcfe6] text-black'
: 'bg-[#e6e6e6] text-gray-700'
}`}
style={{
borderColor: '#888',
borderBottom: activeTab === tab.key ? '2px solid white' : undefined,
minWidth: 140,
fontSize: '1.1rem',
height: '2.4rem', // 글자 크기에서 약간의 여백만 있도록 높이 조정
}}
onClick={() => setActiveTab(tab.key)}
>
{tab.label}
</button>
))}
</div>
{/* 기본정보 탭 */}
{activeTab === 'basic' && (
<div className="border rounded p-6 mb-4">
{/* 구분 */}
<div className="flex justify-between items-center mb-3 gap-4">
<div className="flex items-center">
<label className="font-semibold text-xl mr-2">구분</label>
<select
className="border rounded text-xl px-2 py-1"
value={basicType}
onChange={e => setBasicType(e.target.value)}
>
{BASIC_TYPES.map(opt => (
<option key={opt.key} value={opt.key}>{opt.label}</option>
))}
</select>
</div>
</div>
{/* 분기처리: 하수처리구역 */}
{basicType === 'tb_ms_swage_prcs_zone' && (
<div className="mb-3">
<div className="flex items-center mb-2">
<label className="font-semibold text-xl mr-2">시도</label>
<select className="border rounded text-xl px-2 py-1 mr-4" value={시도} onChange={e => set시도(parseInt(e.target.value))}>
{sidoList.map(opt => (
<option key={opt.id} value={opt.id}>{opt.name}</option>
))}
</select>
<label className="font-semibold text-xl mr-2">시군구</label>
<select className="border rounded text-xl px-2 py-1 mr-4" value={시군구} onChange={e => set시군구(parseInt(e.target.value))}>
{selectedSigunguList.map(opt => (
<option key={opt.id} value={opt.id}>{opt.name}</option>
))}
</select>
</div>
<div className="flex items-center">
<label className="font-semibold text-xl mr-2">하수처리구역명</label>
<input className="border rounded text-xl px-2 py-1 mr-4" value={하수처리구역명} onChange={e => set하수처리구역명(e.target.value)} />
</div>
<div className="flex items-center">
<label className="font-semibold text-xl mr-2">하수처리시설명</label>
<input className="border rounded text-xl px-2 py-1 mr-4" value={하수처리시설명} onChange={e => set하수처리시설명(e.target.value)} />
</div>
</div>
)}
{/* 분기처리: 하수처리시설 */}
{basicType === 'tb_il_swage_prcs_fclt' && (
<div className="mb-3">
{/* 가동개시일자 */}
<div className="flex items-center mb-2">
<label className="font-semibold text-xl mr-2">가동개시일자</label>
{/* 시작일자 */}
<select
className="border rounded text-xl px-2 py-1 mr-1"
value={가동개시_시작_년}
onChange={e => set가동개시_시작_년(e.target.value)}
>
<option value="">년</option>
{Array.from({ length: today.getFullYear() - 1900 + 1 }, (_, i) => {
const y = (today.getFullYear() - i).toString();
return <option key={y} value={y}>{y}</option>;
})}
</select>
{/* <select
className="border rounded text-xl px-2 py-1 mr-1"
value={가동개시_시작_월}
onChange={e => set가동개시_시작_월(e.target.value)}
>
<option value="">월</option>
{[...Array(12)].map((_, i) => {
const m = (i + 1).toString().padStart(2, '0');
return <option key={m} value={m}>{m}</option>;
})}
</select>
<select
className="border rounded text-xl px-2 py-1 mr-4"
value={가동개시_시작_일}
onChange={e => set가동개시_시작_일(e.target.value)}
>
<option value="">일</option>
{[...Array(31)].map((_, i) => {
const d = (i + 1).toString().padStart(2, '0');
return <option key={d} value={d}>{d}</option>;
})}
</select> */}
<span className="mx-1">~</span>
{/* 종료일자 */}
<select
className="border rounded text-xl px-2 py-1 mr-1"
value={가동개시_종료_년}
onChange={e => set가동개시_종료_년(e.target.value)}
>
<option value="">년</option>
{Array.from({ length: today.getFullYear() - 1900 + 1 }, (_, i) => {
const y = (today.getFullYear() - i).toString();
// 시작년이 선택된 경우, 종료년은 시작년 이상만 표시
if (가동개시_시작_년 && parseInt(y) < parseInt(가동개시_시작_년)) return null;
return <option key={y} value={y}>{y}</option>;
})}
</select>
{/* <select
className="border rounded text-xl px-2 py-1 mr-1"
value={가동개시_종료_월}
onChange={e => set가동개시_종료_월(e.target.value)}
>
<option value="">월</option>
{[...Array(12)].map((_, i) => {
const m = (i + 1).toString().padStart(2, '0');
// 시작년/월이 선택된 경우, 종료년이 시작년과 같으면 시작월 이상만 표시
if (
가동개시_시작_년 &&
가동개시_종료_년 === 가동개시_시작_년 &&
가동개시_시작_월 &&
parseInt(m) < parseInt(가동개시_시작_월)
) return null;
return <option key={m} value={m}>{m}</option>;
})}
</select>
<select
className="border rounded text-xl px-2 py-1 mr-4"
value={가동개시_종료_일}
onChange={e => set가동개시_종료_일(e.target.value)}
>
<option value="">일</option>
{[...Array(31)].map((_, i) => {
const d = (i + 1).toString().padStart(2, '0');
// 시작년/월/일이 선택된 경우, 종료년/월이 시작년/월과 같으면 시작일 이상만 표시
if (
가동개시_시작_년 &&
가동개시_종료_년 === 가동개시_시작_년 &&
가동개시_시작_월 &&
가동개시_종료_월 === 가동개시_시작_월 &&
가동개시_시작_일 &&
parseInt(d) < parseInt(가동개시_시작_일)
) return null;
// 시작년/월이 선택된 경우, 종료년/월이 시작년/월과 같고 시작일이 선택되었으면 시작일 이상만 표시
if (
가동개시_시작_년 &&
가동개시_종료_년 === 가동개시_시작_년 &&
가동개시_시작_월 &&
가동개시_종료_월 === 가동개시_시작_월 &&
가동개시_시작_일 &&
parseInt(d) < parseInt(가동개시_시작_일)
) return null;
// 시작년/월만 선택된 경우, 종료년/월이 시작년/월과 같으면 모든 일 표시(위 조건에 포함)
// 시작년만 선택된 경우, 종료년이 시작년과 같으면 모든 월/일 표시(위 조건에 포함)
return <option key={d} value={d}>{d}</option>;
})}
</select> */}
</div>
{/* 시설용량 */}
<div className="flex items-center mb-2">
<label className="font-semibold text-xl mr-2">시설용량</label>
<input
type="text"
className="border rounded text-xl px-2 py-1 mr-2 w-40 placeholder:text-center"
placeholder="최소"
value={시설용량최소}
onChange={e => {
// 숫자만 입력 허용
const val = e.target.value.replace(/[^0-9]/g, '');
set시설용량최소(val);
}}
/>
<span className="text-xl mr-2">~</span>
<input
type="text"
className="border rounded text-xl px-2 py-1 mr-4 w-40 placeholder:text-center"
placeholder="최대"
value={시설용량최대}
onChange={e => {
// 숫자만 입력 허용
const val = e.target.value.replace(/[^0-9]/g, '');
set시설용량최대(val);
}}
/>
</div>
{/* 하수처리시설명 */}
<div className="flex items-center">
<label className="font-semibold text-xl mr-2">하수처리시설명</label>
<input className="border rounded text-xl px-2 py-1 mr-4" value={하수처리시설명} onChange={e => set하수처리시설명(e.target.value)} />
</div>
</div>
)}
{/* 조회 버튼 중앙 정렬 */}
<div className="flex justify-center">
<Button className="w-40 text-base py-2" onClick={handleSearch}>조회</Button>
</div>
</div>
)}
{/* 운영정보 탭 */}
{activeTab === 'operation' && (
<div className="border rounded p-6 mb-4">
<div className="flex items-center gap-6 mb-6">
<label className="w-24 font-semibold text-base">업무구분</label>
<select
className="border rounded px-3 py-2 text-base"
value={workType}
onChange={e => setWorkType(e.target.value)}
style={{ minWidth: 140 }}
>
{WORK_TYPES.map(w => (
<option key={w.key} value={w.key}>{w.label}</option>
))}
</select>
<label className="w-16 text-center font-semibold text-base">년도</label>
<select
className="border rounded px-3 py-2 text-base"
value={startYear}
onChange={e => setStartYear(e.target.value)}
style={{ minWidth: 100 }}
>
<option value="">선택</option>
{YEARS.map(y => (
<option key={y} value={y}>{y}</option>
))}
</select>
<span className="mx-2 text-lg">~</span>
<select
className="border rounded px-3 py-2 text-base"
value={endYear}
onChange={e => setEndYear(e.target.value)}
style={{ minWidth: 100 }}
>
<option value="">선택</option>
{YEARS.map(y => (
<option key={y} value={y}>{y}</option>
))}
</select>
</div>
<div className="flex justify-center">
<Button className="w-40 text-base py-2" onClick={() => setShowTable(true)}>
조회
</Button>
</div>
</div>
)}
{/* 하수도정비기본계획보고서서 탭 */}
{activeTab === 'report' && (
<div className="border rounded p-6 mb-4">
{/* 시도, 시군구, 하수처리구역 한 줄 */}
<div className="flex items-center mb-3">
<label className="font-semibold mr-2">시도</label>
<select className="border rounded px-2 py-1 mr-4" value={시도} onChange={e => set시도(e.target.value)}>
{sidoList.map(opt => (
<option key={opt.id} value={opt.name}>{opt.id}</option>
))}
</select>
<label className="font-semibold mr-2">시군구</label>
<select className="border rounded px-2 py-1 mr-4" value={시군구} onChange={e => set시군구(e.target.value)}>
{/* {sigunguMap.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))} */}
</select>
<label className="font-semibold mr-2">하수처리구역</label>
<select className="border rounded px-2 py-1 mr-2" value={처리구역} onChange={e => set처리구역(e.target.value)}>
{처리구역List.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
<input className="border rounded px-2 py-1 w-24" placeholder="0000" />
</div>
{/* 승인년도 한 줄 */}
<div className="flex items-center mb-4">
<label className="font-semibold mr-2">승인년도</label>
<select className="border rounded px-2 py-1 mr-2" value={설치년도1} onChange={e => set설치년도1(e.target.value)}>
{설치년도List.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
<span className="mx-1">~</span>
<select className="border rounded px-2 py-1" value={설치년도2} onChange={e => set설치년도2(e.target.value)}>
{설치년도List.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</div>
{/* 조회 버튼 중앙 정렬 */}
<div className="flex justify-center">
<Button className="w-40 text-base py-2">조회</Button>
</div>
</div>
)}
{/* 📋 결과 리스트 */}
<div className="border-t pt-6 space-y-2" style={{ maxWidth: '100vw', maxHeight: '60vh', overflowX: 'auto'}}>
{/* 선택된 탭의 label */}
<div className="text-2xl font-bold mb-2">
{TABS.find(tab => tab.key === activeTab)?.label}
</div>
{/* "▶" + 선택된 탭 label + " 조회 결과" */}
<div className="text-lg font-semibold mb-6">
▶ {TABS.find(tab => tab.key === activeTab)?.label} 조회 결과
</div>
<UiTable2 headName='' tableData={tableData} headlist={dynamicHeadlist}
pageSize={tableData.length < 10 ? tableData.length : 10}
total={tableData.length} children={undefined}
cellClick={() => { }} onPageChange={setCurPage}
resetPageNo={false}
cur={curPage}
/>
</div>
</>
);
{/* </DialogContent>
</Dialog>
); */}
return (
<UiModal
open={open}
onOpenChange={onOpenChange}
bodyClass={`
${modalMaxWidth} ${modalMinWidth}
transition-all duration-300
w-full overflow-hidden
`}
body={renderBody()}
headerTitle="검색"
// 필요시 footerContent 등 추가
/>
);
}
export default React.memo(SearchModal);
@@ -0,0 +1,50 @@
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@common/components/ui";
import React from "react";
interface InputSectionProps {
title: string;
data: { year: string; type: string }[];
sectionKey: string;
onInputChange: (section: string, index: number, field: string, value: string) => void;
}
const InputSection: React.FC<InputSectionProps> = ({ title, data, sectionKey, onInputChange }) => {
return (
<div>
{/* 섹션 제목 */}
<div className="card-header">
<h3 className="text-2xl mx-3 font-medium text-label">{title}</h3>
</div>
{/* 테이블 */}
<div className="card !m-[1.2rem] !p-0r">
<Table variant="secondary" className="w-full">
<TableHeader>
<TableRow>
<TableHead className="border border-gray-300 px-2 py-2 text-sm">년도</TableHead>
<TableHead className="border border-gray-300 px-2 py-2 text-sm">구분</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((row, index) => (
<TableRow key={index}>
<TableCell className="border border-gray-300 px-2 py-2 text-sm">
<input
type="text"
value={row.year}
onChange={(e) => onInputChange(sectionKey, index, "year", e.target.value)}
className="w-full text-xl border-radius-0 px-3 py-2 text-base text-center leading-normal"
/>
</TableCell>
<TableCell className="border border-gray-300 px-2 py-2 text-sm text-center leading-normal">
{row.type}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
);
};
export default InputSection;
@@ -0,0 +1,46 @@
import smh from "../popupComponent/smh";
import Ssc from "../popupComponent/Ssc";
import SwtrmtFclt from "../popupComponent/SwtrmtFclt";
// 레이어 이름과 컴포넌트를 매핑
const layerContentMap: { [key: string]: { component?: React.FC<any> ; props?: any; } } = {
// TODO: gis데이터 추가되면 추가 작업 필요
"하수처리시설": {
component: SwtrmtFclt,
},
"하수처리구역": { },
"중점우수토실":{component: Ssc,},
"하수관로":{component: SwtrmtFclt,},
"하수맨홀":{component: smh,},
};
// 동적 컴포넌트 반환 함수
export const getLayerContentComponent = (layerNm: string): React.FC => {
const component = layerContentMap[layerNm];
if (!component || !component.component) {
// component가 없으면 null 반환
return () => null;
}
return component.component;
};
// 사용자 입력 필드 (보수 및 갱신, 청소 및 준설, 점검 및 진단)
export const handleInputChange = (section: string,
index: number,
field: string,
value: string,
dataMap: {
[key: string]: {
data: { year: string; type: string }[];
setData: React.Dispatch<React.SetStateAction<{ year: string; type: string }[]>>;
};
}
) => {
const target = dataMap[section];
if (!target) return;
const updated = [...target.data];
if (updated[index]) {
updated[index][field as keyof typeof updated[number]] = value;
}
target.setData(updated);
};
@@ -0,0 +1,51 @@
// 하수처리시설
export const SwtrmtFcltCol = [
{ id: 'swtrmtFcltNm', title: '하수처리시설명' },
{ id: 'fcltCpct', title: '시설용량' },
{ id: 'rgnExpln', title: '지역설명' },
{ id: 'cmcnYmd', title: '준공일자' },
{ id: 'oprtngStrtYmd', title: '가동개시일자' },
{ id: 'clsgYmd', title: '폐쇄일자' },
{ id: 'prcsMsrcCd', title: '처리공법코드' },
{ id: 'laeCd', title: '지자체코드' },
{ id: 'laeNm', title: '지자체명' },
{ id: 'ctpvNm', title: '시도명' },
{ id: 'sggNm', title: '시군구명' },
{ id: 'sareaCd', title: '권역코드' },
{ id: 'sareaNm', title: '권역명' },
{ id: 'lrgeSareaNm', title: '대권역명' },
{ id: 'rvrNm', title: '하천명' },
{ id: 'addr', title: '주소' },
{ id: 'daddr', title: '상세주소' },
{ id: 'rdnmAddr', title: '도로명주소' },
{ id: 'dtlRdnmAddr', title: '상세도로명주소' },
{ id: 'dschgWtstNm', title: '방류수계명' },
{ id: 'pllnArea', title: '부지면적' },
{ id: 'mngRprtvBzentyNm', title: '관리대행업제명' },
{ id: 'swtrmtPpltnCnt', title: '하수처리인구수' },
{ id: 'prcsZoneAreaSum', title: '처리구역면적합계' },
{ id: 'xcrd', title: 'X좌표' },
{ id: 'ycrd', title: 'Y좌표' },
{ id: 'oprtngSttsCd', title: '가동상태코드' },
{ id: 'rprsMsrcCn', title: '대표공법내용' },
{ id: 'sttyCrtrBod', title: '법정기준생물화학적산소요구량' },
{ id: 'sttyCrtrToc', title: '법정기준TOC' },
{ id: 'sttyCrtrSs', title: '법정기준SS' },
{ id: 'sttyCrtrTn', title: '법정기준총질소' },
{ id: 'sttyCrtrTph', title: '법정기준총인' },
{ id: 'sttyCrtrClbclCnt', title: '법정기준대장균수' },
{ id: 'bsnNm', title: '유역명' },
{ id: 'obsrvnSptCd', title: '관측지점코드' },
];
// 하수처리구역
export const SwtrmtPrcsZoneCol = [
{ id: 'swtrmtZoneSn', title: '하수처리구역 일련번호' },
{ id: 'swtrmtZoneNm', title: '하수처리구역명' },
{ id: 'swtrmtFcltCd', title: '하수처리시설코드' },
{ id: 'ctpvCd', title: '시도코드' },
{ id: 'ctpvNm', title: '시도명' },
{ id: 'sggCd', title: '시군구코드' },
{ id: 'sggNm', title: '시군구명' },
{ id: 'swtrmtFcltNm', title: '하수처리시설명' },
]
@@ -0,0 +1,66 @@
// 우수토실
import Modal from '@/components/ui/modal';
import { VerticalTable } from '@/components/ui/verticalTable';
import { UiTable } from '@common/business_components/ui';
import { Button, TableHead, TableRow } from '@common/components/ui';
import React from 'react'
const inputColumn = [
{ id: "guyeok", title: "처리구역", type: "select", options: ["구역1", "구역2"] },
{ id: "mainSscYn", title: "중점우수토실 대상여부", type: "select", options: ['Y', 'N'] },
];
const headlist = [
{ id: 'startrain', title: '강우시작일시' }, { id: 'endrain', title: '강우종료일시' }, { id: '선행건기', title: '선행건기(h)' }, { id: '강우정보', title: '강우정보' }, { id: '누적강우량', title: '누적강우량(mm)' }, { id: '지속시간', title: '지속시간(h)' }, { id: '강우강도', title: '강우강도(mm/h)' },
]
const headlist3 = [{ id: 'nm', title: '회차' }, { id: 'date', title: '측정일자' }, { id: 'measure', title: '수량(m)' }, { id: 'bod', title: 'BOD(mg/L)' }, { id: 'toc', title: 'TOC(mg/L)' }, { id: '오염부하량', title: '오염부하량(kg)' }]
const Ssc = () => {
const [isModalOpen, setIsModalOpen] = React.useState(false);
const openModal = () => {
setIsModalOpen(true);
};
const closeModal = () => {
setIsModalOpen(false);
};
return (
<div><VerticalTable headName='자체 입력정보' tableData={[]} headlist={inputColumn} pageSize={0} total={0} children={undefined} cellClick={undefined} isEditable={true} onPageChange={function (page: number): void {
throw new Error('Function not implemented.');
}} />
<div>
<div className="card-header">
<h3 className="text-2xl mx-3 font-medium text-label">중점우수토실 운영정보</h3>
</div>
<Button className='mx-auto block w-64 mt-3 py-2 text-white rounded-sm text-sm font-medium hover:bg-blue-700 bg-blue-600' onClick={openModal}>상세보기</Button>
<Modal isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} closeModal={closeModal}>
<div className='grid grid-col-3'><UiTable headName='강우정보' tableData={[]} headlist={headlist} pageSize={0} total={0} children={undefined} cellClick={undefined} onPageChange={function (page: number): void {
throw new Error('Function not implemented.');
}}></UiTable>
<UiTable headName='측정정보' tableData={[]} headlist={[]} pageSize={0} total={0} cellClick={undefined} onPageChange={function (page: number): void {
throw new Error('Function not implemented.');
}}> {/* 커스텀 헤더 */}
<TableRow>
<TableHead colSpan={5} className="text-center bg-blue-100 font-bold">
수량
</TableHead>
<TableHead colSpan={3} className="text-center bg-blue-100 font-bold">
수질
</TableHead>
</TableRow>
<TableRow>
<TableHead className="text-center">측정시작일시</TableHead>
<TableHead className="text-center">측정종료일시</TableHead>
<TableHead className="text-center">측정간격(분)</TableHead>
<TableHead className="text-center">측정기기설치위치</TableHead>
<TableHead className="text-center">유량계 종류 및 모델명</TableHead>
<TableHead className="text-center">채수시작일시</TableHead>
<TableHead className="text-center">채수종료일시</TableHead>
<TableHead className="text-center">채수횟수</TableHead>
</TableRow></UiTable>
<UiTable headName='수량/수질측정 정보' tableData={[]} headlist={headlist3} pageSize={0} total={0} children={undefined} cellClick={undefined} onPageChange={function (page: number): void {
throw new Error('Function not implemented.');
}}></UiTable></div>
</Modal>
</div>
</div>
)
}
export default Ssc
@@ -0,0 +1,45 @@
import { VerticalTable } from '@/components/ui/verticalTable';
import React, { useState } from 'react'
import InputSection from '../InputSection';
import { handleInputChange } from '../common';
// 하수처리시설
// TODO: 컬럼에 보고서 엉 ? 이거 버튼누르면 팝업으로 상세화면 띄워주는 건지 확인 필요
const inputColumn = [
{ id: "title", title: "설치주제", type: "select", options: ["BTL", "재정사업"] },
{ id: "guyeok", title: "처리구역", type: "select", options: ["구역1", "구역2"] },
{ id: "basu", title: "배수구역", type: "input" },
{ id: "bungu", title: "배수분구", type: "input" },
{ id: "wltjs", title: "지선/간선", type: "select", options: ["지선", "간선"] },
{ id: "flow", title: "흐름방식", type: "select", options: ["자연유하", "압송"] },
{ id: "place", title: "매설위치", type: "select", options: ["도로", "하상"] },
];
const SwtrmtFclt = () => {
const [repairData, setRepairData] = useState([
{ year: "2025", type: "개대체" },
{ year: "2025", type: "보수" },
]);
const [cleaningData, setCleaningData] = useState([
{ year: "2025", type: "청소" },
{ year: "2025", type: "준설" },
]);
const [inspectionData, setInspectionData] = useState([
{ year: "2025", type: "관로기술진단" },
{ year: "2025", type: "관로기술진단" },
]);
const dataMap = {
repair: { data: repairData, setData: setRepairData },
cleaning: { data: cleaningData, setData: setCleaningData },
inspection: { data: inspectionData, setData: setInspectionData },
};
return (
<div>
<VerticalTable headName='자체 입력정보' tableData={[]} headlist={inputColumn} pageSize={20} total={0} children={undefined} cellClick={undefined} isEditable={true} onPageChange={function (page: number): void {
throw new Error("Function not implemented.");
}}></VerticalTable>
<InputSection title="보수 및 갱신" data={repairData} sectionKey="repair" onInputChange={(section, index, field, value) => handleInputChange(section, index, field, value, dataMap)} />
<InputSection title="청소 및 준설" data={cleaningData} sectionKey="cleaning" onInputChange={(section, index, field, value) => handleInputChange(section, index, field, value, dataMap)} />
<InputSection title="점검 및 진단" data={inspectionData} sectionKey="inspection" onInputChange={(section, index, field, value) => handleInputChange(section, index, field, value, dataMap)} /></div>
)
}
export default SwtrmtFclt
@@ -0,0 +1,21 @@
// 하수맨홀
import { VerticalTable } from '@/components/ui/verticalTable';
import React from 'react'
const inputColumn = [
{ id: "guyeok", title: "처리구역", type: "select", options: ["구역1", "구역2"] },
{ id: "basu", title: "배수구역", type: "input" },
{ id: "bungu", title: "배수분구", type: "input" },
{ id: "place", title: "매설위치", type: "select", options: ["도로", "하상"] },
{id:'spec', title:'규격', type:'input'},
];
const Smh = () => {
return (
<div>
<VerticalTable headName='자체 입력정보' tableData={[]} headlist={inputColumn} pageSize={20} total={0} children={undefined} cellClick={undefined} isEditable={true} onPageChange={function (page: number): void {
throw new Error("Function not implemented.");
}}></VerticalTable>
</div>
)
}
export default Smh;
@@ -0,0 +1,95 @@
'use client'
import { VerticalTable } from "@/components/ui/verticalTable";
import { Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components/ui";
import { useEffect, useState } from "react";
import { SwtrmtFcltCol, SwtrmtPrcsZoneCol } from "./components/common/table";
import { useGisContext } from "@common/business_components/ui/map/Gis";
import { getData } from "@/components/lib/api";
import { getLayerContentComponent } from "./components/common";
// TODO:
const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, featureData: any }) => {
const { gisMap } = useGisContext() as unknown as { checkedLayerIds: React.MutableRefObject<number[]>; treeData: { lyrSn: number }[] | null; gisCore: any; gisMap: any; };
const featureId = featureData.getId(); // 피처의 id 가져오기
const [layerNm, setLayerNm] = useState<string>(""); // 레이어 이름 상태
const [tableData, setTableData] = useState<any>(null); // 피처 데이터 상태
const [headlist, setHeadlist] = useState<{ id: string; title: string; }[]>([]); // 컬럼리스트 변경상태
let [buttonChk,setButtonChk] = useState(true)
// 피쳐데이터의 현재 레이어 이름 가져오는 함수
const getSelectedLayers = () => {
const layers = gisMap.getLayers().getArray(); // gisMap에서 모든 레이어 가져오기
let featureLayer: { get: (key: string) => any } = { get: (key: string) => null }; // 피처가 속한 레이어를 찾기 위한 변수
layers.forEach((layer: any) => {
const source = layer.getSource();
if (source && source.hasFeature && source.hasFeature(featureData)) {
featureLayer = layer; // 피처가 속한 레이어 찾기
}
});
return featureLayer.get('lyrNm'); // 레이어 이름 출력
};
useEffect(() => {
const fetchData = async () => {
if (featureId) {
const selectedLayerName = getSelectedLayers(); // 레이어 이름 가져오기
setLayerNm(selectedLayerName); // 상태 업데이트
const data = await getData(`v1/map/features/${featureId}`);
switch (selectedLayerName) {
case '하수처리시설':
setHeadlist(SwtrmtFcltCol);
setButtonChk(false)
break;
case '하수처리구역':
setHeadlist(SwtrmtPrcsZoneCol)
setButtonChk(false)
break;
case '중점우수토실':
setButtonChk(false)
break;
case '우수토실':
setButtonChk(true)
break;
case '하수관로':
setButtonChk(true)
break;
case '하수맨홀':
setButtonChk(true)
break;
default:
break;
}
setTableData(data);
};
}
fetchData();
}, [featureId]);
const ContentComponent = layerNm ? getLayerContentComponent(layerNm) : null;
return (
<div id="popup" className="absolute top-0 right-0 w-[350px] h-[calc(100vh-10rem)] bg-white border-r border-gray-300 flex flex-col overflow-hidden transition-all duration-300 z-[200]">
<div className="card-header bg-primary flex items-center justify-between p-4">
<div className="flex mx-3 flex-col">
<h3 className="text-2xl font-medium text-white text-label">{layerNm} 정보</h3>
<div className="font-medium text-white text-label">(00구 00리 하수관로)</div>
</div>
<button
onClick={onClose}
className="text-white hover:text-gray-700 focus:outline-none"
aria-label="닫기"
>
✕
</button>
</div>
<div className="overflow-y-auto flex-1">
<VerticalTable headName='LX 연계정보' tableData={[tableData]} headlist={headlist} pageSize={20} total={0} children={undefined} cellClick={undefined} onPageChange={function (page: number): void {
throw new Error("Function not implemented.");
}}></VerticalTable>
<div className="w-full mx-5 flex items-center justify-end space-x-2">
{buttonChk ? <Button className="px-4 mr-5 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700">
저장
</Button> : null}
</div>
{ContentComponent && <ContentComponent />}
</div>
</div>
)
}
export default FeatureClickEventPopup;
+36
View File
@@ -0,0 +1,36 @@
'use client'
import Gis from '@common/business_components/ui/map/Gis';
import { SearchDiv, SelectBox, UiTable, MainDiv, Title, ContentsDiv, SearchFormLeft, SearchFormRight } from '@common/business_components';
import ListDiv from '@common/business_components/ui/list_div';
import { Button, Input } from '@common/components';
import { useEffect, useState } from "react";
import { mapService } from "@service";
const Map = () => {
const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정
const [selectedFeature,setSelectedFeature] = useState(null); // 선택된 피처 상태
const fetchData = async () => {
try {
const response = await mapService.getLyrStngs();
setTreeData(response.data); // API 응답 저장
} catch (error) {
console.error("데이터 불러오기 실패:", error);
}
};
// 컴포넌트 마운트 시 1회 실행
useEffect(() => {
fetchData();
}, []);
return (
<div className="bg-container">
<Gis treeData={treeData} isSearch={true} />
</div>
);
};
export default Map;
+58
View File
@@ -0,0 +1,58 @@
'use client'
import { SearchDiv, SelectBox, UiTable } from '@common/business_components';
import { Button, TableRow, Input, Label } from '@common/components';
import { TableHead } from '@common/components';
import { useSnStore } from '../../store';
import type { TableUpperProps } from '../../store/table';
const headMakeColSpan = (upHeadList: TableUpperProps[]) => {
return ( <>
<TableRow>{
upHeadList.map((head: TableUpperProps) =>
(!head.upSequnce?<TableHead rowSpan={2}key={head.id}>{head.title}</TableHead>:
head.upName === 'upChangeRe'&& head.upSequnce=== 1?<TableHead key={head.id} rowSpan={1} colSpan={3}>{'중축 개축 증축 '}</TableHead>:null))}
</TableRow>
<TableRow>
{upHeadList.map((head: TableUpperProps) =>
(head.upName === 'upChangeRe'?<TableHead key={head.id}>{head.title}</TableHead>:null))}
</TableRow>
</> )
}
export default function PublicReuseFaciltyInsert() {
const { upHeadList, publicReuseFacilityUper} = useSnStore((state) => state);
const nodeList= headMakeColSpan(upHeadList);
const total = 100;
const headName= ()=>{
return(
<p className="pl-3 text-xl">점검 및 진단 현황</p>
)
}
return ( <>
<div>
<p className="pl-3 text-2xl"> 점검 및 진단 조회</p>
<SearchDiv>
<div className='flex'>
<SelectBox label='시설물' selectArray={[{text:'관로', val:'1'}]} selectClass='w-24' className='mr-4' onSelectValue={()=>{}} />
<SelectBox label='사용여부' selectArray={[{text:'사용', val:'1'}]} selectClass='w-24' className='mr-4' onSelectValue={()=>{}} />
<SelectBox label='경과년도' selectArray={[{text:'10년', val:'1'}]} selectClass='w-24' className='mr-4' onSelectValue={()=>{}} />
<SelectBox label='경사' selectArray={[{text:'', val:'1'}]} selectClass='w-24' className='mr-4' onSelectValue={()=>{}} />
<SelectBox label='흐름방식' selectArray={[{text:'자연유하', val:'1'}]} selectClass='w-24' className='mr-4' onSelectValue={()=>{}} />
<SelectBox label='용도' selectArray={[{text:'오수관', val:'1'}]} selectClass='w-24' className='mr-4' onSelectValue={()=>{}} />
<SelectBox label='관종' selectArray={[{text:'PE관관', val:'1'}]} selectClass='w-24' className='mr-4' onSelectValue={()=>{}} />
<SelectBox label='관경' selectArray={[{text:'50', val:'1'}]} selectClass='w-24' className='mr-4' onSelectValue={()=>{}} />
<Button className='mr-4' size='sm'>조회</Button>
</div>
</SearchDiv>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="colsapn-1">
</div>
</div>
</> )
}
+70
View File
@@ -0,0 +1,70 @@
'use client'
import { ContentsDiv, MainDiv, SearchDiv, SearchFormLeft, SearchFormRight, SearchInput, Title, UiTable } from '@common/business_components/ui'
import { Button, DatePicker, Input, Label } from '@common/components/ui'
import React, { useRef, useState } from 'react'
import FileUploadForm from '../../components/ui/fileUploadForm'
import ListDiv from '@common/business_components/ui/list_div'
import ContentLayout from '@/components/ui/contentLayout'
import { tableData, tableHeadlist } from '../oldPp/page'
const Page = () => {
const [selectedDate, setSelectedDate] = useState(new Date())
const [company, setCompany] = useState('')
const [pjNm, setpjNm] = useState('')
return (
<ContentLayout title='도시침수 예방사업'>
<SearchDiv>
<SearchFormLeft>
<div className='flex items-center gap-4'>
<div className='flex items-center gap-2'>
<Label className="w-20 text-xl text-gray-600">등록기간</Label>
<DatePicker mode={'single'}
selected={selectedDate}
onSelect={(date: Date | undefined) => { if (date) setSelectedDate(date) }} />
<span>~</span>
<DatePicker mode={'single'}
selected={selectedDate}
onSelect={(date: Date | undefined) => { if (date) setSelectedDate(date) }} />
</div>
<div className='flex items-center'>
<Label className="w-20 text-xl text-gray-600">등록자</Label>
<Input
className='w-[100px]'
value={company}
onChange={(e) => setCompany(e.target.value)}
/>
</div>
<div className='flex items-center'>
<Label className="w-20 text-xl text-gray-600">파일명</Label>
<Input
className='w-[100px]'
value={pjNm}
onChange={(e) => setpjNm(e.target.value)}
/>
</div>
</div>
</SearchFormLeft>
<SearchFormRight>
<Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size="sm">조회</Button>
</SearchFormRight>
</SearchDiv>
<ListDiv className="mt-4">
<div className="card-header flex items-center justify-between">
<h3 className="text-2xl font-medium">사업정보 등록현황</h3>
<div>
<Button className="px-4 py-2 !mr-[1.2rem] bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size="sm" >파일 다운로드</Button>
<Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size="sm" >삭제</Button>
</div>
</div>
<UiTable headName='' tableData={tableData} headlist={tableHeadlist} pageSize={10} total={20} children={undefined} cellClick={() => { }} onPageChange={function (page: number): void {
throw new Error('Function not implemented.')
}}></UiTable>
</ListDiv>
<ListDiv className="mt-4">
<FileUploadForm title='도시침수 예방사업 사업추진현황 등록' />
</ListDiv>
</ContentLayout>
)
}
export default Page
@@ -0,0 +1,9 @@
import React from 'react'
const ExcelDown = () => {
return (
<div>excelDown</div>
)
}
export default ExcelDown
+90
View File
@@ -0,0 +1,90 @@
'use client'
import { ContentsDiv, MainDiv, SearchDiv, SearchFormLeft, SearchFormRight, SelectBox, Title, UiTable } from '@common/business_components/ui'
import { Button, DatePicker, Input, Label } from '@common/components/ui'
import React, { useState } from 'react'
import FileUploadForm from '../../components/ui/fileUploadForm'
import ListDiv from '@common/business_components/ui/list_div'
import ContentLayout from '@/components/ui/contentLayout'
// TODO: 임시 id
export const tableHeadlist = [
{ id: 'no', title: 'NO' },
{ id: 'fileNm', title: '파일명' },
{ id: 'manageYear', title: '년도' },
{ id: 'bungi', title: '분기' },
{ id: 'registDt', title: '등록 일자' },
{ id: 'registUsr', title: '등록자' }
]
export const tableData = [
{ 'no': '1', 'fileNm': '파일명', 'manageYear': '2023', 'registDt': '2025-04-16', 'registUsr': 'me', 'bungi': '1분기' },
{ 'no': '2', 'fileNm': '파일명', 'manageYear': '2024', 'registDt': '2025-04-16', 'registUsr': 'me2', 'bungi': '1분기' },
{ 'no': '3', 'fileNm': '파일명', 'manageYear': '2025', 'registDt': '2025-04-16', 'registUsr': 'me3', 'bungi': '1분기' },
{ 'no': '4', 'fileNm': '파일명', 'manageYear': '2025', 'registDt': '2025-04-16', 'registUsr': 'me3', 'bungi': '1분기' },
{ 'no': '5', 'fileNm': '파일명', 'manageYear': '2025', 'registDt': '2025-04-16', 'registUsr': 'me3', 'bungi': '1분기' },
{ 'no': '6', 'fileNm': '파일명', 'manageYear': '2025', 'registDt': '2025-04-16', 'registUsr': 'me3', 'bungi': '1분기' },
{ 'no': '7', 'fileNm': '파일명', 'manageYear': '2025', 'registDt': '2025-04-16', 'registUsr': 'me3', 'bungi': '1분기' },
{ 'no': '8', 'fileNm': '파일명', 'manageYear': '2025', 'registDt': '2025-04-16', 'registUsr': 'me3', 'bungi': '1분기' },
{ 'no': '9', 'fileNm': '파일명', 'manageYear': '2025', 'registDt': '2025-04-16', 'registUsr': 'me3', 'bungi': '1분기' },
{ 'no': '10', 'fileNm': '파일명', 'manageYear': '2025', 'registDt': '2025-04-16', 'registUsr': 'me3', 'bungi': '1분기' }
]
const Page = () => {
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
const [company, setCompany] = useState('')
const [pjNm, setpjNm] = useState('')
return (
<ContentLayout title='노후 하수관로 정비사업'>
<SearchDiv>
<SearchFormLeft>
<div className='flex items-center gap-4'>
<div className='flex items-center space-x-2'>
<Label className="w-20 text-xl text-gray-600">등록기간</Label>
<DatePicker mode={'single'}
selected={selectedDate}
onSelect={(date: Date | undefined) => { if (date) setSelectedDate(date) }} />
<span>~</span>
<DatePicker mode={'single'}
selected={selectedDate}
onSelect={(date: Date | undefined) => { if (date) setSelectedDate(date) }} />
<div className='flex items-center'>
<Label className="w-20 text-xl text-gray-600">등록자</Label>
<Input
className='w-[100px]'
value={company}
onChange={(e) => setCompany(e.target.value)}
/>
</div>
<div className='flex items-center'>
<Label className="w-20 text-xl text-gray-600">파일명</Label>
<Input
className='w-[100px]'
value={pjNm}
onChange={(e) => setpjNm(e.target.value)}
/>
</div>
</div>
</div>
</SearchFormLeft>
<SearchFormRight>
<Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 mr-2" size="sm" >조회</Button>
</SearchFormRight>
</SearchDiv>
<ListDiv className="mt-4">
<div className="card-header flex items-center justify-between">
<h3 className="text-2xl font-medium">사업정보 등록현황</h3>
<div>
<Button className="px-4 py-2 !mr-[1.2rem] bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size="sm" >파일 다운로드</Button>
<Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size="sm" >삭제</Button>
</div>
</div>
<UiTable headName='' tableData={tableData} headlist={tableHeadlist} pageSize={10} total={20} children={undefined} cellClick={() => { }} onPageChange={function (page: number): void {
throw new Error('Function not implemented.')
}}></UiTable>
</ListDiv>
<ListDiv className="mt-4">
<FileUploadForm title='노후 하수관로 정비사업 주진현황 등록' />
</ListDiv>
</ContentLayout>
)
}
export default Page
+65
View File
@@ -0,0 +1,65 @@
'use client'
import "@common/assets/styles/grid.css";
import { useEffect, useState } from "react";
import { SearchDiv, SelectBox, UiTable, MainDiv, Title, ContentsDiv, SearchFormLeft, SearchFormRight } from '@common/business_components';
import ListDiv from '@common/business_components/ui/list_div';
import { Button, Input } from '@common/components';
import { useRouter } from "next/navigation";
import { pplinesService } from "@service";
const tableHeadlist = [
{ id: "ftreSmbol", title: "지형지물부호"},
{ id: "swageLnkgPplPk", title: "하수연결관로기본키"},
{ id: "pbadmsEmd", title: "행정읍면동"},
{ id: "sttyEmd", title: "법정읍면동"}
];
export default function Pplines() {
const router = useRouter()
const total = 20
const pageSize = 20
const [data, setData] = useState<{ content: any[] }>({ content: [] }); // 초기값을 객체 또는 null로 설정
const fetchData = async () => {
try {
const response = await pplinesService.getPplines();
setData(response.data); // API 응답 저장
} catch (error) {
console.error("데이터 불러오기 실패:", error);
}
};
// 컴포넌트 마운트 시 1회 실행
useEffect(() => {
fetchData();
}, []);
return (
<MainDiv>
<Title title="관로시설" />
<ContentsDiv>
<SearchDiv>
<SearchFormLeft>
<SelectBox label='시설물' selectArray={[{text:'관로', val:'1'}]} className='col-span-3' onSelectValue={()=>{}} selectClass='w-24' />
<SelectBox label='사용여부' selectArray={[{text:'사용', val:'1'}]} className='col-span-3' onSelectValue={()=>{}} selectClass='w-24' />
<SelectBox label='경과년도' selectArray={[{text:'10년', val:'1'}]} className='col-span-3' onSelectValue={()=>{}} selectClass='w-24' />
<SelectBox label='경사' selectArray={[{text:'', val:'1'}]} className='col-span-3' onSelectValue={()=>{}} selectClass='w-24' />
<SelectBox label='흐름방식' selectArray={[{text:'자연유하', val:'1'}]} className='col-span-3' onSelectValue={()=>{}} selectClass='w-24' />
<SelectBox label='용도' selectArray={[{text:'오수관', val:'1'}]} className='col-span-3' onSelectValue={()=>{}} selectClass='w-24' />
<SelectBox label='관종' selectArray={[{text:'PE관관', val:'1'}]} className='col-span-3' onSelectValue={()=>{}} selectClass='w-24' />
<SelectBox label='관경' selectArray={[{text:'50', val:'1'}]} className='col-span-3' onSelectValue={()=>{}} selectClass='w-24' />
</SearchFormLeft>
<SearchFormRight>
<Button className="px-4 py2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size="sm" onClick={fetchData}>조회</Button>
</SearchFormRight>
</SearchDiv>
<ListDiv>
<UiTable tableData={data.content} headlist={tableHeadlist} pageSize={pageSize} total={total} children={null} headName="현황정보" cellClick={()=>{}} />
</ListDiv>
</ContentsDiv>
</MainDiv> );
}
+126
View File
@@ -0,0 +1,126 @@
'use client'
import { SearchDiv, SelectBox, UiTable } from '@common/business_components';
import { Button } from '@common/components';
import { useSnStore } from '@store';
import { type TableUpperProps } from '@/store/table';
import { StoreZus } from '@/store/store';
import { useRouter } from "next/navigation";
const upHeadList: TableUpperProps[] = [
{ id: "sido", title: "자산ID111"},
{ id: "sigungo", title: "설비시설"},
{ id: "facilityName", title: "주소"},
{ id: "location", title: "자산코드"},
{ id: "locationgubun", title: "자산명"},
{ id: "facilityCapacity", title: "용도"},
{ id: "yn", title: '신설/교체'},
{ id: "authoDay", title: '처리구역'},
{ id: "area", title: '내용연수' },
{ id: "reuse", title: '취득금액' },
]
const useAStore = StoreZus();
export default function PublicReuseFaciltyInsert() {
//const upHeadList = useSnStore((state) => state.upHeadList)
//const publicReuseFacilityUper = useSnStore((state) => state.publicReuseFacilityUper)
//const { upHeadList, publicReuseFacilityUper, setHeadList, setTableData, decrementList, addRow } = useAStore.getState();
const upHeadList = useSnStore(useAStore => useAStore.upHeadList);
const publicReuseFacilityUper = useSnStore(useAStore => useAStore.publicReuseFacilityUper);
const setHeadList = useSnStore(useAStore => useAStore.setHeadList);
const setTableData = useSnStore(useAStore => useAStore.setTableData);
const addRow = useSnStore(useAStore => useAStore.addRow);
const decrementList = useSnStore(useAStore => useAStore.decrementList);
//const { upHeadList, publicReuseFacilityUper} = useSnStore((state) => state)
//const setHeadList = useSnStore((state) => state.setHeadList)
//const setTableData = useSnStore((state) => state.setTableData)
//const decrementList = useSnStore((state) => state.decrementList)
const handleChangeHeader = () => {
setHeadList(upHeadList)
}
const add = () => {
console.log(11111)
addRow({
sido: "xxxxxxxxxx",
sigungo: "bbbbbbbbb",
facilityName: "newFacility",
location: "S-T-S-000000",
locationgubun: "000000",
facilityCapacity: "오수관",
});
};
const searchDataList = () => {
//if (newFacility.trim() === "") return;
setTableData([ {
sido: "123456",
sigungo: "관로1",
facilityName: "newFacility",
location: "S-T-S-000000",
locationgubun: "000000",
facilityCapacity: "오수관",
}]);
};
const total = 100;
const headName= ()=>{
return(
<p className="pl-3 text-xl">현황정보</p>
)
}
const router = useRouter();
return (
<div>
<p className="pl-3 text-2xl"> 현황정보 조회</p>
<SearchDiv >
<div className='grid grid-cols-12 gap-1 w-4/5'>
<SelectBox label='시설물' selectArray={[{text:'관로', val:'1'}]} className='col-span-3' onSelectValue={(val: string)=>{}} selectClass='w-24' />
<SelectBox label='사용여부' selectArray={[{text:'사용', val:'1'}]} className='col-span-3' onSelectValue={(val: string)=>{}} selectClass='w-24' />
<SelectBox label='경과년도' selectArray={[{text:'10년', val:'1'}]} className='col-span-3' onSelectValue={(val: string)=>{}} selectClass='w-24' />
<SelectBox label='경사' selectArray={[{text:'', val:'1'}]} className='col-span-3' onSelectValue={(val: string)=>{}} selectClass='w-24' />
<SelectBox label='흐름방식' selectArray={[{text:'자연유하', val:'1'}]} className='col-span-3' onSelectValue={(val: string)=>{}} selectClass='w-24' />
<SelectBox label='용도' selectArray={[{text:'오수관', val:'1'}]} className='col-span-3' onSelectValue={(val: string)=>{}} selectClass='w-24' />
<SelectBox label='관종' selectArray={[{text:'PE관관', val:'1'}]} className='col-span-3' onSelectValue={(val: string)=>{}} selectClass='w-24' />
{/* <SelectBox label='관경' selectArray={[{text:'50', val:'1'}]} className='col-span-3'/> */}
</div>
<div className='w-1/5 flex justify-end items-center'>
<Button className='mr-10 rounded-lg'>조회</Button>
</div>
</SearchDiv>
{/* </div> */}
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2">
</div>
</div>
<button onClick={searchDataList} className="mr-3 mt-4 bg-red-500 text-white px-4 py-2 rounded">
조회
</button>
<button onClick={handleChangeHeader} className="mr-3 mt-4 bg-red-500 text-white px-4 py-2 rounded">
헤더
</button>
<button onClick={decrementList} className="mr-3 mt-4 bg-red-500 text-white px-4 py-2 rounded">
삭제
</button>
<button onClick={() => router.push("/oper/pplines")} className="mr-3 mt-4 bg-red-500 text-white px-4 py-2 rounded">
이동
</button>
</div> );
}
+28
View File
@@ -0,0 +1,28 @@
'use client'
import Link from "next/link";
import { Button } from "@common/components/ui";
export default function Home() {
return (
<div>
<div><h3 className="text-2xl text-bold gap-4 mx-4">하수관망도</h3>
<div className="flex flex-row gap-4 p-4">
<Link href={"/map"}><Button>하수관망도</Button></Link>
</div>
</div>
<div><h3 className="text-2xl text-bold gap-4 mx-4">운영정보 관리</h3>
<div className="flex flex-row gap-4 p-4">
<Link href={"/oper/floodPre"}><Button>도시침수 예방사업</Button></Link>
<Link href={"/oper/oldPp"}><Button>노후 하수관로 정비사업</Button></Link>
</div>
</div>
<div><h3 className="text-2xl text-bold gap-4 mx-4">I/I 분석(기초정보 설정)</h3>
<div className="flex flex-row gap-4 p-4">
<Link href={"/ii/irsetting"}><Button>침입수 분석</Button></Link>
<Link href={"/ii/rdii"}><Button>RDII 분석</Button></Link>
<Link href={"/ii/result"}><Button>분석결과</Button></Link>
</div>
</div>
</div>
);
}
+2
View File
@@ -0,0 +1,2 @@
export * from './ppline'
export * from './map'
+37
View File
@@ -0,0 +1,37 @@
import axios from 'axios'
/**
* 관망도 API
*/
const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}/v1/map`
export type LyrStngProps = {
lyr_sn: string
lyr_nm: string
upper_lyr_sn: string
drawOrdr: string
flterSql: string
gobjTy: string
lyrTpNm: string
useYn: string
minViewSize: string
iconNm: string
lblAtrbNm: string
fontNm: string
fontIntnColor: string
fontExtnColor: string
fontExtnWdth: string
lnWdth: string
lnColor: string
polgnColor: string
initEprssYn: string
clickPsblYn: string
}
export const mapService = {
//레이어 설정 조회
getLyrStngs: () => {
return axios.get(`${API_URL}/lyrStngs`)
}
}
+39
View File
@@ -0,0 +1,39 @@
import axios from 'axios'
/**
* 하수관로 API
*/
const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}/v1/pplines`
const PAGE_NO = 1
const PAGE_SIZE = 20
export type PplineProps = {
ftreSmbol: string
swageLnkgPplPk: string
pbadmsEmd: string
sttyEmd: string
}
export const pplinesService = {
//목록
getPplines: (size: number = PAGE_SIZE, page: number = PAGE_NO) => {
return axios.get(`${API_URL}`)
},
//상세
getPpline: (swageLnkgPplPk: string) => {
return axios.get(`${API_URL}/${swageLnkgPplPk}`)
},
//저장
savePpline: (swageLnkgPplPk: string) => {
return axios.post(`${API_URL}`)
},
//수정
updatePpline: (swageLnkgPplPk: string) => {
return axios.put(`${API_URL}/${swageLnkgPplPk}`)
},
//삭제
removePpline: (swageLnkgPplPk: string) => {
return axios.delete(`${API_URL}/${swageLnkgPplPk}`)
}
}
+45
View File
@@ -0,0 +1,45 @@
'use client'
import { type ReactNode, createContext, useContext, useRef } from 'react'
import { useStore } from 'zustand'
import { type SnStore, StoreZus } from './store'
export type StoreApi = ReturnType<typeof StoreZus>
export const StoreContext = createContext<StoreApi | undefined>(
undefined,
)
export interface StoreProviderProps {
children: ReactNode
}
export const StoreProvider = ({
children,
}: StoreProviderProps) => {
const storeRef = useRef<StoreApi | null>(null)
if (!storeRef.current) {
storeRef.current = StoreZus()
}
return (
<StoreContext.Provider value={storeRef.current}>
{children}
</StoreContext.Provider>
)
}
export const useSnStore = <T,>(
//selector: (store: ReturnType<typeof StoreZus>) => T,
selector: (store: SnStore) => T,
): T => {
const storeContext = useContext(StoreContext)
if (!storeContext) {
throw new Error(`useStore must be used within StoreProvider`)
}
return useStore(storeContext, selector)
}
+76
View File
@@ -0,0 +1,76 @@
export type TableUpperProps = {
id: string;
title: string;
upName?: string;
upSequnce?: number;
}
type PublicReuseFacility = {
sido: string,
sigungo: string,
facilityName: string,
location: string,
locationgubun: string,
facilityCapacity: string,
yn?: string,
authoDay?: string,
area?: string,
reuse?: string
}
const upHeadList:TableUpperProps[] = [
{ id: "sido", title: "자산ID"},
{ id: "sigungo", title: "설비시설"},
{ id: "facilityName", title: "주소"},
{ id: "location", title: "자산코드"},
{ id: "locationgubun", title: "자산명"},
{ id: "facilityCapacity", title: "용도"},
{ id: "yn", title: '신설/교체'},
{ id: "authoDay", title: '처리구역'},
{ id: "area", title: '내용연수' },
{ id: "reuse", title: '취득금액' },
];
const publicReuseFacilityUper:PublicReuseFacility[] = [
{
sido: "1048967",
sigungo: "관로",
facilityName: "양평군 양평읍 백안리 25-5전",
location: "S-T-S-2018000675",
locationgubun: "2018000675",
facilityCapacity: "오수관",
yn: "신설",
authoDay: "양평하수처리구역",
area: "30",
reuse: "28,313,220"
}
]
export type tableState = {
isInit: boolean;
upHeadList: TableUpperProps[];
publicReuseFacilityUper: PublicReuseFacility[];
}
type tableActions = {
decrementList: () => void
}
export type tableType = tableState & tableActions;
export const tableStateExport:tableState= {
isInit:false,
upHeadList: upHeadList,
publicReuseFacilityUper: publicReuseFacilityUper,
}
export const tableActionsExport:(set:any)=>tableActions=(set: any) => {
return {
decrementList:()=>set(
(state:tableState) => {
return (
{
upHeadList: state.upHeadList,
publicReuseFacilityUper: state.publicReuseFacilityUper.slice(0, state.publicReuseFacilityUper.length - 1)
})}
)
}
}
+14
View File
@@ -0,0 +1,14 @@
import { createStore } from 'zustand/vanilla';
import { tableActionsExport, tableStateExport, type tableState, type tableType } from './table';
export type SnStore = tableType;
//export type SnStore = ReturnType<typeof StoreZus>["getState"];
export const StoreZus = (initState: tableState = tableStateExport) => {
return createStore<SnStore>()((set) => {
return {
...initState,
...tableActionsExport(set)
}
})
}
//store 등록하는 부분
+116
View File
@@ -0,0 +1,116 @@
export type TableUpperProps = {
id: string,
title: string,
upSequnce?: number,
upName?: string
}
type PublicReuseFacility = {
sido: string,
sigungo: string,
facilityName: string,
location: string,
locationgubun: string,
facilityCapacity: string,
yn?: string,
authoDay?: string,
area?: string,
reuse?: string
}
const upHeadList:TableUpperProps[] = [
{ id: "sido", title: "자산ID"},
{ id: "sigungo", title: "설비시설"},
{ id: "facilityName", title: "주소"},
{ id: "location", title: "자산코드"},
{ id: "locationgubun", title: "자산명"},
{ id: "facilityCapacity", title: "용도"},
{ id: "yn", title: '신설/교체'},
{ id: "authoDay", title: '처리구역'},
{ id: "area", title: '내용연수' },
{ id: "reuse", title: '취득금액' },
];
const publicReuseFacilityUper:PublicReuseFacility[] = [
{
sido: "1048967",
sigungo: "관로",
facilityName: "양평군 양평읍 백안리 25-5전",
location: "S-T-S-2018000675",
locationgubun: "2018000675",
facilityCapacity: "오수관",
yn: "신설",
authoDay: "양평하수처리구역",
area: "30",
reuse: "28,313,220"
}
]
export type tableState = {
isInit: boolean;
upHeadList: TableUpperProps[];
publicReuseFacilityUper: PublicReuseFacility[];
}
type tableActions = {
setHeadList: (newHeadList: TableUpperProps[]) => void
setTableData: (newTableDataList: PublicReuseFacility[]) => void
decrementList: () => void
addRow: (newTableData: PublicReuseFacility) => void
}
export type tableType = tableState & tableActions;
export const tableStateExport:tableState= {
isInit:false,
upHeadList: [
{ id: "sido", title: "자산ID"},
{ id: "sigungo", title: "설비시설"},
{ id: "facilityName", title: "주소"},
{ id: "location", title: "자산코드"},
{ id: "locationgubun", title: "자산명"},
{ id: "facilityCapacity", title: "용도"},
{ id: "yn", title: '신설/교체'},
{ id: "authoDay", title: '처리구역'},
{ id: "area", title: '내용연수' },
{ id: "reuse", title: '취득금액' },
],
publicReuseFacilityUper: publicReuseFacilityUper,
}
export const tableActionsExport:(set:any)=>tableActions=(set: any) => {
return {
// 마지막 데이터 삭제
decrementList: ()=>set(
(state: tableState) => {
return (
{
...state,
upHeadList: state.upHeadList,
publicReuseFacilityUper: state.publicReuseFacilityUper.slice(0, state.publicReuseFacilityUper.length - 1)
})}
),
// 데이터 추가
addRow: (newTableData: PublicReuseFacility)=>set(
(state: tableState) => {
console.log('asdfasdfadsf')
return (
{
...state,
upHeadList: state.upHeadList,
publicReuseFacilityUper: [...state.publicReuseFacilityUper, newTableData]
})}
),
// 테이블 헤더 변경
setHeadList: (newHeadList: TableUpperProps[]) => set(
(state: tableState) => ({
...state,
upHeadList: newHeadList
})
),
// 시설 목록 데이터 변경
setTableData: (newTableData: PublicReuseFacility[]) => set(
(state: tableState) => ({
...state,
publicReuseFacilityUper: newTableData
})
),
}
}
+12
View File
@@ -0,0 +1,12 @@
@import "@common/assets/styles/base.css";
@import "@common/assets/styles/diveicon.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
html,
body {
font-family: "Noto Sans KR", sans-serif;
font-size: 10px;
}
+25
View File
@@ -0,0 +1,25 @@
@tailwind components;
@layer components {
.card {
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
}
.card-border {
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
}
.card-header {
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
}
.card-footer {
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
}
[data-theme="black"] {
.card-border {
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
}
.card-header {
@apply border-b-[#3f4042];
}
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
+16
View File
@@ -0,0 +1,16 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_PORT: string;
readonly VITE_PROXY_HOST: string;
readonly VITE_SERVER_API_URL: string;
readonly VITE_SITE_ID: string;
readonly VITE_CLAIM_NAME: string;
readonly VITE_ACCESS_TOKEN: string;
readonly VITE_REFRESH_TOKEN: string;
readonly VITE_AUTH_USER_ID: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
+16
View File
@@ -0,0 +1,16 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;
+55
View File
@@ -0,0 +1,55 @@
import type { NextConfig } from "next";
import webpack from "webpack";
const geoserverUrl = process.env.NEXT_PUBLIC_GEOSERVER_URL?.replace(/\/$/, '') ?? '';
const nextConfig: NextConfig = {
basePath: "/sn",
typescript: {
ignoreBuildErrors: true, // 이 줄 추가
},
eslint: {
ignoreDuringBuilds: true,
},
webpack: (config) => {
config.plugins.push(
new webpack.DefinePlugin({
"import.meta.env.VITE_PORT": JSON.stringify(
process.env.NEXT_PUBLIC_PORT
),
"import.meta.env.VITE_PROXY_HOST": JSON.stringify(
process.env.NEXT_PUBLIC_PROXY_HOST
),
"import.meta.env.VITE_SERVER_API_URL": JSON.stringify(
process.env.NEXT_PUBLIC_SERVER_API_URL
),
"import.meta.env.VITE_SITE_ID": JSON.stringify(process.env.SITE_ID),
"import.meta.env.VITE_CLAIM_NAME": JSON.stringify(
process.env.NEXT_PUBLIC_CLAIM_NAME
),
"import.meta.env.VITE_ACCESS_TOKEN": JSON.stringify(
process.env.NEXT_PUBLIC_ACCESS_TOKEN
),
"import.meta.env.VITE_REFRESH_TOKEN": JSON.stringify(
process.env.NEXT_PUBLIC_REFRESH_TOKEN
),
"import.meta.env.VITE_AUTH_USER_ID": JSON.stringify(
process.env.NEXT_PUBLIC_AUTH_USER_ID
),
"process.env.SITE_ID": JSON.stringify(process.env.NEXT_PUBLIC_SITE_ID),
})
);
return config;
},
async rewrites() {
return [
{
source: '/geoserver/:hasudo*',
destination: `${geoserverUrl}/:hasudo*`,
},
];
},
};
export default nextConfig;
+50
View File
@@ -0,0 +1,50 @@
{
"name": "sn",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 3002",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@common/assets": "workspace:*",
"@common/business_components": "workspace:*",
"@common/components": "workspace:*",
"@frontend-opensource/use-react-hooks": "^1.1.4",
"@hookform/resolvers": "^4.1.3",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@tanstack/react-query": "^5.67.1",
"@tanstack/react-table": "^8.21.2",
"axios": "^1.8.1",
"chart.js": "^4.4.8",
"dayjs": "^1.11.13",
"js-cookie": "^3.0.5",
"next": "15.1.7",
"react-chartjs-2": "^5.3.0",
"react-hook-form": "^7.54.2",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.24.2",
"zustand": "^5.0.3",
"ol": "^7.3.0",
"ol-ext": "^4.0.11",
"proj4": "^2.9.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@faker-js/faker": "^9.5.0",
"@types/js-cookie": "^3.0.6",
"@types/node": "^22.13.1",
"@types/webpack": "^5.28.5",
"eslint": "^9",
"eslint-config-next": "15.1.7",
"postcss": "^8",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.11",
"steiger": "^0.5.5",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
+8
View File
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 961 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1016 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 791 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1009 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1009 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1017 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Some files were not shown because too many files have changed in this diff Show More