diff --git a/.github/workflows/sn_build.yml b/.github/workflows/sn_build.yml new file mode 100644 index 000000000..51d9f5d98 --- /dev/null +++ b/.github/workflows/sn_build.yml @@ -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 \ No newline at end of file diff --git a/apps/sn/.gitignore b/apps/sn/.gitignore new file mode 100644 index 000000000..364691fc4 --- /dev/null +++ b/apps/sn/.gitignore @@ -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 \ No newline at end of file diff --git a/apps/sn/.prettierrc b/apps/sn/.prettierrc new file mode 100644 index 000000000..b4bfed357 --- /dev/null +++ b/apps/sn/.prettierrc @@ -0,0 +1,3 @@ +{ + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/apps/sn/app/clientLayout.tsx b/apps/sn/app/clientLayout.tsx new file mode 100644 index 000000000..e8fdbe032 --- /dev/null +++ b/apps/sn/app/clientLayout.tsx @@ -0,0 +1,7 @@ +'use client' + +import { BaseLayout } from "@common/components"; + +export function ClientLayout({ children }: { children: React.ReactNode }) { + return {children}; +} \ No newline at end of file diff --git a/apps/sn/app/components/lib/api/index.ts b/apps/sn/app/components/lib/api/index.ts new file mode 100644 index 000000000..a171844a1 --- /dev/null +++ b/apps/sn/app/components/lib/api/index.ts @@ -0,0 +1,61 @@ +const BASE_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ''; + +export const postData = async (url: string, data: object): Promise => { + 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 => { + 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 => { + 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 => { + 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 => { + 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; +}; \ No newline at end of file diff --git a/apps/sn/app/components/ui/contentLayout.tsx b/apps/sn/app/components/ui/contentLayout.tsx new file mode 100644 index 000000000..738e26b16 --- /dev/null +++ b/apps/sn/app/components/ui/contentLayout.tsx @@ -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 ( + + + <ContentsDiv> + {children} + </ContentsDiv> + </MainDiv> + ) +} + +export default ContentLayout \ No newline at end of file diff --git a/apps/sn/app/components/ui/fileUploadForm.tsx b/apps/sn/app/components/ui/fileUploadForm.tsx new file mode 100644 index 000000000..e111d8362 --- /dev/null +++ b/apps/sn/app/components/ui/fileUploadForm.tsx @@ -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 \ No newline at end of file diff --git a/apps/sn/app/components/ui/formSection.tsx b/apps/sn/app/components/ui/formSection.tsx new file mode 100644 index 000000000..28e4618f7 --- /dev/null +++ b/apps/sn/app/components/ui/formSection.tsx @@ -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 \ No newline at end of file diff --git a/apps/sn/app/components/ui/modal.tsx b/apps/sn/app/components/ui/modal.tsx new file mode 100644 index 000000000..bc8085f71 --- /dev/null +++ b/apps/sn/app/components/ui/modal.tsx @@ -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 \ No newline at end of file diff --git a/apps/sn/app/components/ui/verticalTable.tsx b/apps/sn/app/components/ui/verticalTable.tsx new file mode 100644 index 000000000..36451bfbd --- /dev/null +++ b/apps/sn/app/components/ui/verticalTable.tsx @@ -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> + </> + ); +} \ No newline at end of file diff --git a/apps/sn/app/ii/BaseLayout.tsx b/apps/sn/app/ii/BaseLayout.tsx new file mode 100644 index 000000000..db502cbd8 --- /dev/null +++ b/apps/sn/app/ii/BaseLayout.tsx @@ -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 \ No newline at end of file diff --git a/apps/sn/app/ii/irsetting/page.tsx b/apps/sn/app/ii/irsetting/page.tsx new file mode 100644 index 000000000..16b44ea95 --- /dev/null +++ b/apps/sn/app/ii/irsetting/page.tsx @@ -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 \ No newline at end of file diff --git a/apps/sn/app/ii/rdii/page.tsx b/apps/sn/app/ii/rdii/page.tsx new file mode 100644 index 000000000..9a04102e7 --- /dev/null +++ b/apps/sn/app/ii/rdii/page.tsx @@ -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 > + ) +} diff --git a/apps/sn/app/ii/result/page.tsx b/apps/sn/app/ii/result/page.tsx new file mode 100644 index 000000000..f42f2ab29 --- /dev/null +++ b/apps/sn/app/ii/result/page.tsx @@ -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 \ No newline at end of file diff --git a/apps/sn/app/layout.tsx b/apps/sn/app/layout.tsx new file mode 100644 index 000000000..1d3e231f7 --- /dev/null +++ b/apps/sn/app/layout.tsx @@ -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> + ) +} diff --git a/apps/sn/app/map/SearchModal.tsx b/apps/sn/app/map/SearchModal.tsx new file mode 100644 index 000000000..84624e45b --- /dev/null +++ b/apps/sn/app/map/SearchModal.tsx @@ -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); diff --git a/apps/sn/app/map/components/InputSection.tsx b/apps/sn/app/map/components/InputSection.tsx new file mode 100644 index 000000000..6c411e2cf --- /dev/null +++ b/apps/sn/app/map/components/InputSection.tsx @@ -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; \ No newline at end of file diff --git a/apps/sn/app/map/components/common/index.ts b/apps/sn/app/map/components/common/index.ts new file mode 100644 index 000000000..1af83674c --- /dev/null +++ b/apps/sn/app/map/components/common/index.ts @@ -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); +}; \ No newline at end of file diff --git a/apps/sn/app/map/components/common/table.ts b/apps/sn/app/map/components/common/table.ts new file mode 100644 index 000000000..a64e610d3 --- /dev/null +++ b/apps/sn/app/map/components/common/table.ts @@ -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: '하수처리시설명' }, +] diff --git a/apps/sn/app/map/components/popupComponent/Ssc.tsx b/apps/sn/app/map/components/popupComponent/Ssc.tsx new file mode 100644 index 000000000..a6e783ac9 --- /dev/null +++ b/apps/sn/app/map/components/popupComponent/Ssc.tsx @@ -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 \ No newline at end of file diff --git a/apps/sn/app/map/components/popupComponent/SwtrmtFclt.tsx b/apps/sn/app/map/components/popupComponent/SwtrmtFclt.tsx new file mode 100644 index 000000000..b6d785414 --- /dev/null +++ b/apps/sn/app/map/components/popupComponent/SwtrmtFclt.tsx @@ -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 \ No newline at end of file diff --git a/apps/sn/app/map/components/popupComponent/smh.tsx b/apps/sn/app/map/components/popupComponent/smh.tsx new file mode 100644 index 000000000..e693a28ce --- /dev/null +++ b/apps/sn/app/map/components/popupComponent/smh.tsx @@ -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; \ No newline at end of file diff --git a/apps/sn/app/map/featureClickEventPopup.tsx b/apps/sn/app/map/featureClickEventPopup.tsx new file mode 100644 index 000000000..cc63caf76 --- /dev/null +++ b/apps/sn/app/map/featureClickEventPopup.tsx @@ -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; \ No newline at end of file diff --git a/apps/sn/app/map/page.tsx b/apps/sn/app/map/page.tsx new file mode 100644 index 000000000..b547fb7e4 --- /dev/null +++ b/apps/sn/app/map/page.tsx @@ -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; diff --git a/apps/sn/app/oper/diag/page.tsx b/apps/sn/app/oper/diag/page.tsx new file mode 100644 index 000000000..013d8eecc --- /dev/null +++ b/apps/sn/app/oper/diag/page.tsx @@ -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> + </> ) +} + + + diff --git a/apps/sn/app/oper/floodPre/page.tsx b/apps/sn/app/oper/floodPre/page.tsx new file mode 100644 index 000000000..d2c5fb848 --- /dev/null +++ b/apps/sn/app/oper/floodPre/page.tsx @@ -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 \ No newline at end of file diff --git a/apps/sn/app/oper/oldPp/excelDown/excelDown.tsx b/apps/sn/app/oper/oldPp/excelDown/excelDown.tsx new file mode 100644 index 000000000..d1bd5cc70 --- /dev/null +++ b/apps/sn/app/oper/oldPp/excelDown/excelDown.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +const ExcelDown = () => { + return ( + <div>excelDown</div> + ) +} + +export default ExcelDown \ No newline at end of file diff --git a/apps/sn/app/oper/oldPp/page.tsx b/apps/sn/app/oper/oldPp/page.tsx new file mode 100644 index 000000000..3dd169245 --- /dev/null +++ b/apps/sn/app/oper/oldPp/page.tsx @@ -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 \ No newline at end of file diff --git a/apps/sn/app/oper/pplines/page.tsx b/apps/sn/app/oper/pplines/page.tsx new file mode 100644 index 000000000..48621bddc --- /dev/null +++ b/apps/sn/app/oper/pplines/page.tsx @@ -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> ); +} diff --git a/apps/sn/app/oper/soil/page.tsx b/apps/sn/app/oper/soil/page.tsx new file mode 100644 index 000000000..75becb751 --- /dev/null +++ b/apps/sn/app/oper/soil/page.tsx @@ -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> ); +} + + + diff --git a/apps/sn/app/page.tsx b/apps/sn/app/page.tsx new file mode 100644 index 000000000..fbf788346 --- /dev/null +++ b/apps/sn/app/page.tsx @@ -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> + ); +} diff --git a/apps/sn/app/service/index.ts b/apps/sn/app/service/index.ts new file mode 100644 index 000000000..fda21d232 --- /dev/null +++ b/apps/sn/app/service/index.ts @@ -0,0 +1,2 @@ +export * from './ppline' +export * from './map' diff --git a/apps/sn/app/service/map.ts b/apps/sn/app/service/map.ts new file mode 100644 index 000000000..062cfeedb --- /dev/null +++ b/apps/sn/app/service/map.ts @@ -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`) + } +} diff --git a/apps/sn/app/service/ppline.ts b/apps/sn/app/service/ppline.ts new file mode 100644 index 000000000..33e4c9871 --- /dev/null +++ b/apps/sn/app/service/ppline.ts @@ -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}`) + } +} diff --git a/apps/sn/app/store/index.tsx b/apps/sn/app/store/index.tsx new file mode 100644 index 000000000..d917c17f9 --- /dev/null +++ b/apps/sn/app/store/index.tsx @@ -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) +} \ No newline at end of file diff --git a/apps/sn/app/store/oper/index.tsx b/apps/sn/app/store/oper/index.tsx new file mode 100644 index 000000000..7fef98463 --- /dev/null +++ b/apps/sn/app/store/oper/index.tsx @@ -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) + })} + ) + } +} diff --git a/apps/sn/app/store/store.tsx b/apps/sn/app/store/store.tsx new file mode 100644 index 000000000..9b489fabf --- /dev/null +++ b/apps/sn/app/store/store.tsx @@ -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 등록하는 부분 diff --git a/apps/sn/app/store/table.tsx b/apps/sn/app/store/table.tsx new file mode 100644 index 000000000..30d884de4 --- /dev/null +++ b/apps/sn/app/store/table.tsx @@ -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 + }) + ), + } +} diff --git a/apps/sn/app/styles/base.css b/apps/sn/app/styles/base.css new file mode 100644 index 000000000..8453ff9e8 --- /dev/null +++ b/apps/sn/app/styles/base.css @@ -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; +} diff --git a/apps/sn/app/styles/components.css b/apps/sn/app/styles/components.css new file mode 100644 index 000000000..b67efb8f0 --- /dev/null +++ b/apps/sn/app/styles/components.css @@ -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]; + } + } +} diff --git a/apps/sn/components.json b/apps/sn/components.json new file mode 100644 index 000000000..65412202e --- /dev/null +++ b/apps/sn/components.json @@ -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" +} \ No newline at end of file diff --git a/apps/sn/env.d.ts b/apps/sn/env.d.ts new file mode 100644 index 000000000..31ff74044 --- /dev/null +++ b/apps/sn/env.d.ts @@ -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; +} diff --git a/apps/sn/eslint.config.mjs b/apps/sn/eslint.config.mjs new file mode 100644 index 000000000..c85fb67c4 --- /dev/null +++ b/apps/sn/eslint.config.mjs @@ -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; diff --git a/apps/sn/next.config.ts b/apps/sn/next.config.ts new file mode 100644 index 000000000..bc9505fef --- /dev/null +++ b/apps/sn/next.config.ts @@ -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; diff --git a/apps/sn/package.json b/apps/sn/package.json new file mode 100644 index 000000000..29f630648 --- /dev/null +++ b/apps/sn/package.json @@ -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" + } +} diff --git a/apps/sn/postcss.config.mjs b/apps/sn/postcss.config.mjs new file mode 100644 index 000000000..1a69fd2a4 --- /dev/null +++ b/apps/sn/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/apps/sn/public/images/map/layericon/BML_CBAS_PS.png b/apps/sn/public/images/map/layericon/BML_CBAS_PS.png new file mode 100644 index 000000000..5ff4add3e Binary files /dev/null and b/apps/sn/public/images/map/layericon/BML_CBAS_PS.png differ diff --git a/apps/sn/public/images/map/layericon/BML_ELEV_PS.png b/apps/sn/public/images/map/layericon/BML_ELEV_PS.png new file mode 100644 index 000000000..83970a331 Binary files /dev/null and b/apps/sn/public/images/map/layericon/BML_ELEV_PS.png differ diff --git a/apps/sn/public/images/map/layericon/BML_ETCP_PS.png b/apps/sn/public/images/map/layericon/BML_ETCP_PS.png new file mode 100644 index 000000000..df9e4957b Binary files /dev/null and b/apps/sn/public/images/map/layericon/BML_ETCP_PS.png differ diff --git a/apps/sn/public/images/map/layericon/BML_STMS_PS.png b/apps/sn/public/images/map/layericon/BML_STMS_PS.png new file mode 100644 index 000000000..423f2e8fc Binary files /dev/null and b/apps/sn/public/images/map/layericon/BML_STMS_PS.png differ diff --git a/apps/sn/public/images/map/layericon/BML_SYMB_PS.png b/apps/sn/public/images/map/layericon/BML_SYMB_PS.png new file mode 100644 index 000000000..497589bea Binary files /dev/null and b/apps/sn/public/images/map/layericon/BML_SYMB_PS.png differ diff --git a/apps/sn/public/images/map/layericon/RDL_BYST_PS.png b/apps/sn/public/images/map/layericon/RDL_BYST_PS.png new file mode 100644 index 000000000..1e78cba68 Binary files /dev/null and b/apps/sn/public/images/map/layericon/RDL_BYST_PS.png differ diff --git a/apps/sn/public/images/map/layericon/RDL_CROS_PS.png b/apps/sn/public/images/map/layericon/RDL_CROS_PS.png new file mode 100644 index 000000000..57b5f2ca5 Binary files /dev/null and b/apps/sn/public/images/map/layericon/RDL_CROS_PS.png differ diff --git a/apps/sn/public/images/map/layericon/RDL_ETCT_PS.png b/apps/sn/public/images/map/layericon/RDL_ETCT_PS.png new file mode 100644 index 000000000..29c0b77b4 Binary files /dev/null and b/apps/sn/public/images/map/layericon/RDL_ETCT_PS.png differ diff --git a/apps/sn/public/images/map/layericon/RDL_OCUP_PS.png b/apps/sn/public/images/map/layericon/RDL_OCUP_PS.png new file mode 100644 index 000000000..8a1ac0074 Binary files /dev/null and b/apps/sn/public/images/map/layericon/RDL_OCUP_PS.png differ diff --git a/apps/sn/public/images/map/layericon/RDL_RDSN_PS.png b/apps/sn/public/images/map/layericon/RDL_RDSN_PS.png new file mode 100644 index 000000000..5629de9fb Binary files /dev/null and b/apps/sn/public/images/map/layericon/RDL_RDSN_PS.png differ diff --git a/apps/sn/public/images/map/layericon/RDL_SCLT_PS.png b/apps/sn/public/images/map/layericon/RDL_SCLT_PS.png new file mode 100644 index 000000000..7281dc61d Binary files /dev/null and b/apps/sn/public/images/map/layericon/RDL_SCLT_PS.png differ diff --git a/apps/sn/public/images/map/layericon/RDL_SEPC_PS.png b/apps/sn/public/images/map/layericon/RDL_SEPC_PS.png new file mode 100644 index 000000000..087b4e941 Binary files /dev/null and b/apps/sn/public/images/map/layericon/RDL_SEPC_PS.png differ diff --git a/apps/sn/public/images/map/layericon/RDL_STAT_PS.png b/apps/sn/public/images/map/layericon/RDL_STAT_PS.png new file mode 100644 index 000000000..13d8ea689 Binary files /dev/null and b/apps/sn/public/images/map/layericon/RDL_STAT_PS.png differ diff --git a/apps/sn/public/images/map/layericon/RDL_STLT_PS.png b/apps/sn/public/images/map/layericon/RDL_STLT_PS.png new file mode 100644 index 000000000..489524b5c Binary files /dev/null and b/apps/sn/public/images/map/layericon/RDL_STLT_PS.png differ diff --git a/apps/sn/public/images/map/layericon/RDL_TFSN_PS.png b/apps/sn/public/images/map/layericon/RDL_TFSN_PS.png new file mode 100644 index 000000000..6dfff6696 Binary files /dev/null and b/apps/sn/public/images/map/layericon/RDL_TFSN_PS.png differ diff --git a/apps/sn/public/images/map/layericon/RDL_TRCT_PS.png b/apps/sn/public/images/map/layericon/RDL_TRCT_PS.png new file mode 100644 index 000000000..ed1a72d8a Binary files /dev/null and b/apps/sn/public/images/map/layericon/RDL_TRCT_PS.png differ diff --git a/apps/sn/public/images/map/layericon/RDL_TREE_PS.png b/apps/sn/public/images/map/layericon/RDL_TREE_PS.png new file mode 100644 index 000000000..0ee84dedc Binary files /dev/null and b/apps/sn/public/images/map/layericon/RDL_TREE_PS.png differ diff --git a/apps/sn/public/images/map/layericon/RDL_TRSN_PS.png b/apps/sn/public/images/map/layericon/RDL_TRSN_PS.png new file mode 100644 index 000000000..15675cb31 Binary files /dev/null and b/apps/sn/public/images/map/layericon/RDL_TRSN_PS.png differ diff --git a/apps/sn/public/images/map/layericon/RDL_TSCT_PS.png b/apps/sn/public/images/map/layericon/RDL_TSCT_PS.png new file mode 100644 index 000000000..ffc225159 Binary files /dev/null and b/apps/sn/public/images/map/layericon/RDL_TSCT_PS.png differ diff --git a/apps/sn/public/images/map/layericon/SWL_CLAY_PS.png b/apps/sn/public/images/map/layericon/SWL_CLAY_PS.png new file mode 100644 index 000000000..e603ffa9c Binary files /dev/null and b/apps/sn/public/images/map/layericon/SWL_CLAY_PS.png differ diff --git a/apps/sn/public/images/map/layericon/SWL_CLAY_PS_RED.png b/apps/sn/public/images/map/layericon/SWL_CLAY_PS_RED.png new file mode 100644 index 000000000..0e5450688 Binary files /dev/null and b/apps/sn/public/images/map/layericon/SWL_CLAY_PS_RED.png differ diff --git a/apps/sn/public/images/map/layericon/SWL_DEPT_PS.png b/apps/sn/public/images/map/layericon/SWL_DEPT_PS.png new file mode 100644 index 000000000..fede6c304 Binary files /dev/null and b/apps/sn/public/images/map/layericon/SWL_DEPT_PS.png differ diff --git a/apps/sn/public/images/map/layericon/SWL_MANH_PS.png b/apps/sn/public/images/map/layericon/SWL_MANH_PS.png new file mode 100644 index 000000000..75217f494 Binary files /dev/null and b/apps/sn/public/images/map/layericon/SWL_MANH_PS.png differ diff --git a/apps/sn/public/images/map/layericon/SWL_MANH_PS_BLACK.png b/apps/sn/public/images/map/layericon/SWL_MANH_PS_BLACK.png new file mode 100644 index 000000000..5d2e8a70a Binary files /dev/null and b/apps/sn/public/images/map/layericon/SWL_MANH_PS_BLACK.png differ diff --git a/apps/sn/public/images/map/layericon/SWL_MANH_PS_BLUE.png b/apps/sn/public/images/map/layericon/SWL_MANH_PS_BLUE.png new file mode 100644 index 000000000..e521cb84a Binary files /dev/null and b/apps/sn/public/images/map/layericon/SWL_MANH_PS_BLUE.png differ diff --git a/apps/sn/public/images/map/layericon/SWL_MANH_PS_GRAY.png b/apps/sn/public/images/map/layericon/SWL_MANH_PS_GRAY.png new file mode 100644 index 000000000..e19f3d8b3 Binary files /dev/null and b/apps/sn/public/images/map/layericon/SWL_MANH_PS_GRAY.png differ diff --git a/apps/sn/public/images/map/layericon/SWL_MANH_PS_GREEN.png b/apps/sn/public/images/map/layericon/SWL_MANH_PS_GREEN.png new file mode 100644 index 000000000..4503c13c6 Binary files /dev/null and b/apps/sn/public/images/map/layericon/SWL_MANH_PS_GREEN.png differ diff --git a/apps/sn/public/images/map/layericon/SWL_MANH_PS_RED.png b/apps/sn/public/images/map/layericon/SWL_MANH_PS_RED.png new file mode 100644 index 000000000..b8fa45f9c Binary files /dev/null and b/apps/sn/public/images/map/layericon/SWL_MANH_PS_RED.png differ diff --git a/apps/sn/public/images/map/layericon/SWL_MANH_PS_YELLOW.png b/apps/sn/public/images/map/layericon/SWL_MANH_PS_YELLOW.png new file mode 100644 index 000000000..b7de1fbfd Binary files /dev/null and b/apps/sn/public/images/map/layericon/SWL_MANH_PS_YELLOW.png differ diff --git a/apps/sn/public/images/map/layericon/SWL_SPEW_PS.png b/apps/sn/public/images/map/layericon/SWL_SPEW_PS.png new file mode 100644 index 000000000..d260ea3a6 Binary files /dev/null and b/apps/sn/public/images/map/layericon/SWL_SPEW_PS.png differ diff --git a/apps/sn/public/images/map/layericon/SWL_SPOT_PS.png b/apps/sn/public/images/map/layericon/SWL_SPOT_PS.png new file mode 100644 index 000000000..84c7992e4 Binary files /dev/null and b/apps/sn/public/images/map/layericon/SWL_SPOT_PS.png differ diff --git a/apps/sn/public/images/map/layericon/SWL_SPOT_PS_BLACK.png b/apps/sn/public/images/map/layericon/SWL_SPOT_PS_BLACK.png new file mode 100644 index 000000000..fe67f8f98 Binary files /dev/null and b/apps/sn/public/images/map/layericon/SWL_SPOT_PS_BLACK.png differ diff --git a/apps/sn/public/images/map/layericon/SWL_SPOT_PS_BLUE.png b/apps/sn/public/images/map/layericon/SWL_SPOT_PS_BLUE.png new file mode 100644 index 000000000..691f2f45e Binary files /dev/null and b/apps/sn/public/images/map/layericon/SWL_SPOT_PS_BLUE.png differ diff --git a/apps/sn/public/images/map/layericon/SWL_SPOT_PS_GREEN.png b/apps/sn/public/images/map/layericon/SWL_SPOT_PS_GREEN.png new file mode 100644 index 000000000..d09c4bd97 Binary files /dev/null and b/apps/sn/public/images/map/layericon/SWL_SPOT_PS_GREEN.png differ diff --git a/apps/sn/public/images/map/layericon/SWL_SPOT_PS_GREY.png b/apps/sn/public/images/map/layericon/SWL_SPOT_PS_GREY.png new file mode 100644 index 000000000..32d0bf519 Binary files /dev/null and b/apps/sn/public/images/map/layericon/SWL_SPOT_PS_GREY.png differ diff --git a/apps/sn/public/images/map/layericon/SWL_SPOT_PS_RED.png b/apps/sn/public/images/map/layericon/SWL_SPOT_PS_RED.png new file mode 100644 index 000000000..a2da36f9b Binary files /dev/null and b/apps/sn/public/images/map/layericon/SWL_SPOT_PS_RED.png differ diff --git a/apps/sn/public/images/map/layericon/SWL_VENT_PS.png b/apps/sn/public/images/map/layericon/SWL_VENT_PS.png new file mode 100644 index 000000000..834eeec21 Binary files /dev/null and b/apps/sn/public/images/map/layericon/SWL_VENT_PS.png differ diff --git a/apps/sn/public/images/map/layericon/TL_SPBD_ENTRC.png b/apps/sn/public/images/map/layericon/TL_SPBD_ENTRC.png new file mode 100644 index 000000000..8e7de8593 Binary files /dev/null and b/apps/sn/public/images/map/layericon/TL_SPBD_ENTRC.png differ diff --git a/apps/sn/public/images/map/layericon/TL_SPGF_LOC.png b/apps/sn/public/images/map/layericon/TL_SPGF_LOC.png new file mode 100644 index 000000000..74b0adac3 Binary files /dev/null and b/apps/sn/public/images/map/layericon/TL_SPGF_LOC.png differ diff --git a/apps/sn/public/images/map/layericon/TL_SPOT_NOIN.png b/apps/sn/public/images/map/layericon/TL_SPOT_NOIN.png new file mode 100644 index 000000000..4800beaeb Binary files /dev/null and b/apps/sn/public/images/map/layericon/TL_SPOT_NOIN.png differ diff --git a/apps/sn/public/images/map/layericon/TL_SPRD_BANO.png b/apps/sn/public/images/map/layericon/TL_SPRD_BANO.png new file mode 100644 index 000000000..8d08dca29 Binary files /dev/null and b/apps/sn/public/images/map/layericon/TL_SPRD_BANO.png differ diff --git a/apps/sn/public/images/map/layericon/TL_SPRD_CRSRD.png b/apps/sn/public/images/map/layericon/TL_SPRD_CRSRD.png new file mode 100644 index 000000000..57b5f2ca5 Binary files /dev/null and b/apps/sn/public/images/map/layericon/TL_SPRD_CRSRD.png differ diff --git a/apps/sn/public/images/map/layericon/TL_SPRD_NONM.png b/apps/sn/public/images/map/layericon/TL_SPRD_NONM.png new file mode 100644 index 000000000..e68c05ab6 Binary files /dev/null and b/apps/sn/public/images/map/layericon/TL_SPRD_NONM.png differ diff --git a/apps/sn/public/images/map/layericon/TL_SPRD_RENM.png b/apps/sn/public/images/map/layericon/TL_SPRD_RENM.png new file mode 100644 index 000000000..4800beaeb Binary files /dev/null and b/apps/sn/public/images/map/layericon/TL_SPRD_RENM.png differ diff --git a/apps/sn/public/images/map/layericon/TL_SPRD_RWNM.png b/apps/sn/public/images/map/layericon/TL_SPRD_RWNM.png new file mode 100644 index 000000000..e68c05ab6 Binary files /dev/null and b/apps/sn/public/images/map/layericon/TL_SPRD_RWNM.png differ diff --git a/apps/sn/public/images/map/layericon/UFL_BMAN_PS.png b/apps/sn/public/images/map/layericon/UFL_BMAN_PS.png new file mode 100644 index 000000000..f2f64a641 Binary files /dev/null and b/apps/sn/public/images/map/layericon/UFL_BMAN_PS.png differ diff --git a/apps/sn/public/images/map/layericon/UFL_BOPC_PS.png b/apps/sn/public/images/map/layericon/UFL_BOPC_PS.png new file mode 100644 index 000000000..0f8e99f45 Binary files /dev/null and b/apps/sn/public/images/map/layericon/UFL_BOPC_PS.png differ diff --git a/apps/sn/public/images/map/layericon/UFL_BPIP_PS.png b/apps/sn/public/images/map/layericon/UFL_BPIP_PS.png new file mode 100644 index 000000000..dcc3622a7 Binary files /dev/null and b/apps/sn/public/images/map/layericon/UFL_BPIP_PS.png differ diff --git a/apps/sn/public/images/map/layericon/UFL_BPOL_PS.png b/apps/sn/public/images/map/layericon/UFL_BPOL_PS.png new file mode 100644 index 000000000..4f1102989 Binary files /dev/null and b/apps/sn/public/images/map/layericon/UFL_BPOL_PS.png differ diff --git a/apps/sn/public/images/map/layericon/UFL_BTRS_PS.png b/apps/sn/public/images/map/layericon/UFL_BTRS_PS.png new file mode 100644 index 000000000..557c17089 Binary files /dev/null and b/apps/sn/public/images/map/layericon/UFL_BTRS_PS.png differ diff --git a/apps/sn/public/images/map/layericon/UFL_GMNT_PS.png b/apps/sn/public/images/map/layericon/UFL_GMNT_PS.png new file mode 100644 index 000000000..6081d8b00 Binary files /dev/null and b/apps/sn/public/images/map/layericon/UFL_GMNT_PS.png differ diff --git a/apps/sn/public/images/map/layericon/UFL_GPIP_PS.png b/apps/sn/public/images/map/layericon/UFL_GPIP_PS.png new file mode 100644 index 000000000..5d7e86fd2 Binary files /dev/null and b/apps/sn/public/images/map/layericon/UFL_GPIP_PS.png differ diff --git a/apps/sn/public/images/map/layericon/UFL_GPRS_PS.png b/apps/sn/public/images/map/layericon/UFL_GPRS_PS.png new file mode 100644 index 000000000..79c120f20 Binary files /dev/null and b/apps/sn/public/images/map/layericon/UFL_GPRS_PS.png differ diff --git a/apps/sn/public/images/map/layericon/UFL_GVAL_PS.png b/apps/sn/public/images/map/layericon/UFL_GVAL_PS.png new file mode 100644 index 000000000..e8f7cde2e Binary files /dev/null and b/apps/sn/public/images/map/layericon/UFL_GVAL_PS.png differ diff --git a/apps/sn/public/images/map/layericon/UFL_KMAN_PS.png b/apps/sn/public/images/map/layericon/UFL_KMAN_PS.png new file mode 100644 index 000000000..764ce8ba5 Binary files /dev/null and b/apps/sn/public/images/map/layericon/UFL_KMAN_PS.png differ diff --git a/apps/sn/public/images/map/layericon/UFL_KPOL_PS.png b/apps/sn/public/images/map/layericon/UFL_KPOL_PS.png new file mode 100644 index 000000000..d9fa712cf Binary files /dev/null and b/apps/sn/public/images/map/layericon/UFL_KPOL_PS.png differ diff --git a/apps/sn/public/images/map/layericon/UFL_VALV_PS.png b/apps/sn/public/images/map/layericon/UFL_VALV_PS.png new file mode 100644 index 000000000..1b5d70178 Binary files /dev/null and b/apps/sn/public/images/map/layericon/UFL_VALV_PS.png differ diff --git a/apps/sn/public/images/map/layericon/WTL_FIRE_PS.png b/apps/sn/public/images/map/layericon/WTL_FIRE_PS.png new file mode 100644 index 000000000..4137a3f74 Binary files /dev/null and b/apps/sn/public/images/map/layericon/WTL_FIRE_PS.png differ diff --git a/apps/sn/public/images/map/layericon/WTL_FLOW_PS.png b/apps/sn/public/images/map/layericon/WTL_FLOW_PS.png new file mode 100644 index 000000000..285f7edfa Binary files /dev/null and b/apps/sn/public/images/map/layericon/WTL_FLOW_PS.png differ diff --git a/apps/sn/public/images/map/layericon/WTL_MANH_PS.png b/apps/sn/public/images/map/layericon/WTL_MANH_PS.png new file mode 100644 index 000000000..5f2a57cfe Binary files /dev/null and b/apps/sn/public/images/map/layericon/WTL_MANH_PS.png differ diff --git a/apps/sn/public/images/map/layericon/WTL_META_PS.png b/apps/sn/public/images/map/layericon/WTL_META_PS.png new file mode 100644 index 000000000..22c7af31b Binary files /dev/null and b/apps/sn/public/images/map/layericon/WTL_META_PS.png differ diff --git a/apps/sn/public/images/map/layericon/WTL_PIPE_PS.png b/apps/sn/public/images/map/layericon/WTL_PIPE_PS.png new file mode 100644 index 000000000..c9c6863f6 Binary files /dev/null and b/apps/sn/public/images/map/layericon/WTL_PIPE_PS.png differ diff --git a/apps/sn/public/images/map/layericon/WTL_RSRV_PS.png b/apps/sn/public/images/map/layericon/WTL_RSRV_PS.png new file mode 100644 index 000000000..d7b307a61 Binary files /dev/null and b/apps/sn/public/images/map/layericon/WTL_RSRV_PS.png differ diff --git a/apps/sn/public/images/map/layericon/WTL_RSRV_PS_GRAY.png b/apps/sn/public/images/map/layericon/WTL_RSRV_PS_GRAY.png new file mode 100644 index 000000000..33a667a1e Binary files /dev/null and b/apps/sn/public/images/map/layericon/WTL_RSRV_PS_GRAY.png differ diff --git a/apps/sn/public/images/map/layericon/WTL_VALV_PS.png b/apps/sn/public/images/map/layericon/WTL_VALV_PS.png new file mode 100644 index 000000000..ca2b4788b Binary files /dev/null and b/apps/sn/public/images/map/layericon/WTL_VALV_PS.png differ diff --git a/apps/sn/public/images/map/layericon/default.png b/apps/sn/public/images/map/layericon/default.png new file mode 100644 index 000000000..92bf1f587 Binary files /dev/null and b/apps/sn/public/images/map/layericon/default.png differ diff --git a/apps/sn/public/images/map/layericon/reset.png b/apps/sn/public/images/map/layericon/reset.png new file mode 100644 index 000000000..45f889206 Binary files /dev/null and b/apps/sn/public/images/map/layericon/reset.png differ diff --git a/apps/sn/public/images/map/toolbar/arrow_down.png b/apps/sn/public/images/map/toolbar/arrow_down.png new file mode 100644 index 000000000..88590ecaf Binary files /dev/null and b/apps/sn/public/images/map/toolbar/arrow_down.png differ diff --git a/apps/sn/public/images/map/toolbar/arrow_right.png b/apps/sn/public/images/map/toolbar/arrow_right.png new file mode 100644 index 000000000..7090c37dd Binary files /dev/null and b/apps/sn/public/images/map/toolbar/arrow_right.png differ diff --git a/apps/sn/public/images/map/toolbar/backward_icon.png b/apps/sn/public/images/map/toolbar/backward_icon.png new file mode 100644 index 000000000..3a853475e Binary files /dev/null and b/apps/sn/public/images/map/toolbar/backward_icon.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_cont_area.png b/apps/sn/public/images/map/toolbar/btn_cont_area.png new file mode 100644 index 000000000..044562b55 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_cont_area.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_cont_area_on.png b/apps/sn/public/images/map/toolbar/btn_cont_area_on.png new file mode 100644 index 000000000..0129ff1bd Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_cont_area_on.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_cont_next.png b/apps/sn/public/images/map/toolbar/btn_cont_next.png new file mode 100644 index 000000000..075111dcf Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_cont_next.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_cont_next_on.png b/apps/sn/public/images/map/toolbar/btn_cont_next_on.png new file mode 100644 index 000000000..4fdbe3a00 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_cont_next_on.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_cont_prev.png b/apps/sn/public/images/map/toolbar/btn_cont_prev.png new file mode 100644 index 000000000..7e3fae7d2 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_cont_prev.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_cont_prev_on.png b/apps/sn/public/images/map/toolbar/btn_cont_prev_on.png new file mode 100644 index 000000000..fb2d55cfd Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_cont_prev_on.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_cont_print.png b/apps/sn/public/images/map/toolbar/btn_cont_print.png new file mode 100644 index 000000000..82596b83d Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_cont_print.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_cont_print_on.png b/apps/sn/public/images/map/toolbar/btn_cont_print_on.png new file mode 100644 index 000000000..bb7f27334 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_cont_print_on.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_cont_ruler.png b/apps/sn/public/images/map/toolbar/btn_cont_ruler.png new file mode 100644 index 000000000..501e460a4 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_cont_ruler.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_cont_ruler_on.png b/apps/sn/public/images/map/toolbar/btn_cont_ruler_on.png new file mode 100644 index 000000000..676a81bcc Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_cont_ruler_on.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_cont_save.png b/apps/sn/public/images/map/toolbar/btn_cont_save.png new file mode 100644 index 000000000..c057b9377 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_cont_save.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_cont_save_on.png b/apps/sn/public/images/map/toolbar/btn_cont_save_on.png new file mode 100644 index 000000000..bf8e6d623 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_cont_save_on.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_cont_sketch.png b/apps/sn/public/images/map/toolbar/btn_cont_sketch.png new file mode 100644 index 000000000..38a5f507f Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_cont_sketch.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_cont_sketch_on.png b/apps/sn/public/images/map/toolbar/btn_cont_sketch_on.png new file mode 100644 index 000000000..79fb54e03 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_cont_sketch_on.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_cont_zoomin.png b/apps/sn/public/images/map/toolbar/btn_cont_zoomin.png new file mode 100644 index 000000000..ac32fbab0 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_cont_zoomin.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_cont_zoomin_on.png b/apps/sn/public/images/map/toolbar/btn_cont_zoomin_on.png new file mode 100644 index 000000000..f258a965f Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_cont_zoomin_on.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_cont_zoomout.png b/apps/sn/public/images/map/toolbar/btn_cont_zoomout.png new file mode 100644 index 000000000..2ee0b5161 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_cont_zoomout.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_cont_zoomout_on.png b/apps/sn/public/images/map/toolbar/btn_cont_zoomout_on.png new file mode 100644 index 000000000..58dcbfdae Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_cont_zoomout_on.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_edit_add.png b/apps/sn/public/images/map/toolbar/btn_edit_add.png new file mode 100644 index 000000000..fe4f0993f Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_edit_add.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_edit_add_on.png b/apps/sn/public/images/map/toolbar/btn_edit_add_on.png new file mode 100644 index 000000000..37a836298 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_edit_add_on.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_edit_del.png b/apps/sn/public/images/map/toolbar/btn_edit_del.png new file mode 100644 index 000000000..3bb372ab9 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_edit_del.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_edit_del_on.png b/apps/sn/public/images/map/toolbar/btn_edit_del_on.png new file mode 100644 index 000000000..89e558739 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_edit_del_on.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_edit_next.png b/apps/sn/public/images/map/toolbar/btn_edit_next.png new file mode 100644 index 000000000..df4913c90 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_edit_next.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_edit_next_on.png b/apps/sn/public/images/map/toolbar/btn_edit_next_on.png new file mode 100644 index 000000000..4e5540b55 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_edit_next_on.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_edit_prev.png b/apps/sn/public/images/map/toolbar/btn_edit_prev.png new file mode 100644 index 000000000..e94bf6d42 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_edit_prev.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_edit_prev_on.png b/apps/sn/public/images/map/toolbar/btn_edit_prev_on.png new file mode 100644 index 000000000..8645dedfa Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_edit_prev_on.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_edit_save.png b/apps/sn/public/images/map/toolbar/btn_edit_save.png new file mode 100644 index 000000000..5f01dd55c Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_edit_save.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_edit_save_on.png b/apps/sn/public/images/map/toolbar/btn_edit_save_on.png new file mode 100644 index 000000000..b035687d6 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_edit_save_on.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_edit_slt.png b/apps/sn/public/images/map/toolbar/btn_edit_slt.png new file mode 100644 index 000000000..aaf2bbd08 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_edit_slt.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_edit_slt_on.png b/apps/sn/public/images/map/toolbar/btn_edit_slt_on.png new file mode 100644 index 000000000..02d035a74 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_edit_slt_on.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_edit_snap.png b/apps/sn/public/images/map/toolbar/btn_edit_snap.png new file mode 100644 index 000000000..fc91d4304 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_edit_snap.png differ diff --git a/apps/sn/public/images/map/toolbar/btn_edit_snap_on.png b/apps/sn/public/images/map/toolbar/btn_edit_snap_on.png new file mode 100644 index 000000000..f856d8fbe Binary files /dev/null and b/apps/sn/public/images/map/toolbar/btn_edit_snap_on.png differ diff --git a/apps/sn/public/images/map/toolbar/close_icon.png b/apps/sn/public/images/map/toolbar/close_icon.png new file mode 100644 index 000000000..e8e76d5d7 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/close_icon.png differ diff --git a/apps/sn/public/images/map/toolbar/ico_gis_sprite.png b/apps/sn/public/images/map/toolbar/ico_gis_sprite.png new file mode 100644 index 000000000..0ee4b5985 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/ico_gis_sprite.png differ diff --git a/apps/sn/public/images/map/toolbar/icon-viewclose.png b/apps/sn/public/images/map/toolbar/icon-viewclose.png new file mode 100644 index 000000000..d93ce9c05 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/icon-viewclose.png differ diff --git a/apps/sn/public/images/map/toolbar/img_base_map.png b/apps/sn/public/images/map/toolbar/img_base_map.png new file mode 100644 index 000000000..7f96fcf5e Binary files /dev/null and b/apps/sn/public/images/map/toolbar/img_base_map.png differ diff --git a/apps/sn/public/images/map/toolbar/img_network_map.png b/apps/sn/public/images/map/toolbar/img_network_map.png new file mode 100644 index 000000000..df80af6dc Binary files /dev/null and b/apps/sn/public/images/map/toolbar/img_network_map.png differ diff --git a/apps/sn/public/images/map/toolbar/img_road_map.png b/apps/sn/public/images/map/toolbar/img_road_map.png new file mode 100644 index 000000000..02d015d12 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/img_road_map.png differ diff --git a/apps/sn/public/images/map/toolbar/img_settle_map.png b/apps/sn/public/images/map/toolbar/img_settle_map.png new file mode 100644 index 000000000..1a93f50e8 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/img_settle_map.png differ diff --git a/apps/sn/public/images/map/toolbar/loading.gif b/apps/sn/public/images/map/toolbar/loading.gif new file mode 100644 index 000000000..7c3683bb4 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/loading.gif differ diff --git a/apps/sn/public/images/map/toolbar/map_pin.png b/apps/sn/public/images/map/toolbar/map_pin.png new file mode 100644 index 000000000..b6cc2f6aa Binary files /dev/null and b/apps/sn/public/images/map/toolbar/map_pin.png differ diff --git a/apps/sn/public/images/map/toolbar/toggle_off.png b/apps/sn/public/images/map/toolbar/toggle_off.png new file mode 100644 index 000000000..d5dea9f67 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/toggle_off.png differ diff --git a/apps/sn/public/images/map/toolbar/toggle_on.png b/apps/sn/public/images/map/toolbar/toggle_on.png new file mode 100644 index 000000000..9cb573875 Binary files /dev/null and b/apps/sn/public/images/map/toolbar/toggle_on.png differ diff --git a/apps/sn/tailwind.config.ts b/apps/sn/tailwind.config.ts new file mode 100644 index 000000000..5a1819bab --- /dev/null +++ b/apps/sn/tailwind.config.ts @@ -0,0 +1,19 @@ +import type { Config } from "tailwindcss"; +import presetConfig from "../../tailwind.config.preset"; + +export default { + presets: [presetConfig], + darkMode: ["class", "[data-mode='dark']"], + content: [ + "./app/**/*.{js,ts,jsx,tsx,mdx}", + "../../packages/common/components/src/ui/**/*.{js,ts,jsx,tsx,mdx}", + "../../packages/common/business_components/src/ui/**/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + boxShadow: { + blur: "0 0 10px -1px rgba(0, 0, 0, 0.06)", + }, + }, + }, +} satisfies Config; diff --git a/apps/sn/tsconfig.json b/apps/sn/tsconfig.json new file mode 100644 index 000000000..74251f81b --- /dev/null +++ b/apps/sn/tsconfig.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "verbatimModuleSyntax": true, + "strictNullChecks": true, + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "baseUrl": "app", + "paths": { + "@/*": ["./*"], + "@common/components/*": ["../../../packages/common/components/src/*"], + "@common/components": ["../../../packages/common/components/src"], + "@common/business_components/*": ["../../../packages/common/business_components/src/*"], + "@common/business_components": ["../../../packages/common/business_components/src"], + "@common/network/*": ["../../../packages/common/network/*"], + "@common/network": ["../../../packages/common/network/"], + "@store": ["./store"], + "@service": ["./service"] + } + }, + "include": [ + ".eslintrc.*", + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + "**/*.mts", + "**/*.js", + "**/*.cjs", + "**/*.mjs", + "**/*.jsx", + "**/*.json", + ".next/types/**/*.ts" + ], + "exclude": ["**/node_modules", "**/.*/*"] +} diff --git a/package.json b/package.json index 159f30a50..ae28525c6 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "build": "turbo run build --filter=!wr", "dev": "turbo run dev", "dev:cs": "turbo run dev --filter=cs", + "dev:sn": "turbo run dev --filter=sn", "dev:wr": "turbo run dev --filter=wr", "preview:components": "turbo run storybook --filter=@common/components", "preinstall": "npx only-allow pnpm" diff --git a/packages/common/business_components/package.json b/packages/common/business_components/package.json index a23f31058..e7aae9fc5 100644 --- a/packages/common/business_components/package.json +++ b/packages/common/business_components/package.json @@ -31,6 +31,7 @@ "@radix-ui/react-scroll-area": "^1.2.3", "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-slot": "^1.1.2", + "axios": "1.8.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^3.6.0", diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js new file mode 100644 index 000000000..5cfff011d --- /dev/null +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -0,0 +1,172 @@ +import './lib/css/gis.common.css'; +import React, { createContext, useContext, useEffect, useMemo, useRef, useState, useCallback } from 'react'; +import { Tabs, TabsList, TabsTrigger, TabsContent } from '@common/components/ui/tabs'; +import OlMap from 'ol/Map'; +import View from 'ol/View'; +import { defaults as defaultControls, OverviewMap } from 'ol/control'; +import { defaults } from 'ol/interaction'; +import * as olProj from 'ol/proj'; +import proj4 from 'proj4'; +import { register } from 'ol/proj/proj4'; +import GisApp from './lib/gis.core'; +import GisContent from './GisContent'; +import GisLonLat from './GisLonLat'; +import GisToolBar from './GisToolBar'; +import GisTab from './GisTab'; +import MapToggleBtn from './MapToggleBtn'; +import FeatureClickEventPopup from '@/map/featureClickEventPopup'; + +export const GisContext = createContext({gisCore: null, gisMap: null, treeData:null, checkedLayerIds:null}); +export const useGisContext = () => useContext(GisContext); + +function Gis({ treeData = [], isSearch = false }) { + const mapRef = useRef(); + const [mapObject, setMapObject] = useState({}); + const [tabVisible, setTabVisible] = useState(true); + const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 + const [featureData,setFeatureData] = useState(null) // 선택된 피쳐 데이터 + const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태 + const gisCore = useMemo(() => new GisApp.core({ targetId: mapRef.current }), []); + const mapInstanceRef = useRef(null); // 맵 생성 여부를 추적할 ref + const checkedLayerIds = useRef([]); + let gisMap = null; + const closePopup = () => { + setIsOpen(false); // 팝업 닫기 + setFeatureData(null) + }; + const handleFeatureSelect = (data) => { + console.log('check',data) + setFeatureData(data); // 선택된 피처 데이터 저장 + setIsOpen(true); // 팝업 열기 + }; + + // SearchModal이 열릴 때 featureClickEventPopup 닫기 + useEffect(() => { + if (isSearchModalOpen && isOpen) { + setIsOpen(false); + setFeatureData(null); + } + }, [isSearchModalOpen]); + + useEffect(() => { + if (!mapRef.current) return; + if (mapInstanceRef.current) return; // 이미 생성된 맵이 있다면 중단 + + // EPSG:5179 등록 + proj4.defs([ + [ + 'EPSG:5179', + '+proj=tmerc +lat_0=38 +lon_0=127.5 +k=0.9996 +x_0=1000000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs', + ], + ]); + register(proj4); + + //맵 생성 + const map = new OlMap({ + layers: [gisCore.baseLayers['base'], gisCore.baseLayers['satellite']], + renderer: 'webgl', + target: mapRef.current, + interactions: defaults({ doubleClickZoom: false }), + controls: defaultControls({ + zoom: false, + rotate: false, + attributionOptions: { + collapsible: false, + }, + }).extend([]), + view: new View({ + projection: GisApp.TO_PROJ, + zoom: gisCore.defaults.zoom, + center: olProj.transform( + [126.85306, 35.571634], + GisApp.FROM_PROJ, + GisApp.TO_PROJ + ), + minZoom: gisCore.defaults.minZoom, + maxZoom: gisCore.defaults.maxZoom, + }), + }); + + //미니맵 + const overviewMap = new OverviewMap({ + layers: [GisApp.Module.core.baseMiniMapLayers['base']], + view: new View({ + projection: GisApp.TO_PROJ, + minZoom: 9, + }), + collapsed: false, + collapseLabel: '\u00BB', + label: '\u00AB', + }); + + map.addControl(overviewMap); + gisCore.__proto__.map = map; + + gisCore.scaleControl(); + gisCore.changeBaseLayer('base'); + setMapObject({ map: map }); + mapInstanceRef.current = map; // 생성 완료 플래그 설정 + }, []); + + useEffect(() => { + if (!treeData || treeData.length === 0) return; + + gisCore.deleteAllOverlay(); + gisCore.deleteAllLayer(); + + const layerCode = gisCore.convertLayerInfosToLayerCode(treeData, 'lyrSn'); + GisApp.layerCode = layerCode; + + //setTreeData(data); + const layerItems = getLayerItems(layerCode); + //setEditLayerItems(prev => [...layerItems]) + }, [treeData]); + + const getLayerItems= (layerCode) => { + const layerItems = Object.entries(layerCode) + .filter(([key, obj], idx) => obj?.updtPsblYn === 'Y') + .map(([key, obj]) => ({ + ...obj, + name: obj.lyrNm, + label: obj.lyrNm, + value: obj.lyrSn + '', + })) + .sort((a, b) => { + if (a.sortOrdr > b.sortOrdr) return 1; + if (a.sortOrdr < b.sortOrdr) return -1; + return 0; + }); + return layerItems; + }; + + if (mapObject.map) { + try { + gisMap = mapObject.map; + } catch (e) { + console.error('오류 발생:', e); // 시큐어코딩 관련 수정 : 오류 정보를 로그로 출력 + } + } + + return ( + <> + <GisContext.Provider value={{gisCore, gisMap, treeData, checkedLayerIds}}> + <div className="h-[calc(100vh-10rem)] flex relative"> + <GisTab visible={tabVisible} setVisible={setTabVisible} isSearch={isSearch} isSearchModalOpen={isSearchModalOpen} setIsSearchModalOpen={setIsSearchModalOpen} /> + <GisContent onFeatureSelect={handleFeatureSelect}/> + {isOpen && featureData && ( + <FeatureClickEventPopup + featureData={featureData} + onClose={closePopup} // 닫기 콜백 전달 + /> + )} + <div ref={mapRef} className="gis-map flex-1 relative" style={{ height: '100%', padding: 0 }} /> + {/* <GisLonLat/> + <GisToolBar/> + <MapToggleBtn/> */} + </div> + </GisContext.Provider> + </> + ); +} + +export default React.memo(Gis); diff --git a/packages/common/business_components/src/ui/map/GisContent.js b/packages/common/business_components/src/ui/map/GisContent.js new file mode 100644 index 000000000..93edcd12c --- /dev/null +++ b/packages/common/business_components/src/ui/map/GisContent.js @@ -0,0 +1,60 @@ +import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; +import GisApp from './lib/gis.core'; +import { useGisContext } from './Gis'; +import GisLonLat from './GisLonLat'; +import GisSelect from './GisSelect'; +import GisToolBar from './GisToolBar'; +import './lib/css/gis.edit.css'; +import MapToggleBtn from './MapToggleBtn'; +import GisEdit from './GisEdit'; + +export default function GisContent({ edit = false, onFeatureSelect }) { + const { gisMap, gisCore } = useGisContext(); + const [visibleComponents, setVisibleComponents] = useState({}); + const addressResultLayer = useRef(null); + const [playShow, setPlayShow] = useState({ show: false }); + + //셀렉트 버튼 이벤트 + // const clickFeature = useCallback(feature => { + // if (feature) { + // const layer = feature.getLayer(); + // setVisibleComponents(prevState => ({ + // ...prevState, + // })); + // } + // }, []); + + const selectRslt = useCallback(feature => { + GisApp.Module.layer.highLightFeature( + [feature], + { + id: GisApp.hlLayerId, + zIndex: 10, + opacity: 1, + selectable: true, + }, + { width: 30 } + ); + clickFeature(feature); + }, []); + + useEffect(() => { + if (gisMap) { + setVisibleComponents(prevState => ({ + ...prevState, + })); + } + }, [gisMap]); + + if (!gisMap) return null; + + return ( + <> + {/*<GisTree />*/} + <MapToggleBtn /> + <GisLonLat /> + <GisToolBar /> + <GisSelect onFeatureSelect={onFeatureSelect}/> + </> + ); +} diff --git a/packages/common/business_components/src/ui/map/GisEdit.js b/packages/common/business_components/src/ui/map/GisEdit.js new file mode 100644 index 000000000..dcf51545e --- /dev/null +++ b/packages/common/business_components/src/ui/map/GisEdit.js @@ -0,0 +1,173 @@ +import GisApp from './lib/gis.core'; +import React, { useContext, useEffect, useRef, useState } from 'react'; +import { GisContext, setTree } from './Gis'; +import { Button } from "@common/components"; +import { gAlert, gConfirm } from './lib/util'; +// import { fclApi } from '../../js/apis/fcl/fclApi'; + +export default React.memo(function GisEdit() { + const { editLayerItems = [], siteSn, locgovCd, checkedLayerIds } = useContext(GisContext); + const editModule = GisApp.Module.edit; + const ftrIdn = useRef(null); + const selectedLyrId = useRef(null); + if (editLayerItems.length > 0 && !selectedLyrId.current) { + selectedLyrId.current = editLayerItems[0].value; + } + const [btnEvt, setBtnEvt] = useState({ edit: false, modify: false, add: false, snap: false }); + const edit = () => { + const edit = !btnEvt.edit; + if (edit) { + setBtnEvt({ ...btnEvt, edit: edit }); + } else { + setBtnEvt({ edit: edit, modify: false, add: false, snap: false }); + editModule.clearUndoRedoFeature(); + } + editModule.removeInteraction(); + }; + + const modify = () => { + const modify = !btnEvt.modify; + setBtnEvt({ ...btnEvt, ...{ add: false }, modify }); + if (modify) { + editModule.setModify(); + } else { + editModule.removeModify(); + } + }; + + const add = () => { + let add = !btnEvt.add; + if (add) { + if (!checkedLayerIds?.current?.includes(selectedLyrId.current)) { + alert('트리에서 레이어 체크 후 추가 가능합니다.'); + return; + } + const errMsg = editModule.setDraw(selectedLyrId.current); + if (errMsg) { + alert(errMsg); + return; + } + } else { + editModule.removeDraw(); + } + setBtnEvt({ ...btnEvt, ...{ modify: false }, add }); + }; + + const del = () => { + editModule.deleteFeature(); + }; + + const undo = () => { + GisApp.Module.edit.undo(); + }; + + const redo = () => { + GisApp.Module.edit.redo(); + }; + + const snap = () => { + const snap = !btnEvt.snap; + setBtnEvt({ ...btnEvt, snap }); + if (snap) { + editModule.setSnap(); + } else { + editModule.removeSnap(); + } + }; + + const validateEditFeatures = editFeatures => { + if (editFeatures.length === 0) { + //gAlert('INFO', 'NO_CHANGED_DATA'); + return false; + } + return true; + }; + + const save = async () => { + const editFeatureInfo = editModule.getEditFeatureInfo(); + const editFeatures = Object.values(editFeatureInfo).map(value => ({ + siteSn, + locgovCd, + geom: value.geom, + flag: value.flag, + lyrId: value.lyr_id, + })); + if (!validateEditFeatures(editFeatures)) { + return; + } + if (!gConfirm('CONFIRM', 'SAVE_QUEST')) return false; + // const result = await fclApi.saveEditFeatures(editFeatures); + // if (result.code === 200) { + // editModule.clearUndoRedo(); + // } + }; + + useEffect(() => { + //setBtnEvt({ edit: false, modify: false, add: false, snap: false }); + editModule.setUndoInteraction(); + editModule.removeInteraction(); + }, [locgovCd]); + if (editLayerItems.length === 0) return; + return ( + <> + <div className="edit_control map_control_div ol-unselectable ol-control" style={{ pointerEvents: 'auto', top: '28px', left: '120px', zIndex: '1' }}> + <label htmlFor={'selectLyrId'}></label> + <select + id={'selectLyrId'} + onChange={evt => { + const lyrId = evt.target.value; + selectedLyrId.current = lyrId + ''; + setBtnEvt({ ...btnEvt, ...{ add: false } }); + editModule.setInteractionActive(false); + }} + > + {editLayerItems.map(layerItem => ( + <option value={layerItem?.lyrSn} key={layerItem?.lyrSn}> + {layerItem?.lyrNm} + </option> + ))} + </select> + <Button btn={{ type: `${btnEvt.edit ? 'primary' : 'secondary'}`, fnc: edit }}>{`${btnEvt.edit ? ' 편집종료' : '편집하기'}`}</Button> + {btnEvt.edit && ( + <ul> + <li> + <div className={`btn_edit_slt${btnEvt.modify ? ' active' : ''}`} title="수정" onClick={modify}> + <span>수정</span> + </div> + </li> + <li> + <div className={`btn_edit_add${btnEvt.add ? ' active' : ''}`} title="추가" onClick={add}> + <span>추가</span> + </div> + </li> + <li> + <div className="btn_edit_del" title="삭제" onClick={del}> + <span>삭제</span> + </div> + </li> + <li> + <div className="btn_edit_prev" title="이전" onClick={undo}> + <span>이전</span> + </div> + </li> + <li> + <div className="btn_edit_next" title="다음" onClick={redo}> + <span>다음</span> + </div> + </li> + <li> + <div className={`btn_edit_snap${btnEvt.snap ? ' active' : ''}`} title="스냅" onClick={snap}> + <span>스냅</span> + </div> + </li> + <li> + <div className="btn_edit_save" title="저장" onClick={save}> + <span>저장</span> + </div> + </li> + </ul> + )} + </div> + </> + ); +}); diff --git a/packages/common/business_components/src/ui/map/GisLonLat.js b/packages/common/business_components/src/ui/map/GisLonLat.js new file mode 100644 index 000000000..2cee2a9c8 --- /dev/null +++ b/packages/common/business_components/src/ui/map/GisLonLat.js @@ -0,0 +1,44 @@ +import React from 'react'; +import GisApp from './lib/gis.core'; +import { MousePosition } from 'ol/control'; +import { useContext, useEffect, useRef } from 'react'; +import { GisContext } from './Gis'; +export default React.memo(function GisLonLat() { + const { gisCore } = useContext(GisContext); + const labelLonLatRef = useRef(null); + + useEffect(() => { + if (!gisCore?.map || !labelLonLatRef.current) return; + + // 기존 MousePosition 제거를 위한 컨트롤 추적 + let existingMouseControls = gisCore.map + .getControls() + .getArray() + .filter(control => control instanceof MousePosition); + + // 기존 MousePosition 컨트롤 제거 + existingMouseControls.forEach(control => { + gisCore.map.removeControl(control); + }); + + // 새로운 MousePosition 컨트롤 추가 + const mousePosition = new MousePosition({ + coordinateFormat: function (coordinate) { + let lng = coordinate[0]; + let lat = coordinate[1]; + return `경도 : ${lng.toFixed(3)}   위도 : ${lat.toFixed(3)}`; + }, + projection: GisApp.FROM_PROJ, + className: 'custom-mouse-position', + target: labelLonLatRef.current, + }); + gisCore.map.addControl(mousePosition); + }, [gisCore.map]); + return ( + <> + <ul className="map-scale-area"> + <li id="label_lonlat" ref={labelLonLatRef} /> + </ul> + </> + ); +}); diff --git a/packages/common/business_components/src/ui/map/GisSearch.js b/packages/common/business_components/src/ui/map/GisSearch.js new file mode 100644 index 000000000..c321d8efa --- /dev/null +++ b/packages/common/business_components/src/ui/map/GisSearch.js @@ -0,0 +1,9 @@ +import React from 'react'; + +export default function GisSearch() { + return ( + <div className="p-2"> + <p>범례 영역입니다 (향후 컴포넌트로 교체 가능)</p> + </div> + ); +} \ No newline at end of file diff --git a/packages/common/business_components/src/ui/map/GisSelect.js b/packages/common/business_components/src/ui/map/GisSelect.js new file mode 100644 index 000000000..4aa8f3049 --- /dev/null +++ b/packages/common/business_components/src/ui/map/GisSelect.js @@ -0,0 +1,55 @@ +import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; +import GisApp from './lib/gis.core'; +import GisOlSelect from './lib/gis.select'; +import { GisContext } from './Gis'; + +export default React.memo(function GisSelect({ onFeatureSelect }) { + + const { gisMap } = useContext(GisContext); + + + // useEffect(() => { + // if (!gisMap) return; // 아직 초기화 안 됐으면 아무것도 안 함 + + // const select = new GisOlSelect(gisMap); + // GisApp.Module.select = select; + + // select.getSelectedInfo(function (features) { + // if (features?.[0]) { + // // onFeatureClick?.(features[0]); // props로 넘긴 함수 실행 + // onFeatureSelect(layer.getProperties()); //피쳐 속성데이터 상위컴포넌트로 전달 + // } + // }); + // }, [gisMap, onFeatureSelect]); + + const select = new GisOlSelect(gisMap); + GisApp.Module.select = select; + + select.getSelectedInfo(function (features) { + //clickFeature(features[0]); + if(features.length > 0) { + const layer = features[0]; + onFeatureSelect(layer); //피쳐 속성데이터 상위컴포넌트로 전달 + } + }); + + return ( + <> + {/* <div className="select_control map_control_div ol-unselectable ol-control" style={{ pointerEvents: 'auto', zIndex: '1' }}> + <h3>선,면,다각형을 이용한 시설물 선택 영역</h3> + <ul> + <li> + <div className="sprite_gis icon1 btn_select_one" title="단일선택" onClick={reset}> + <span>단일선택</span> + </div> + </li> + <li> + <div className="sprite_gis icon4 btn_select_cancel" title="새로고침" onClick={reset}> + <span>새로고침</span> + </div> + </li> + </ul> + </div> */} + </> + ); +}); diff --git a/packages/common/business_components/src/ui/map/GisTab.js b/packages/common/business_components/src/ui/map/GisTab.js new file mode 100644 index 000000000..d658c0bfb --- /dev/null +++ b/packages/common/business_components/src/ui/map/GisTab.js @@ -0,0 +1,81 @@ +import React, { useState } from 'react'; +import { Tabs, TabsList, TabsTrigger } from '@common/components/ui/tabs'; +import GisTree from './GisTree'; +import GisSearch from './GisSearch'; +import { ChevronLeft, ChevronRight, Search } from 'lucide-react'; +import SearchModal from '@/map/SearchModal'; + + +function GisTab({ visible, setVisible, isSearch, isSearchModalOpen, setIsSearchModalOpen }) { + const [activeTab, setActiveTab] = useState('layers'); + + return ( + <> + <div + className={`absolute top-0 left-0 w-[250px] h-[calc(100vh-10rem)] bg-white border-r border-gray-300 flex flex-col overflow-hidden transition-all duration-300 z-40 + ${visible ? 'translate-x-0 opacity-100' : '-translate-x-full opacity-0 pointer-events-none'}`} + > + <Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col overflow-hidden"> + <TabsList className="flex-none mt-4 border-none rounded-none"> + <TabsTrigger value="layers" className="flex-1 h-10">레이어</TabsTrigger> + {isSearch && ( + <button + type="button" + onClick={() => setIsSearchModalOpen(true)} + className="h-10 w-10 flex items-center justify-center px-0" + > + <Search size={18} className="text-gray-700" /> + </button> + // <TabsTrigger value="search" className="flex-1 h-10 flex items-center justify-center"> + // <Search size={18} /> + // </TabsTrigger> + )} + </TabsList> + + {/* GisTree는 항상 마운트되며, 탭 상태에 따라 보여주기만 함 */} + <div + className={`grow overflow-y-auto p-2 pt-0 border-none shadow-none ${activeTab === 'layers' ? 'block' : 'hidden' + }`} + > + <GisTree /> + </div> + + {/* GisSearch도 동일하게 처리 */} + {/* {isSearch && ( + <div + className={`grow overflow-y-auto p-2 pt-0 border-none shadow-none ${ + activeTab === 'search' ? 'block' : 'hidden' + }`} + > + <GisSearch /> + </div> + )} */} + + + {/* 🔍 검색 모달 */} + <SearchModal open={isSearchModalOpen} onOpenChange={setIsSearchModalOpen} /> + </Tabs> + + {/* 접기 버튼 */} + <button + onClick={() => setVisible(false)} + className="absolute top-1/2 right-[-3px] transform -translate-y-1/2 z-50 bg-white border border-l border-gray-300 rounded-l-none rounded-r px-1 py-2 shadow-sm" + > + <ChevronLeft size={16} /> + </button> + </div> + + {/* 열기 버튼 */} + {!visible && ( + <button + onClick={() => setVisible(true)} + className="absolute top-1/2 left-[-3px] transform -translate-y-1/2 z-50 bg-white border border-r border-gray-300 rounded-r px-1 py-2 shadow-sm" + > + <ChevronRight size={16} /> + </button> + )} + </> + ); +} + +export default React.memo(GisTab); diff --git a/packages/common/business_components/src/ui/map/GisToolBar.js b/packages/common/business_components/src/ui/map/GisToolBar.js new file mode 100644 index 000000000..c5c613cc9 --- /dev/null +++ b/packages/common/business_components/src/ui/map/GisToolBar.js @@ -0,0 +1,71 @@ +import React from 'react'; +import GisApp from './lib/gis.core'; +import GisOlMeasure from './lib/gis.measure'; +import { useContext } from 'react'; +import { useGisContext } from './Gis'; +export default React.memo(function GisToolBar() { + const { gisMap } = useGisContext(); + if (!gisMap) return <></>; + const gisMeasuer = new GisOlMeasure(gisMap); + GisApp.Module.measure = gisMeasuer; + const zoomIn = () => { + if (gisMap.getView().getMaxZoom() > gisMap.getView().getZoom()) { + gisMap.getView().animate({ + zoom: gisMap.getView().getZoom() + 1, + duration: 250, + }); + } + }; + const zoomOut = () => { + if (gisMap.getView().getMaxZoom() > gisMap.getView().getZoom()) { + gisMap.getView().animate({ + zoom: gisMap.getView().getZoom() - 1, + duration: 250, + }); + } + }; + + const measure = type => { + gisMeasuer.addInteraction(type); + }; + + const reset = () => { + gisMeasuer.removeFeature(); + }; + return ( + <> + <div className="custom-toolbar"> + <div className="tool-bar"> + <h3>toolbar 영역</h3> + <ul> + <li> + <div className="btn_control_zoomin" title="확대" onClick={zoomIn}> + <span>확대</span> + </div> + </li> + <li> + <div className="btn_control_zoomout" title="축소" onClick={zoomOut}> + <span>축소</span> + </div> + </li> + <li> + <div className="btn_control_ruler btn_control_measure" title="거리 측정" onClick={() => measure(GisApp.measure.LineString)}> + <span>거리측정</span> + </div> + </li> + <li> + <div className="btn_control_area btn_control_measure" title="면적 측정" onClick={() => measure(GisApp.measure.Polygon)}> + <span>면적측정</span> + </div> + </li> + <li> + <div className="sprite_gis icon4 btn_select_cancel" title="리셋" onClick={() => reset()}> + <span>리셋</span> + </div> + </li> + </ul> + </div> + </div> + </> + ); +}); diff --git a/packages/common/business_components/src/ui/map/GisTree.js b/packages/common/business_components/src/ui/map/GisTree.js new file mode 100644 index 000000000..d01786187 --- /dev/null +++ b/packages/common/business_components/src/ui/map/GisTree.js @@ -0,0 +1,209 @@ +import GisApp from './lib/gis.core'; +import { util } from './lib/util'; +import React, { useEffect, useState } from 'react'; +import { useGisContext } from './Gis'; +import { DndProvider } from "react-dnd"; +import { + Tree, + MultiBackend, + getBackendOptions +} from "@minoru/react-dnd-treeview"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@common/components/ui/tabs"; + +export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes = () => {} }) { + const { gisCore, treeData } = useGisContext(); + const [checked, setChecked] = useState([]); + const [expanded, setExpanded] = useState([]); + const [nodes, setNodes] = useState([]); + const [isSidebarOpen, setIsSidebarOpen] = useState(true); + + useEffect(() => { + if (treeData && treeData.length > 0) { + const nodeData = setNodeValues(treeData); + setNodes(nodeData); + gisCore.setAllLayersOnMap(false); + } + }, [treeData]); + + useEffect(() => { + if (nodes.length > 0) { + const defaultChecked = getDefaultCheckedNodes(nodes); + setChecked(defaultChecked); + const defaultExpanded = Array.from(new Set([ + ...defaultChecked, + ...defaultChecked.flatMap((id) => getAncestors(id, nodes)) + ])); + setExpanded(defaultExpanded); + gisCore.setLayerOnMap(defaultChecked, true); + } + }, [nodes]); + + const getDefaultCheckedNodes = (nodes, values = []) => { + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + if (node.data.initEprssYn === 'Y') { + values.push(node.data.lyrSn); + } + } + return values; + }; + + const getDescendants = (id, all) => { + const children = all.filter((n) => n.parent === id); + return children.reduce( + (acc, child) => acc.concat(child.id, ...getDescendants(child.id, all)), + [] + ); + }; + + const getAncestors = (id, all) => { + const node = all.find((n) => n.id === id); + if (!node || node.parent == null) return []; + return [node.parent, ...getAncestors(node.parent, all)]; + }; + + const isEveryChildChecked = (parentId, all, checked) => { + const children = all.filter((n) => n.parent === parentId); + return children.every((c) => checked.includes(c.id)); + }; + + const handleCheck = (id) => { + setChecked((prevChecked) => { + const isChecked = prevChecked.includes(id); + let newChecked = [...prevChecked]; + if (isChecked) { + const descendants = getDescendants(id, nodes); + newChecked = newChecked.filter((cid) => cid !== id && !descendants.includes(cid)); + const ancestors = getAncestors(id, nodes); + ancestors.forEach((p) => { + if (newChecked.includes(p)) { + newChecked = newChecked.filter((cid) => cid !== p); + } + }); + } else { + const descendants = getDescendants(id, nodes); + newChecked = Array.from(new Set([...newChecked, id, ...descendants])); + const ancestors = getAncestors(id, nodes); + ancestors.forEach((p) => { + if (isEveryChildChecked(p, nodes, newChecked)) { + newChecked.push(p); + } + }); + // setExpanded((prev) => Array.from(new Set([...prev, ...ancestors]))); + } + const allIds = nodes.map((n) => n.id); + const unchecked = allIds.filter((nid) => !newChecked.includes(nid)); + gisCore.setLayerOnMap(newChecked, true); + gisCore.setLayerOnMap(unchecked, false); + return newChecked; + }); + }; + + const setNodeValues = (result) => { + return result.map((item) => { + const src = makeLegendSrc(item); + let label = item.lyrNm; + return { + id: item.lyrSn, + text: label, + parent: item.upperLyrSn, + droppable: !!item.children, + data: item, + icon: src ? <img src={src} alt={item.lyrNm} /> : '', + }; + }); + }; + + const makeLegendSrc = layerinfo => { + if(!layerinfo.gobjTy) return null; + + let src; + if (layerinfo.iconNm) { + src = GisApp.CONTEXT_LAYER_PATH + layerinfo.iconNm; + } else { + const ss = makeLegendImage(layerinfo); + src = ss.src; + } + return src; + }; + + const makeLegendImage = layerinfo => { + let image = document.createElement('img'); + let canvas = document.createElement('canvas'); + canvas.width = 16; + canvas.height = 16; + let ctx = canvas.getContext('2d'); + ctx.beginPath(); + + if (layerinfo.gobjTy === 'lineLabelPoligon') { + ctx.moveTo(1, 1); + ctx.lineTo(15, 1); + ctx.lineTo(15, 15); + ctx.lineTo(1, 15); + ctx.lineTo(1, 1); + } else if (layerinfo.gobjTy === 'line') { + ctx.moveTo(1, 15); + ctx.lineTo(5, 1); + ctx.lineTo(9, 15); + ctx.lineTo(15, 1); + } else { + ctx.moveTo(1, 1); + ctx.lineTo(15, 1); + ctx.lineTo(15, 15); + ctx.lineTo(1, 15); + ctx.lineTo(1, 1); + } + + if (layerinfo.gobjTy === 'lineLabelPoligon') { + ctx.fillStyle = layerinfo.polgnColor; + ctx.fill(); + ctx.lineWidth = 1; + ctx.strokeStyle = layerinfo.lnColor; + ctx.stroke(); + } + + if (layerinfo.gobjTy === 'line') { + ctx.lineWidth = 1; + ctx.strokeStyle = layerinfo.lnColor; + ctx.stroke(); + } + + image.src = canvas.toDataURL('image/png'); + return image; + }; + + if (!treeData || treeData.length === 0) return null; + + return ( + <> + <DndProvider backend={MultiBackend} options={getBackendOptions()}> + <Tree + tree={nodes} + rootId={null} + initialOpen={expanded} + onOpenChange={setExpanded} + render={(node, { depth, isOpen, onToggle }) => ( + <div + className="flex items-center gap-2 py-1 px-2 hover:bg-gray-100 rounded" + style={{ marginInlineStart: depth * 16 + (!node.droppable ? 16 : 0) }} + > + {node.droppable && ( + <i onClick={onToggle} className={`diveicon di-arrow-forward mr-2 text-base text-label ${isOpen ? 'rotate-90' : ''}`} + title={isOpen ? '닫기' : '열기'}> + </i> + )} + <input type="checkbox" checked={checked.includes(node.id)} onChange={() => handleCheck(node.id)} className="cursor-pointer"/> + {node.icon && <span>{node.icon}</span>} + <span className="text-[1.3rem] font-medium text-label">{node.text}</span> + </div> + )} + sort={(a, b) => { + const aOrder = parseInt(a.data?.sortOrdr ?? '0'); + const bOrder = parseInt(b.data?.sortOrdr ?? '0'); + return aOrder - bOrder; + }} + /> + </DndProvider> + </> + ); +}); diff --git a/packages/common/business_components/src/ui/map/Google3DMap.js b/packages/common/business_components/src/ui/map/Google3DMap.js new file mode 100644 index 000000000..2ae6547dd --- /dev/null +++ b/packages/common/business_components/src/ui/map/Google3DMap.js @@ -0,0 +1,60 @@ +import React, { useEffect, useRef, useState } from 'react'; +import OLCesium from 'olcs/OLCesium'; +import olView from 'ol/View.js'; +import olSourceImageWMS from 'ol/source/ImageWMS.js'; +import olSourceOSM from 'ol/source/OSM.js'; +import olLayerImage from 'ol/layer/Image.js'; +import olLayerTile from 'ol/layer/Tile.js'; +import olMap from 'ol/Map.js'; +import { GLOBAL } from '../../js/consts/global'; + +const Google3DMap = () => { + const mapRef = useRef(null); + const [ol3d, setOl3d] = useState(null); + + useEffect(() => { + if (!mapRef.current) return; + + const customProjSource = new olSourceImageWMS({ + attributions: '© <a href="http://www.geo.admin.ch/internet/geoportal/en/home.html">National parks / geo.admin.ch</a>', + crossOrigin: 'anonymous', + params: { LAYERS: 'ch.bafu.schutzgebiete-paerke_nationaler_bedeutung' }, + projection: 'EPSG:21781', + url: 'https://wms.geo.admin.ch/', + }); + + const { Cesium } = window; + Cesium.Ion.defaultAccessToken = GLOBAL.CESIUM_ION_TOKEN; + + const ol2d = new olMap({ + layers: [new olLayerTile({ source: new olSourceOSM() }), new olLayerImage({ source: customProjSource })], + target: mapRef.current, + view: new olView({ + center: [860434.6266531206, 6029479.0044273855], + zoom: 6, + }), + }); + + const ol3dInstance = new OLCesium({ + map: ol2d, + time: () => Cesium.JulianDate.now(), + }); + + const scene = ol3dInstance.getCesiumScene(); + Cesium.createWorldTerrainAsync().then(tp => { + scene.terrainProvider = tp; + }); + + ol3dInstance.setEnabled(true); + setOl3d(ol3dInstance); + + return () => { + ol3dInstance.setEnabled(false); + ol2d.setTarget(null); + }; + }, []); + + return <div ref={mapRef} style={{ width: '100%', height: '100vh' }} />; +}; + +export default Google3DMap; diff --git a/packages/common/business_components/src/ui/map/MapToggleBtn.js b/packages/common/business_components/src/ui/map/MapToggleBtn.js new file mode 100644 index 000000000..fe8258ecb --- /dev/null +++ b/packages/common/business_components/src/ui/map/MapToggleBtn.js @@ -0,0 +1,32 @@ +import React, { useContext, useState } from 'react'; +import { Button } from "@common/components"; +import { GisContext } from './Gis'; +import GisApp from './lib/gis.core'; + +export default React.memo(function MapToggleBtn() { + const { gisCore } = useContext(GisContext); + const [activeMap, setActiveMap] = useState({ label: '일반', type: 'base' }); // 기본 선택값 + + const handleSelect = mapObj => { + setActiveMap(mapObj); + gisCore.changeBaseLayer(mapObj.type); + }; + + return ( + <div className="mapbtn" style={{ zIndex: 101 }}> + <button className="sprite_gis viewer" title="지도선택">지도 선택</button> + <ul className="choice_map"> + <li className={activeMap.type === 'base' ? 'on' : ''}> + <button className="map1" id="base" title="일반지도" onClick={() => handleSelect({ label: '일반', type: 'base' })}> + <div>일반지도</div> + </button> + </li> + <li className={activeMap.type === 'satellite' ? 'on' : ''}> + <button className="map2" id="satelite" title="위성지도" onClick={() => handleSelect({ label: '위성', type: 'satellite' })}> + <div>위성지도</div> + </button> + </li> + </ul> + </div> + ); +}); diff --git a/packages/common/business_components/src/ui/map/lib/css/gis.common.css b/packages/common/business_components/src/ui/map/lib/css/gis.common.css new file mode 100644 index 000000000..e479fc501 --- /dev/null +++ b/packages/common/business_components/src/ui/map/lib/css/gis.common.css @@ -0,0 +1,1035 @@ +@charset "UTF-8"; + +.custom-toolbar { + position: absolute; + top: -20px; + right: 0px; +} +.mapbtn { + position: absolute; + top: 10px; + right: 20px; +} +.mapbtn h3 { + overflow: hidden; + font-size: 0; + line-height: 0; + text-indent: -9999px; +} +.mapbtn > button { + overflow: hidden; + width: 40px; + height: 38px; + background: url(../images/toolbar/ico_gis_sprite.png) #fff no-repeat; + border: 1px solid #9aa7b8; + border-radius: 3px; + box-shadow: 0px 0px 5px #999; +} +.mapbtn .map1 { + background-image: url('../images/toolbar/img_map1.png'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + width: 60px; + height: 40px; +} +.mapbtn .map2 { + background-image: url('../images/toolbar/img_map2.png'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + width: 60px; + height: 40px; +} +.mapbtn > button > div { + display: none; + overflow: hidden; + text-indent: -9999px; +} +.mapbtn .viewer { + background-position: 0 -120px; +} +.mapbtn > a.danmyun { + background: url(../images/toolbar/ico_gis_sprite.png) #fff 0 -400px; +} +.mapbtn > a:hover { + margin: -1px; + border: 2px solid #1357ce; +} +.mapbtn .choice_map { + display: none; + position: absolute; + top: 0; + right: 20px; + width: 150px; +} +.mapbtn:hover ul.choice_map { + display: block; +} +.mapbtn ul.choice_map { + font-size: 0; +} +.mapbtn ul.choice_map li { + float: left; + margin-right: 5px; + background: #fff; + width: 60px; + text-align: center; + border: 2px solid transparent; + border-radius: 3px; + box-shadow: 0px 0px 5px #999; + overflow: hidden; +} +.mapbtn ul.choice_map li:last-child { + margin-right: 0; +} +.mapbtn ul.choice_map > li:hover { + border: 2px solid #1357ce; +} +.mapbtn ul.choice_map > li.on { + border: 2px solid #1357ce; +} +.mapbtn ul.choice_map li span { + font-size: 12px; + color: #999; + text-align: center; +} +.mapbtn ul.choice_map li.on span { + font-size: 12px; + color: #333; + text-align: center; +} + +.tool-bar { + position: absolute; + top: 70px; + right: 20px; + z-index: 2; +} +.tool-bar h3 { + overflow: hidden; + font-size: 0; + line-height: 0; + text-indent: -9999px; +} +.tool-bar .status { + margin-bottom: 10px; + overflow: hidden; + width: 40px; + height: 40px; + background: #fff; + border: 1px solid #9aa7b8; + border-radius: 3px; + box-shadow: 0px 0px 5px #999; + box-sizing: border-box; +} +.tool-bar .status div { + display: block; + width: 40px; + height: 40px; + background: url(../images/toolbar/ico_gis_sprite.png) no-repeat; + background-position: 0 -160px; + text-indent: -9999px; +} +.tool-bar .status div:hover { + background-position: -40px -160px; +} +.tool-bar .status div.on { + background: url(../images/toolbar/ico_gis_sprite.png) no-repeat; + background-position: 0 -200px; +} +.tool-bar .status div.on:hover { + background-position: -40px -200px; +} +.tool-bar ul { + display: inline-block; + overflow: hidden; + width: 40px; + background: #fff; + border: 1px solid #9aa7b8; + border-radius: 3px; + box-shadow: 0px 0px 5px #999; + font-size: 0; + box-sizing: border-box; + padding: 0px; +} +.tool-bar ul li { + height: 40px; + border-bottom: 1px solid #eeeeee; + box-sizing: border-box; +} +.tool-bar ul li:last-child { + border-bottom: 0 none; +} +.tool-bar ul li div { + width: 40px; + height: 40px; + display: inline-block; +} +.tool-bar ul li div span { + display: none; + overflow: hidden; + text-indent: -9999px; +} + +.ol-scale-line { + background: rgba(0, 60, 136, 0.3); + border-radius: 4px; + padding: 2px; + position: absolute; + bottom: 10px; + right: 200px; +} + +/* 로드뷰 관련 */ +#roadview-container { + width: 100%; + height: 100%; + z-index: 500; + display: block; + position: unset; + left: 50%; + top: 0px; +} +#roadview-container .roadview-container-1p { + width: 50%; + height: 100%; + float: left; + position: relative; +} +#roadview-container .roadview-container-2p { + width: 50%; + height: 100%; + float: left; + position: relative; +} +#roadview-container .roadview { + position: absolute; + top: 0; + right: 0; + left: 0; + bottom: 0; + z-index: 0; + background: #999; +} + +/* Map Contents: Map Control Bar */ +.map_control { + position: absolute; + right: 20px; + top: 200px; + display: block; +} +.control_icons { + background: rgba(255, 255, 255, 0.9); + border-radius: 4px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); + padding: 10px; + display: block; +} +.control_icons li a { + display: block; + width: 40px; + height: 40px; + font-size: 0; +} +.control_icons li a:hover { + border-radius: 100%; + box-shadow: 0 3px 5px rgba(0, 0, 0, 0.1); +} +.btn_control_zoomin { + background: url(../images/toolbar/btn_cont_zoomin.png) no-repeat center; +} +.btn_control_zoomin:hover { + background: url(../images/toolbar/btn_cont_zoomin_on.png) no-repeat center #67aee3; +} +.btn_control_zoomout { + background: url(../images/toolbar/btn_cont_zoomout.png) no-repeat center; +} +.btn_control_zoomout:hover { + background: url(../images/toolbar/btn_cont_zoomout_on.png) no-repeat center #67aee3; +} +.btn_control_prev { + background: url(../images/toolbar/btn_cont_prev.png) no-repeat center; +} +.btn_control_prev:hover { + background: url(../images/toolbar/btn_cont_prev_on.png) no-repeat center #67aee3; +} +.btn_control_next { + background: url(../images/toolbar/btn_cont_next.png) no-repeat center; +} +.btn_control_next:hover { + background: url(../images/toolbar/btn_cont_next_on.png) no-repeat center #67aee3; +} +.btn_control_ruler { + background: url(../images/toolbar/btn_cont_ruler.png) no-repeat center center; +} +.btn_control_ruler:hover { + background: url(../images/toolbar/btn_cont_ruler_on.png) no-repeat center #67aee3; +} +.btn_control_ruler.active { + background: url(../images/toolbar/btn_cont_ruler_on.png) no-repeat center #67aee3; +} +.btn_control_area { + background: url(../images/toolbar/btn_cont_area.png) no-repeat center; +} +.btn_control_area:hover { + background: url(../images/toolbar/btn_cont_area_on.png) no-repeat center #67aee3; +} +.btn_control_area.active { + background: url(../images/toolbar/btn_cont_area_on.png) no-repeat center #67aee3; +} +.btn_control_save { + background: url(../images/toolbar/btn_cont_save.png) no-repeat center; +} +.btn_control_save:hover { + background: url(../images/toolbar/btn_cont_save_on.png) no-repeat center #67aee3; +} +.btn_control_sketch { + background: url(../images/toolbar/btn_cont_sketch.png) no-repeat center; +} +.btn_control_sketch:hover { + background: url(../images/toolbar/btn_cont_sketch_on.png) no-repeat center #67aee3; +} +.btn_control_print { + background: url(../images/toolbar/btn_cont_print.png) no-repeat center; +} +.btn_control_print:hover { + background: url(../images/toolbar/btn_cont_print_on.png) no-repeat center #67aee3; +} + +/*하단 */ +.map-scale-area { + position: absolute; + bottom: 40px; + right: 200px; + display: inline-block; + z-index: 101; + list-style: none; +} +.map-scale-area li { + float: left; + margin-right: 15px; + font-size: 12px; + font-weight: 600; + color: #333; + text-shadow: -2px 0 #fff, 0 2px #fff, 2px 0 #fff, 0 -2px #fff; + -moz-text-shadow: -2px 0 #fff, 0 2px #fff, 2px 0 #fff, 0 -2px #fff; + -webkit-text-shadow: -2px 0 #fff, 0 2px #fff, 2px 0 #fff, 0 -2px #fff; +} +.map-scale-area li:last-child { + margin-right: 0; +} +.map-scale-area .scale-bar { + margin-right: 12px; + margin-top: 5px; + padding: 0 10px; + border: 2px solid #333; + border-top: 0 none; + height: 9px; + position: relative; + z-index: 999; +} +.map-scale-area .scale-bar .line-text { + position: relative; + top: -11px; +} + +/* sprite */ +.sprite_gis { + display: inline-block; /* overflow: hidden; */ + background: url(../images/toolbar/ico_gis_sprite.png) 0 0 no-repeat; + line-height: 999px; + vertical-align: bottom; +} + +.select_control { + position: absolute; + top: 80px; + left: 450px; +} +.select_control h3 { + overflow: hidden; + font-size: 0; + line-height: 0; + text-indent: -9999px; +} +.select_control ul { + display: inline-block; + overflow: hidden; + height: 40px; + font-size: 0; + background: #fff; + border: 1px solid #9aa7b8; + border-radius: 3px; + box-shadow: 0px 0px 5px #999; + box-sizing: border-box; + padding: 0px; + margin: 0px; +} +.select_control ul li { + float: left; + width: 40px; + border-right: 1px solid #eeeeee; +} +.select_control ul li:last-child { + border-right: 0 none; +} +.select_control ul li div { + overflow: hidden; + width: 40px; + height: 40px; + background: url(../images/toolbar/ico_gis_sprite.png) no-repeat; +} +.select_control ul li div span { + display: none; + overflow: hidden; + text-indent: -9999px; +} +.select_control .sprite_gis.icon1 { + background-position: 0 0; +} +.select_control .sprite_gis.icon1:hover { + background-position: -40px 0; +} +.select_control .sprite_gis.icon2 { + background-position: 0 -40px; +} +.select_control .sprite_gis.icon2:hover { + background-position: -40px -40px; +} +.select_control .sprite_gis.icon3 { + background-position: 0 -80px; +} +.select_control .sprite_gis.icon3:hover { + background-position: -40px -80px; +} +.select_control .sprite_gis.icon4 { + background-position: 0 -360px; +} +.select_control .sprite_gis.icon4:hover { + background-position: -40px -360px; +} +.select_control .sprite_gis.icon4.active { + background-position: -40px -360px; +} + +/* 20230516 하수도 자산관리용 아이콘*/ +.sprite_gis.icon4 { + background-position: 0 -360px; +} +.sprite_gis.icon4:hover { + background-position: -40px -360px !important; + background-color: #67aee3; +} + +.select_control select { + float: right; + min-width: 100px; + height: 40px; + line-height: 40px; + margin-left: 10px; + padding-left: 10px; + font-size: 13px; + color: #666; + border: 1px solid #fff; + border-radius: 3px; + box-shadow: 0px 0px 5px #999; +} + +.select_control-left0 { + left: 0px; +} + +/*** result_conts ***/ +.result_conts { + position: absolute; + top: 92px; + left: -320px; + height: 540px; + bottom: 0; + width: 320px; + border-right: 1px solid #bdbdbd; + box-shadow: 0px 0px 10px #999; + background: #fff; + box-sizing: border-box; + z-index: 500; + -webkit-transition: left 0.2s ease-out, opacity 0.3s ease-out; + -moz-transition: left 0.2s ease-out, opacity 0.3s ease-out; + transition: left 0.2s ease-out, opacity 0.3s ease-out; +} +.result_conts button.toggle { + display: block; + width: 30px; + height: 50px; + position: absolute; + top: 50%; + right: -30px; + margin-top: -28px; + background: url(../images/toolbar/toggle_off.png); + outline: none; + border: none; +} +.result_conts.on button.toggle { + background: url(../images/toolbar/toggle_on.png); +} +.result_conts.on { + left: 0; +} + +.result_conts .cheader { + padding: 16px 20px; + border-bottom: 1px solid #dddddd; + font-size: 14px; + line-height: 18px; + box-sizing: border-box; +} +.result_conts .cheader span.title { + color: #666; +} +.result_conts .cheader span.title em { + padding-right: 5px; + color: #152346; + font-weight: bold; +} +.result_conts .cbody { + position: absolute; + top: 51px; + bottom: 0; + left: 0; + right: 0; + padding: 15px 10px 0; + overflow: hidden; + overflow-y: auto; +} +.result_conts .cbody ul.list { + position: absolute; + top: 54px; + bottom: 41px; + left: 10px; + right: 10px; + overflow: hidden; + overflow-y: auto; + color: #999; + list-style: none; + padding: 0px; +} +.result_conts .cbody ul.list li { + border-bottom: 1px dotted #bbbbbb; +} +.result_conts .cbody .list li a { + color: rgb(98, 89, 202); + display: block; + width: 100%; + padding: 19px 0 15px 10px; +} +.result_conts .cbody .list li a .zip { + display: inline-block; + padding: 0 5px; + border: 1px solid #152346; + font-size: 12px; + color: #152346; + margin-right: 10px; +} +.result_conts .cbody .list li a .zip2 { + display: inline-block; + padding: 0 5px; + border: 1px solid #cc33cc; + font-size: 12px; + color: #cc33cc; +} +.result_conts .cbody .list li a .addr { + font-size: 0.875rem; + display: block; + padding-top: 5px; + color: #989898; +} +.t_count { + font-size: 0.875rem; +} + +/** 시설물정보 검색결과 하수관용도별 색상구분 : 하수관로 **/ +.result_conts .cbody .list li a .addr .SBA000_facility_init { + color: #999999; +} +.result_conts .cbody .list li a .addr .SBA001_facility_init { + color: #cc9933; +} +.result_conts .cbody .list li a .addr .SBA002_facility_init { + color: #ff9900; +} +.result_conts .cbody .list li a .addr .SBA003_facility_init { + color: #0000ff; +} +.result_conts .cbody .list li a .addr .SBA004_facility_init { + color: #cc0000; +} +.result_conts .cbody .list li a .addr .SBA999_facility_init { + color: #222222; +} + +/** 시설물정보 검색결과 하수관용도별 색상구분 : 하수맨홀 **/ +.result_conts .cbody .list li a .addr .SMU000_facility_init { + color: #999999; +} +.result_conts .cbody .list li a .addr .SMU001_facility_init { + color: #0000ff; +} +.result_conts .cbody .list li a .addr .SMU002_facility_init { + color: #cc0000; +} +.result_conts .cbody .list li a .addr .SMU003_facility_init { + color: #ff9900; +} +.result_conts .cbody .list li a .addr .SMU004_facility_init { + color: #cc9933; +} +.result_conts .cbody .list li a .addr .SMU999_facility_init { + color: #222222; +} + +/** 시설물정보 검색결과 하수관용도별 색상구분 : 물받이 **/ +.result_conts .cbody .list li a .addr .SBD000_facility_init { + color: #999999; +} +.result_conts .cbody .list li a .addr .SBD001_facility_init { + color: #0000ff; +} +.result_conts .cbody .list li a .addr .SBD002_facility_init { + color: #cc0000; +} +.result_conts .cbody .list li a .addr .SBD003_facility_init { + color: #ff9900; +} +.result_conts .cbody .list li a .addr .SBD999_facility_init { + color: #222222; +} + +.result_conts .cbody .list li.on, +.result_conts .cbody .list li:hover { + background-color: #ebebeb; +} +.result_conts .cbody .list li.on a .zip, +.result_conts .cbody .list li:hover a .zip { + color: #fff; + background-color: #152346; +} +.result_conts .cbody .list li.on a .addr, +.result_conts .cbody .list li:hover a .addr { + color: #000; +} + +.result_conts .cbody .paginate { + position: absolute; + bottom: 0; + left: 20px; + right: 0; + padding: 10px 0; + font-size: 11px; + line-height: normal; + text-align: center; +} + +/*** 페이징 **/ +.paginate { + padding: 10px 0; + font-size: 11px; + line-height: normal; + text-align: center; +} +.result_conts .cbody .paginate .pagination .page-link { + font-size: 11px !important; +} + +/*** 검색결과 더보기 **/ +.result_facility { + position: absolute; + left: 320px; + bottom: 0; + width: 1280px; + height: 200px; + border-radius: 10px; + border-right: 1px solid #bdbdbd; + box-shadow: 0px 0px 10px #999; + background: #fff; + box-sizing: border-box; + z-index: 500; + -webkit-transition: left 0.2s ease-out, opacity 0.3s ease-out; + -moz-transition: left 0.2s ease-out, opacity 0.3s ease-out; + transition: left 0.2s ease-out, opacity 0.3s ease-out; +} + +.result_facility_popup { + width: 1130px; +} + +.result_facility .cheader { + padding: 16px 20px; + border-bottom: 1px solid #dddddd; + font-size: 35px; + line-height: 18px; + box-sizing: border-box; + color: #666; + text-align: center; +} + +/*** 상수도 시설현황 **/ +.fcltyLayerSmmyDiv { + position: absolute; + top: 10px; + left: 500px; + bottom: 0; + background: #fff; + width: 130px; + overflow-x: hidden; + height: 40px; + overflow-y: hidden; + border-radius: 5px; +} +.fcltyLayerSmmyHeaderArea { + background: #ffffff; + color: black; +} +.fcltyLayerSmmyHeaderArea span { + color: #000000; + font-size: 16px; + padding-left: 10px; + line-height: 40px; +} + +.assetsSmmyDiv { + position: absolute; + top: 55px; + left: 320px; + bottom: 0; + background: #fff; + width: 310px; + overflow-x: hidden; + height: 690px; + overflow-y: auto; + border-radius: 5px; +} +.assetsNotMappingDiv { + position: absolute; + top: 10px; + right: 20px; + bottom: 0; + background: #fff; + width: 180px; + overflow-x: hidden; + height: 70px; + overflow-y: hidden; + border-radius: 5px; +} +.assetsNotAcceptDiv { + position: absolute; + top: 80px; + right: 20px; + bottom: 0; + background: #fff; + width: 180px; + overflow-x: hidden; + height: 70px; + overflow-y: hidden; + border-radius: 5px; +} + +.puDiv { + position: relative; + width: 390px; + height: 180px; + background: #fff; + margin: 4px auto 10px; + padding: 0; + border-bottom: 0px; +} +.puDiv .puHeader { + display: block; + position: relative; + margin-left: 10px; +} +.puDiv .puContent { + display: block; + position: relative; + width: 395px; + padding: 2px; + text-align: center; + margin-left: 10px; +} +.puGrayTb { + position: relative; + width: 98.3%; + margin-top: 5px; + table-layout: fixed; + font-family: Verdana; +} +.puGrayTb th { + text-align: center; + height: 29px; + background: #b9d5ff; + color: #454545; + border: 1px solid #9d9d9d; + font-size: 12px; + font-weight: bold; +} +.puGrayTb td { + text-align: center; + height: 15px; + background: #fff; + color: #454545; + padding-left: 5px; + font-size: 12px; + border: 1px solid #cbcbcb; + font-weight: bold; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.puHeaderArea { + position: relative; + height: 40px; +} +.assetsSmmyHeaderArea { + background: #00c73c; + margin-bottom: 7px; +} +.assetsSmmyHeaderArea span { + color: #fff; + font-size: 16px; + padding-left: 10px; + line-height: 40px; +} +.puHeaderArea .close { + position: relative; + top: 5px; + float: right; + color: #fff; + background: url('../images/toolbar/icon-viewclose.png') no-repeat; + text-indent: -9999px; + width: 30px; + height: 30px; +} +.header h3:after { + content: ''; + display: block; + background: #00a0ea; + width: 20px; + height: 3px; + font-size: 0; + margin-top: 5px; +} + +.switch { + position: relative; + display: inline-block; + width: 40px; + height: 22px; + left: 10px; + bottom: 2px; +} +.switch input { + opacity: 0; + width: 0; + height: 0; +} +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: 0.4s; + transition: 0.4s; + border-radius: 34px; +} +.slider:before { + position: absolute; + content: ''; + height: 15px; + width: 16px; + left: 4px; + bottom: 3.55px; + background-color: white; + -webkit-transition: 0.4s; + transition: 0.4s; + border-radius: 50%; +} +input:checked + .slider { + background-color: #7e7d7d; +} +input:focus + .slider { + box-shadow: 0 0 1px #7e7d7d; +} +input:checked + .slider:before { + -webkit-transform: translateX(26px); + -ms-transform: translateX(26px); + transform: translateX(16px); +} +.value { + position: absolute; + top: -11px; + left: -44px; +} + +table.board-table th { + text-align: center; + padding: 4px 0 !important; + border: 1px solid #bdbdbd52; + color: #000000; + font-size: 11px; + width: 50px; + background: #ffffff; +} +table.board-table td { + text-align: center; + padding: 4px 0 !important; + border: 1px solid #f1eded; + color: #000000; + font-size: 11px; + width: 50px; + background: #ffffff; +} +table.board-table thead .red { + background: #ff0000; + color: #ffffff; + font-size: 12px; + text-align: center; + padding: 5px; + font-family: Verdana; +} +table.board-table thead .blue { + background: #2a63dd; + color: #ffffff; + font-size: 12px; + text-align: center; + padding: 5px; + font-family: Verdana; +} +table.board-table thead .green { + background: green; + color: #ffffff; + font-size: 12px; + text-align: center; + padding: 5px; + font-family: Verdana; +} +table.board-table thead .gray { + background: #c0c0c0; + color: #ffffff; + font-size: 12px; + text-align: center; + padding: 5px; + font-family: Verdana; +} +table.board-table tbody th td { + font-size: 13px; + font-weight: bold; + font-family: Verdana; +} + +.ol-custom-popup { + position: absolute; + background-color: #fff; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + padding: 15px; + border-radius: 10px; + border: 1px solid #ccc; + bottom: 12px; + left: -50px; + min-width: 280px; + font-size: 15; +} +.ol-custom-popup:after, +.ol-popup:before { + top: 100%; + border: solid transparent; + content: ' '; + height: 0; + width: 0; + position: absolute; + pointer-events: none; +} +.ol-custom-popup:after { + border-top-color: #fff; + border-width: 10px; + left: 48px; + margin-left: -10px; +} +.ol-custom-popup:before { + border-top-color: #ccc; + border-width: 11px; + left: 48px; + margin-left: -11px; +} +.ol-custom-popup-closer { + text-decoration: none; + position: absolute; + top: 2px; + right: 8px; +} +.ol-custom-popup-closer:after { + content: '✖'; +} +.ol-overviewmap { + width: 150px; + position: absolute; + z-index: 1; + right: 0px; + bottom: 0px; +} +.ol-overviewmap .ol-overviewmap-map { + border: 1px solid #7b98bc; + height: 100px; + margin: 10px; +} +.ol-overviewmap-box { + border: 2px dotted rgba(0, 60, 136, 0.7); +} +.ol-overviewmap button { + display: none; +} +.ol-overviewmap-map canvas { + height: 100%; +} + +.gis-map canvas { + transform: matrix(1, 0, 0, 1, 0, 0) !important; + width: inherit; + height: inherit; +} + +.gis-content .evl-legends{ + bottom: -20px !important; + left: 1350px; + z-index: 101; +} + + +/* cesium-viewer-toolbar 숨기기 */ +.cesium-viewer-toolbar { + display: none; +} + +/* cesium-viewer-animationContainer 숨기기 */ +.cesium-viewer-animationContainer { + display: none; +} +.cesium-viewer-bottom { + display: none; +} +.cesium-viewer-timelineContainer { + display: none; +} +.cesium-credit-logoContainer{ + display: none !important; +} +.cesium-credit-wrapper{ + display: none !important; +} +.cesium-credit-expand-link{ + display: none !important; +} + + diff --git a/packages/common/business_components/src/ui/map/lib/css/gis.edit.css b/packages/common/business_components/src/ui/map/lib/css/gis.edit.css new file mode 100644 index 000000000..c2531e4b0 --- /dev/null +++ b/packages/common/business_components/src/ui/map/lib/css/gis.edit.css @@ -0,0 +1,192 @@ +@charset 'utf-8'; + +.edit_control { + position: absolute; + top: 20px; + left: 610px; +} +.edit_control h3 { + overflow: hidden; + font-size: 0; + line-height: 0; + text-indent: -9999px; +} +.edit_control ul { + float: left; + overflow: hidden; + height: 40px; + margin-left: 10px; + background: #fff; + font-size: 0; + border: 1px solid #9aa7b8; + border-radius: 3px; + box-shadow: 0px 0px 5px #999; + box-sizing: border-box; + padding: 0; + cursor: pointer; +} +.edit_control ul li { + display: inline-block; + width: 40px; + height: 40px; + border-right: 1px solid #eeeeee; +} +.edit_control ul li:last-child { + border-right: 0 none; +} +/*background 삭제*/ +/* .edit_control ul li a {overflow:hidden;width: 40px;height: 40px;background: url(../../images/om/ico_gis_sprite.png) no-repeat;} */ +.edit_control ul li a { + overflow: hidden; + width: 40px; + height: 40px; + display: inline-block; + padding: 0; +} +.edit_control ul li a span { + display: none; + overflow: hidden; + text-indent: -9999px; +} +.edit_control select { + float: left; + min-width: 150px; + height: 40px; + line-height: 40px; + margin-left: 10px; + padding-left: 10px; + font-size: 13px; + color: #666; + border: 1px solid #fff; + border-radius: 3px; + box-shadow: 0px 0px 5px #999; +} + +.edit_control input { + float: left; + min-width: 150px; + height: 40px; + line-height: 40px; + margin-left: 10px; + padding-left: 10px; + font-size: 13px; + color: #666; + border: 1px solid #fff; + border-radius: 3px; + box-shadow: 0px 0px 5px #999; +} + +.edit_control button { + float: left; + min-width: 80px; + height: 40px; + margin-left: 5px; + font-size: 13px; + color: #fff; + text-align: center; + border-radius: 3px; + cursor: pointer; +} +.edit_control button:hover { + background: #1357ce; +} + +.edit_toolbar { + position: absolute; + left: 280px; + top: 52px; +} +.btn_edit_slt { + background: url(../images/toolbar/btn_edit_slt.png) no-repeat center; + height: 40px; +} +.btn_edit_slt:hover { + background: url(../images/toolbar/btn_edit_slt_on.png) no-repeat center; + height: 40px; +} +.btn_edit_slt.active { + background: url(../images/toolbar/btn_edit_slt_on.png) no-repeat center; + height: 40px; +} +.btn_edit_add { + background: url(../images/toolbar/btn_edit_add.png) no-repeat center; + height: 40px; +} +.btn_edit_add:hover { + background: url(../images/toolbar/btn_edit_add_on.png) no-repeat center; + height: 40px; +} +.btn_edit_add.active { + background: url(../images/toolbar/btn_edit_add_on.png) no-repeat center; + height: 40px; +} + +.btn_edit_del { + background: url(../images/toolbar/btn_edit_del.png) no-repeat center; + height: 40px; +} + +.btn_edit_del:hover { + background: url(../images/toolbar/btn_edit_del_on.png) no-repeat center; + height: 40px; +} + +.btn_edit_prev { + background: url(../images/toolbar/btn_edit_prev.png) no-repeat center; + height: 40px; +} + +.btn_edit_prev:hover { + background: url(../images/toolbar/btn_edit_prev_on.png) no-repeat center; + height: 40px; +} + +.btn_edit_next { + background: url(../images/toolbar/btn_edit_next.png) no-repeat center; + height: 40px; +} + +.btn_edit_next:hover { + background: url(../images/toolbar/btn_edit_next_on.png) no-repeat center; + height: 40px; +} + +.btn_edit_snap { + background: url(../images/toolbar/btn_edit_snap.png) no-repeat center; + height: 40px; +} + +.btn_edit_snap:hover { + background: url(../images/toolbar/btn_edit_snap_on.png) no-repeat center; + height: 40px; +} + +.btn_edit_snap.active { + background: url(../images/toolbar/btn_edit_snap_on.png) no-repeat center; + height: 40px; +} + +.btn_edit_save { + background: url(../images/toolbar/btn_edit_save.png) no-repeat center; + height: 40px; +} + +.btn_edit_save:hover { + background: url(../images/toolbar/btn_edit_save_on.png) no-repeat center; + height: 40px; +} + +.btn_edit_transform { + background: url(../images/toolbar/btn_edit_slt.png) no-repeat center; + height: 40px; +} + +.btn_edit_transform:hover { + background: url(../images/toolbar/btn_edit_slt_on.png) no-repeat center; + height: 40px; +} + +.btn_edit_transform.active { + background: url(../images/toolbar/btn_edit_slt_on.png) no-repeat center; + height: 40px; +} diff --git a/packages/common/business_components/src/ui/map/lib/css/gis.roadview.css b/packages/common/business_components/src/ui/map/lib/css/gis.roadview.css new file mode 100644 index 000000000..917d1b778 --- /dev/null +++ b/packages/common/business_components/src/ui/map/lib/css/gis.roadview.css @@ -0,0 +1,7 @@ +@charset 'utf-8'; +/* 로드뷰 관련 */ +.roadview-container {width: 100%; height: 100%; z-index:500; display:block;position: absolute; left: 0px;top: 0px;} +.roadview-container .roadview-container-1p {width: 310px; height: 190px; bottom: 15px; left: 15px; top: auto; border-radius: 2px; overflow: hidden; z-index: 1000; position: absolute;} +.roadview-container .roadview-container-2p {width: 100%; height:100%; float:left; position:relative;} +.roadview-container .roadview {position:absolute; top:0; right:0; left:0; bottom:0; z-index:0; background:#999;} + diff --git a/packages/common/business_components/src/ui/map/lib/css/gis.sketch.css b/packages/common/business_components/src/ui/map/lib/css/gis.sketch.css new file mode 100644 index 000000000..e69c02a7a --- /dev/null +++ b/packages/common/business_components/src/ui/map/lib/css/gis.sketch.css @@ -0,0 +1,206 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, +body, +div, +span, +applet, +object, +iframe, +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote, +pre, +a, +abbr, +acronym, +address, +big, +cite, +code, +del, +dfn, +em, +img, +ins, +kbd, +q, +s, +samp, +small, +strike, +strong, +sub, +sup, +tt, +var, +b, +u, +i, +center, +dl, +dt, +dd, +ol, +ul, +li, +fieldset, +form, +label, +legend, +table, +caption, +tbody, +tfoot, +thead, +tr, +th, +td, +article, +aside, +canvas, +details, +embed, +figure, +figcaption, +footer, +header, +hgroup, +menu, +nav, +output, +ruby, +section, +summary, +time, +mark, +audio, +video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +menu, +nav, +section { + display: block; +} +body { + line-height: 1; +} +ol, +ul { + list-style: none; +} +blockquote, +q { + quotes: none; +} +blockquote:before, +blockquote:after, +q:before, +q:after { + content: ""; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} + +body { + background-color: #e6e6fc; + display: flex; + flex-direction: column; + align-items: center; + padding: 50px 0px; +} + +.canvas { + width: 800px; + height: 500px; + background-color: white; + border-radius: 15px; + box-shadow: 6px 6px 5px grey; +} + +.controls .controls__colors { + display: flex; +} +.controls__colors .controls__color { + width: 50px; + height: 50px; + border-radius: 25px; + margin-right: 5px; + cursor: pointer; + box-shadow: 3px 3px 5px grey; +} + +.jscolor { + color: transparent; + font-size: 0; + line-height: 0; + + width: 50px; + height: 50px; + border-radius: 25px; + border-width: 0px; + margin-right: 5px; + cursor: pointer; + box-shadow: 3px 3px 5px grey; +} + +.controls { + display: flex; + flex-direction: column; + align-items: center; +} + +.controls .controls__btns { + margin-top: 20px; + margin-bottom: 30px; +} + +.controls__btns button:active { + transform: scale(0.98); +} + +.controls__btns button { + all: unset; + cursor: pointer; + background-color: white; + padding: 5px 0px; + width: 80px; + text-align: center; + border-radius: 10px; + box-shadow: 3px 3px 3px grey; + border: 2px solid rgba(0, 0, 0, 0.2); + color: rgba(0, 0, 0, 0.8); + text-transform: uppercase; + font-weight: 600; + font-size: 12px; +} + +.controls .controls__range { + margin-top: 20px; +} diff --git a/packages/common/business_components/src/ui/map/lib/css/gis.tooltip.css b/packages/common/business_components/src/ui/map/lib/css/gis.tooltip.css new file mode 100644 index 000000000..043d842e2 --- /dev/null +++ b/packages/common/business_components/src/ui/map/lib/css/gis.tooltip.css @@ -0,0 +1,35 @@ +@charset 'utf-8'; + +.ol-tooltip { + position: relative; + background: rgba(0, 0, 0, 0.5); + border-radius: 4px; + color: white; + padding: 4px 8px; + opacity: 0.7; + white-space: nowrap; + font-size: 12px; +} +.ol-tooltip-measure { + opacity: 1; + font-weight: bold; +} +.ol-tooltip-static { + background-color: #ffcc33; + color: black; + border: 1px solid white; +} +.ol-tooltip-measure:before, +.ol-tooltip-static:before { + border-top: 6px solid rgba(0, 0, 0, 0.5); + border-right: 6px solid transparent; + border-left: 6px solid transparent; + content: ""; + position: absolute; + bottom: -6px; + margin-left: -7px; + left: 50%; +} +.ol-tooltip-static:before { + border-top-color: #ffcc33; +} \ No newline at end of file diff --git a/packages/common/business_components/src/ui/map/lib/gis.core.js b/packages/common/business_components/src/ui/map/lib/gis.core.js new file mode 100644 index 000000000..faebb90d3 --- /dev/null +++ b/packages/common/business_components/src/ui/map/lib/gis.core.js @@ -0,0 +1,923 @@ +import MousePosition from 'ol/control/MousePosition'; +import ScaleLine from 'ol/control/ScaleLine'; +import Feature from 'ol/Feature'; +import WKT from 'ol/format/WKT'; +import LinearRing from 'ol/geom/LinearRing'; +import Draw from 'ol/interaction/Draw'; +import TileLayer from 'ol/layer/Tile'; +import * as olProj from 'ol/proj'; +import VectorSource from 'ol/source/Vector'; +import XYZ from 'ol/source/XYZ'; +import GeoSJON from 'ol/format/GeoJSON'; +import { Fill, Stroke, Style } from 'ol/style'; +import CircleStyle from 'ol/style/Circle'; +import VectorLayer from 'ol/layer/Vector'; +import { Point } from 'ol/geom'; +import { boundingExtent } from 'ol/extent'; +import olSourceImageWMS from 'ol/source/ImageWMS'; +import { GeoJSON } from 'ol/format'; +import GisOlEditLayer from './gis.edit'; +import GisOlLayer from './gis.layer'; + +const GisApp = {}; +/** + * GisApp 전역변수 생성 + */ +//레이어 정보 +GisApp.layerInfos = []; +//기본정보 +//GisApp.GEOSERVER_URL = 'http://rsone.iptime.org:10010/geoserver/hasudo/ows'; +//GisApp.GEOSERVER_WMS_URL = 'http://rsone.iptime.org:10010/geoserver/hasudo/wms'; +GisApp.GEOSERVER_URL = '/sn/geoserver/hasudo/ows'; +GisApp.GEOSERVER_WMS_URL = '/sn/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}`; +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_DATA_PROJ = 'EPSG:4326'; +GisApp.CONTEXT_LAYER_PATH = '/sn/images/map/layericon/'; +GisApp.layerCode = {}; +GisApp.hlLayerId = 'LightFeatureList'; +GisApp.hLayerId = 'highLightFeature'; +GisApp.wtlkgAnal = 'wtlkgAnal'; + +GisApp.Module = { + core: null, + select: null, + measure: null, + layer: null, + style: null, + edit: null, +}; + +/** + * GisApp.layerInfos Set + */ +GisApp.setLayerInfos = function (layerInfos) { + GisApp.layerInfos = layerInfos; +}; + +/** + * GisApp core + * @requires gis.layer.js + */ +GisApp.core = function (options) { + GisApp.core.prototype.initialize(options); +}; + +GisApp.core.prototype = { + defaults: { + target: '', + centerX: 37.2806094, + centerY: 127.0032502, + zoom: 11, + 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' }, + 14: { zoom: 16, minZoom: 10, maxZoom: 22, centerX: 34.9390499, centerY: 127.6960757, dataProj: 'EPSG:5186' },*/ + }, + epsg_5179: null, + map: null, // ol.Map + view: {}, // ol.view + interactions: {}, // interaction 모음 객체 + selectedFeature: null, // 현재 선택된 feature + baseLayers: {}, + baseMiniMapLayers: {}, + layers: {}, + initialize: function (options) { + let option = this.options['1']; + if (option) { + Object.assign(this.defaults, option); + GisApp.TO_DATA_PROJ = option.dataProj; + } + Object.assign(this.defaults, options); + GisApp.Module.core = this; + GisApp.Module.layer = new GisOlLayer(this.defaults); + GisApp.Module.edit = new GisOlEditLayer(this.defaults); + + 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'); + }, + layerType: { + point: 'POINT', + line: 'LINE', + poligon: 'POLIGON', + }, + chgMngFcltOptions: function (fcltCd) { + let option = this.options[fcltCd]; + Object.assign(this.defaults, option); + this.setCenter(Number(this.defaults.centerX), Number(this.defaults.centerY)); + this.setZoom(15); + }, + getBaseMap: function (_id) { + let url = GisApp.VWORLD_URL; + if (_id === 'satellite') { + url = GisApp.VWORLD_SATELLITE_URL; + } + let baseMap = new TileLayer({ + id: _id, + visible: true, + type: 'base', + source: new XYZ({ + url: url, + crossOrigin: 'anonymous', + }), + zIndex: 0, + }); + return baseMap; + }, + /** + * [사용]테이블명으로 레이어 아이디 조회 + */ + getLayerCodeByTbNm: function (_tableNm) { + let returnLayerCode = null; + for (let i in GisApp.layerCode) { + let layerCode = GisApp.layerCode[i]; + if (layerCode.tableName === _tableNm) { + returnLayerCode = layerCode; + } + } + return returnLayerCode; + }, + /** + * [사용]베이스 레이어 변경 + * Parameters: + * _title - {String} 베이스 레이어 이름 + * + * Returns: + */ + changeBaseLayer: function (_id) { + this.map.getLayers().forEach(function (layer) { + let type = layer.get('type'); + let id = layer.get('id'); + if (type == 'base') { + if (id == _id) { + layer.setVisible(true); + } else { + layer.setVisible(false); + } + } + }); + }, + /** + * [사용]지도 중심좌표 설정 + */ + 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); + const style = new Style({ + image: new CircleStyle({ + radius: 5, + fill: new Fill({ + color: 'rgba(255,0,0,1)', + }), + stroke: new Stroke({ + color: 'rgba(255,0,0,1)', + width: 2, + }), + }), + }); + + // 빨간색 점 추가 (스타일 적용) + let redPoint = new Feature({ + geometry: new Point(center), + }); + + let redPointLayer = new VectorLayer({ + source: new VectorSource({ + features: [redPoint], + }), + style: style, + }); + redPointLayer.set('id', 'redPointLayer'); + // 이전에 만들었던 빨간색 점 레이어 삭제 + this.deleteLayer('redPointLayer'); + + // 새로운 빨간색 점 레이어를 맵에 추가 + this.map.addLayer(redPointLayer); + view.setCenter(center); + } else { + view.setCenter([Number(x), Number(y)]); + } + }, + + /** + * [사용]주소검색 결과 지도이동 + */ + setAddressResult: function (x, y, geom, addressName) { + if (!x || !y) return; + let view = this.map.getView(); + let center = olProj.transform([Number(y), Number(x)], GisApp.FROM_PROJ, GisApp.TO_PROJ); + + const style = [ + new Style({ + image: new CircleStyle({ + radius: 5, + fill: new Fill({ + color: 'rgba(255,0,0,1)', + }), + stroke: new Stroke({ + color: 'rgba(255,0,0,1)', + width: 2, + }), + }), + }), + new Style({ + stroke: new Stroke({ + color: 'blue', + width: 3, + }), + fill: new Fill({ + color: 'rgba(0, 0, 255, 0.1)', + }), + }), + ]; + + // 빨간색 점 추가 (스타일 적용) + let redPoint = new Feature({ + geometry: new Point(center), + }); + let redArea = new WKT().readFeature(geom, { + dataProjection: GisApp.FROM_PROJ, + featureProjection: GisApp.TO_PROJ, + }); + + let redPointLayer = new VectorLayer({ + source: new VectorSource({ + features: [redPoint, redArea], + }), + style: style, + }); + + redPointLayer.set('id', 'redPointLayer'); + // 이전에 만들었던 빨간색 점 레이어 삭제 + this.deleteLayer('redPointLayer'); + + // 새로운 빨간색 점 레이어를 맵에 추가 + this.map.addLayer(redPointLayer); + + view.animate({ center: center, zoom: 19 }); + return redPointLayer; + }, + /* + * [사용]zoom 설정 + */ + setZoom: function (zoom) { + if (!zoom) return; + this.map.getView().setZoom(zoom); + }, + /* + * [사용]zoom 정보 반환 + */ + getZoom: function () { + return this.map.getView().getZoom(); + }, + /** + * [사용]레이어 맵에 추가(외부에서 생성된 layer를 파라미터로 받아 맵에 추가) + */ + setWfsLayerOnMap: function (layer) { + this.deleteLayer(layer.get('id')); + this.map.addLayer(layer); + return layer; + }, + /** + * [사용]레이어 생성(지오서버에서 WFS 방식으로 레이어정보 가져와 레이어 생성) + */ + getWfsLayer: function (layerCodeObj) { + return GisApp.Module.layer.getWfsLayer(layerCodeObj); + }, + /** + * [사용]레이어 맵에 추가(getWfsLayer 사용하여 레이어 생성) + */ + addWfsLayer: function (layerCodeObj) { + let layer = this.getWfsLayer(layerCodeObj); + this.deleteLayer(layer.get('id')); + this.map.addLayer(layer); + return layer; + }, + /** + * [사용]레이어 맵에 추가(getWmsLayer 사용하여 레이어 생성) + */ + addWmsLayer: function (layerCodeObj) { + let layer = GisApp.Module.layer.getWmsLayer(layerCodeObj); + this.deleteLayer(layer.get('id')); + this.map.addLayer(layer); + return layer; + }, + /** + * [사용]레이어 삭제 + */ + deleteLayer: function (layerId) { + const map = this.map; + if (!layerId) return; + map.getLayers().forEach(function (layer) { + let deleteLayer = null; + if (layer != null && layer.get('id') != null) { + let vlayerId = layer.get('id'); + if (vlayerId) { + if (vlayerId === layerId) { + deleteLayer = layer; + } else if (`${vlayerId}`.includes(`${layerId}`)) { + deleteLayer = layer; + } + if (deleteLayer) { + map.removeLayer(deleteLayer); + } + } + } + }); + }, + /** + * [사용]레이어 전체 삭제 + */ + deleteAllLayer: function () { + const map = this.map; + map.getLayers().forEach(function (layer) { + if (layer?.get('type') !== 'base') { + map.removeLayer(layer); + } + }); + //맵에 보이지 않는 레이어 삭제 + GisApp.Module.layer?.layers.forEach(function (layer) { + map.removeLayer(layer); + }); + }, + /** + * [사용]레이어이름으로 레이어 조회 반환 + */ + getLayerById: function (layerId) { + if (!layerId) return; + let layer = null; + this.map.getAllLayers().forEach(function (el) { + if (el != null && el?.get('id') != null) { + let vlayerId = el?.get('id'); + if (Number(vlayerId) === Number(layerId)) { + layer = el; + } + } + }); + if (layer != null) { + GisApp.Module.layer.layers.forEach(function (el) { + if (el != null && el?.get('id') != null) { + let vlayerId = el?.get('id'); + if (Number(vlayerId) === Number(layerId)) { + layer = el; + } + } + }); + } + return layer; + }, + /** + * [사용]오버레이 전체 삭제 + */ + deleteAllOverlay: function () { + let map = this.map; + let overlays = map.getOverlays(); + if (overlays != null) { + let overlayArray = overlays.getArray(); + let overlaySize = overlayArray.length; + if (overlaySize > 0) { + for (let i = overlaySize - 1; i >= 0; i--) { + let overlay = overlayArray[i]; + map.removeOverlay(overlay); + } + } + } + Object.values(GisApp.Module.style.overlays).forEach(overlay => { + if (overlay) { + map.removeOverlay(overlay); + } + }); + GisApp.Module.style.overlays = {}; + }, + /** + * [사용]오버레이 아이디로 삭제 + */ + deleteOverlay: function (_id) { + let map = this.map; + let overlays = map.getOverlays(); + if (overlays != null) { + let overlayArray = overlays.getArray(); + let overlaySize = overlayArray.length; + if (overlaySize > 0) { + for (let i = overlaySize - 1; i >= 0; i--) { + let overlay = overlayArray[i]; + let id = overlay.getId(); + if (id === _id) { + map.removeOverlay(overlay); + delete GisApp.Module.style.overlays[_id]; + } + } + } + } + }, + /** + * [사용]맵 이벤트 삭제 + */ + removeMapEventFnNm: function (handlerNm, fnNm) { + let map = this.map; + for (let i in map.listeners_[handlerNm]) { + let listener = map.listeners_[handlerNm][i]; + if (listener.name == fnNm) { + map.listeners_[handlerNm].splice(i, 1); + } + } + }, + /** + * [사용]위경도 -> 시분초 변환 + */ + coordTrans: function (coord) { + let str = String(coord); /* String 변환 */ + let split = str.split('.'); /* 소수점 split */ + let hour = split[0]; /* 시간 */ + let minute = parseFloat((coord - parseInt(split[0]).toFixed(6)) * 60).toFixed(6); + let min = String(minute).split('.')[0]; + let second = parseFloat((minute - parseInt(min)) * 60).toFixed(2); + return hour + '도 ' + min + '분 ' + second + '초'; + }, + /** + * [사용]시분초 -> 위경도 변환 + */ + coordTransrate: function (degree, minute, second) { + let min = (Number(minute) / 60).toFixed(2); + let sec = (Number(second) / 3600).toFixed(4); + return degree + Number(min) + Number(sec); + }, + /** + * [사용]마우스 좌표 위치 + */ + mousePositionControl: function () { + let element = document.getElementById('label_lonlat'); + let me = this; + let mousePosition = new MousePosition({ + coordinateFormat: function (coordinate) { + let text = ''; + if (element) { + let lng = coordinate[0]; + let lat = coordinate[1]; + text += '경도 : ' + lng.toFixed(7) + ' ( ' + me.coordTrans(lng) + ' ) '; + text += '위도 : ' + lat.toFixed(7) + '( ' + me.coordTrans(lat) + ' )'; + } + return text; + }, + projection: GisApp.FROM_PROJ, + className: 'custom-mouse-position', + target: document.getElementById('label_lonlat'), + }); + this.map.addControl(mousePosition); + }, + /** + * [사용]스케일 표시 + */ + scaleControl: function () { + let scale = new ScaleLine({ + minWidth: 100, + units: 'metric', + }); + this.map.addControl(scale); + }, + /** + * [사용]레이어 맵에 셋팅 + * + * Parameters: + * layerList - {Array} [layerNm : GisApp.layerCode 키] + * visible - {Boolean} 레이어 visible 여부 + * + * Returns: + */ + setLayerOnMap: function (layerIds, visible, addQurryFlter) { + for (let i in layerIds) { + let id = layerIds[i]; + let layer = this.getLayerById(id); + if (layer) { + layer.setVisible(visible); + } else if (visible) { + const layerCode = GisApp.layerCode[id]; + const queryFlter = layerCode?.flterSql || '' + ' ' + (addQurryFlter || ''); + layerCode['flterSql'] = queryFlter; + //layer = this.addWmsLayer(layerCode); + layer = this.addWfsLayer(layerCode); + } + } + if (GisApp.Module.select) { + this.setSelectInteraction(); + } + if (GisApp.Module.edit.snap) { + GisApp.Module.edit.setSnap(); + } + }, + /** + * [사용]shp 파일 다운로드 + */ + getShapeLayer: function (option) { + GisApp.Module.layer.getShapeLayer(option); + }, + /** + * [사용]레이어 셀렉트 가능하게 셋팅 + */ + setSelectInteraction: function () { + let layers = this.map.getLayers(); + if (layers) { + for (let i in layers.array_) { + let layer = layers.array_[i]; + if (layer.get('chcYn') === 'Y') { + GisApp.Module.select.addLayer(layer); + } + } + } + }, + /** + * [사용]layerInfos(array) To layerCode(Object) + */ + convertLayerInfosToLayerCode: function (array, key, parentName) { + let initialValue = {}; + + for (let i = 0; i < array.length; i++) { + let item = {}; + Object.assign(item, array[i]); + if (parentName) { + item.parentName = parentName; + } + initialValue[item[key]] = item; + + if (item.children && item.children.length > 0) { + let child = this.convertLayerInfosToLayerCode(item.children, key, item.name); + Object.assign(initialValue, child); + } + if (initialValue[item[key]].children) { + delete initialValue[item[key]].children; + } + } + return initialValue; + }, + /** + * [사용]피처 좌표값 WKT포맷으로 변경 + */ + convertFeatureGeomToWKT: function (feature) { + let geometry = feature.getGeometry(); + const wktGeom = this.convertGeomToWkt(geometry); + return wktGeom; + }, + /** + * [사용]WKT포맷 Geometry로 변경 + */ + convertWktToGeom: function (wtkGeom) { + let format = new WKT(); + let geom = format.readGeometry(wtkGeom); + return geom; + }, + /** + * [사용]좌표와 properties의 키값으로 피처 검색 + */ + getFeatureGeomProp: function (geometry, ftrIds, ftrCde) { + let returnFeature; + this.map.getLayers().forEach(function (layer) { + let type = layer.get('type'); + if (type != 'base') { + let source = layer.getSource(); + + let type = geometry.getType(); + let coordinates = geometry.getCoordinates(); + if (type.toUpperCase() === 'POINT') { + coordinates = [coordinates]; + } else { + coordinates = coordinates[0]; + } + let featureFtrIds; + let featureFtrCde; + for (let i in coordinates) { + let coordinate = coordinates[i]; + let feature = source.getClosestFeatureToCoordinate(coordinate); + if (feature) { + let properties = feature.getProperties(); + featureFtrIds = properties['FTR_IDS']; + featureFtrCde = properties['FTR_CDE']; + if (ftrIds == featureFtrIds && ftrCde == featureFtrCde) { + returnFeature = feature; + } + } + } + } + }); + return returnFeature; + }, + /** + * [사용]관리번호로 피처검색 + */ + getFeatureByFtrIdn: function (typeName, ftrIdn) { + let returnFeature; + this.map.getLayers().forEach(function (layer) { + let layerTypeName = layer.get('typeName'); + if (layerTypeName === typeName) { + layer + .getSource() + .getFeatures() + .forEach(feature => { + let featureFtrIdn = feature.get('ftrIdn'); + if (featureFtrIdn === ftrIdn) { + returnFeature = feature; + } + }); + } + }); + return returnFeature; + }, + /** + * [사용]맵 이벤트 추가 + */ + addMapEvent: function (evtNm, id, callback) { + if (id) { + let evt = this.map.on(evtNm, callback); + evt.listener.id = id; + } + }, + /** + * [사용]맵 이벤트 삭제 + */ + removeMapEvent: function (evtNm, id) { + if (id) { + let listeners = this.map.listeners_[evtNm]; + for (let i in listeners) { + let listenter = listeners[i]; + if (listenter.id == id) { + listeners.splice(i, 1); + } + } + } + }, + /** + * [사용]마커 그리기 추가 + */ + drawMarker: function (layer, callback) { + let map = this.map; + let drawSource = layer.getSource(); + let draw = new Draw({ + type: 'Point', + source: drawSource, + condition: function (evt) { + return true; + }, + }); + map.addInteraction(draw); + draw.on('drawend', function (event) { + callback(event); + map.removeInteraction(draw); + }); + }, + /** + * [사용]피처 삭제 + */ + deleteFeature: function (layerId, featureId) { + if (!layerId || !featureId) return; + let layer = this.getLayerById(layerId); + if (layer != null) { + let feature = layer.getSource().getFeatureById(featureId); + if (feature != null) { + layer.getSource().removeFeature(feature); + } + } + }, + /** + * [사용]레이어 + */ + fitExtent: function (extent, zoom) { + let map = this.map; + let mapSize = map.getSize(); + if (!zoom) { + zoom = this.getZoom(); + } + if (extent) { + //map.getView().fit(extent, { size: [mapSize[0], mapSize[1]] }); + this.map.getView().fit(extent, { + maxZoom: 18, // 너무 확대되지 않도록 최대 줌 제한 + duration: 500, // 부드러운 애니메이션 효과 (밀리초 단위) + }); + } + this.setZoom(zoom); + }, + getGeomLineOfFeature: function (feature) { + if (!feature) { + return; + } + let geometry = feature.getGeometry(); + let type = geometry.getType(); + if (!(type == 'LineString' || type == 'MultiLineString')) { + return; + } + let geomLienString = geometry; + if (type != 'LineString') { + //lienString = geometry.getLineString(); + } + return geomLienString; + }, + getCenterGeomLineOfFeature: function (feature) { + if (!feature) { + return; + } + let center = []; + let geomLienString = this.getGeomLineOfFeature(feature); + let coords = geomLienString.getCoordinates(); + let coordCnt = coords.length; + if (coordCnt == 1) { + coords = coords[0]; + } + //라인 한개 + if (coordCnt == 2) { + return geomLienString; + } + //멀티라인 + let centerCnt = Math.floor(coordCnt / 2); + let lineCoordinates = []; + lineCoordinates.push(coords[centerCnt - 1]); + lineCoordinates.push(coords[centerCnt]); + geomLienString = new LinearRing(lineCoordinates); + return geomLienString; + }, + getCenterOfLineGeom: function (geom) { + let center = []; + let extent = geom.getExtent(); + let x = extent[0] + (extent[2] - extent[0]) / 2; + let y = extent[1] + (extent[3] - extent[1]) / 2; + center.push(x); + center.push(y); + return center; + }, + /** + * 1 -> 01 두자리수로 변환 + */ + fillZeroToZoomLevel: function (n, digits) { + let zero = ''; + n = n.toString(); + if (digits > n.length) { + for (let i = 0; digits - n.length > i; i++) { + zero += '0'; + } + } + return zero + n; + }, + convertGeoJsonToFeature: function (data, dataProjection) { + let format = new GeoSJON(); + let feature = format.readFeature(data, { + dataProjection: dataProjection || GisApp.TO_DATA_PROJ, + featureProjection: GisApp.TO_PROJ, + }); + return feature; + }, + convertGeoJsonToFeatures: function (data, dataProjection) { + let format = new GeoSJON(); + const features = []; + for (let obj of data) { + let feature = null; + try { + feature = format.readFeature(obj, { + dataProjection: dataProjection || GisApp.TO_DATA_PROJ, + featureProjection: GisApp.TO_PROJ, + }); + } catch (error) { + console.log(obj); + } + features.push(feature); + } + return features; + }, + + /** + * [사용]WKT포맷 Geometry로 feature 생성 + */ + convertWktToFeature: function (wtkGeom, dataProjection) { + let format = new WKT(); + let feature = format.readFeature(wtkGeom, { + dataProjection: dataProjection || GisApp.TO_DATA_PROJ, + featureProjection: GisApp.TO_PROJ, + }); + return feature; + }, + + /** + * [사용]geometry를 WKT포맷으로 변경 + */ + convertGeomToWkt: function (geometry) { + let format = new WKT(); + let wktGeom = format.writeGeometry(geometry, { + dataProjection: GisApp.TO_DATA_PROJ, + featureProjection: GisApp.TO_PROJ, + }); + return wktGeom; + }, + /** + * [사용]레이어 전체 표시 컨트롤 + */ + setAllLayersOnMap: function (visible) { + this.map.getLayers().forEach(function (el) { + if (el !== null && el.get('id') !== null) { + let layerId = el.get('id'); + if ('base' !== layerId && 'satellite' !== layerId && 'minimap-base' !== layerId) { + el.setVisible(visible); + } + } + }); + }, + fitExtentZoomFeatures: function (features, zoom, paddingX = 0, paddingY = 0) { + if (!features.length) return; + // Feature들의 경계를 계산하여 extent 생성 + const extent = this.getExtentFeatures(features); + if (zoom) { + //const paddingX = 0; // X축 이동량 (양수는 오른쪽, 음수는 왼쪽) + //const paddingY = -10000; // Y축 이동량 (양수는 아래쪽, 음수는 위쪽) + extent[0] -= paddingX; + extent[1] -= paddingY; + this.fitExtent(extent, zoom); + } else { + this.map.getView().fit(extent, { + maxZoom: 18, // 너무 확대되지 않도록 최대 줌 제한 + duration: 500, // 부드러운 애니메이션 효과 (밀리초 단위) + }); + } + }, + getExtentFeatures: function (features) { + if (!features.length) return; + let extent = []; + const allCoordinates = features + .map(feature => { + const geometry = feature.getGeometry(); + if (!geometry || typeof geometry.getCoordinates !== 'function') { + return null; // geometry가 없거나 getCoordinates() 메서드가 없으면 null 반환 + } + return geometry.getCoordinates(); + }) + .filter(coords => coords !== null); + function extractCoordinatePairs(arr) { + let result = []; + + function recursiveFlatten(subArray) { + if (!Array.isArray(subArray)) return; + + for (let item of subArray) { + if (Array.isArray(item) && item.length === 2 && typeof item[0] === 'number' && typeof item[1] === 'number') { + // [숫자, 숫자] 형태의 배열이면 추가 + result.push(item); + } else { + // 그렇지 않다면 재귀 호출 + recursiveFlatten(item); + } + } + } + + recursiveFlatten(arr); + return result; + } + + const flattenCoord = extractCoordinatePairs(allCoordinates); + extent = boundingExtent(flattenCoord); + return extent; + }, +}; + +/** + * [사용]피처 레이어 검색 + */ +Feature.prototype.getLayer = function () { + let map = GisApp.Module.core.map; + let this_ = this, + layer_; + let sameFeature = function (feature) { + return this_ === feature ? true : false; + }; + map.getLayers().forEach(function (layer) { + let source = layer.getSource(); + if (source instanceof VectorSource) { + let features = source.getFeatures(); + if (features.length > 0) { + let found = features.some(sameFeature); + if (found) { + layer_ = layer; + } + } + } + }); + return layer_; +}; + +export default GisApp; diff --git a/packages/common/business_components/src/ui/map/lib/gis.edit.js b/packages/common/business_components/src/ui/map/lib/gis.edit.js new file mode 100644 index 000000000..ce3fae555 --- /dev/null +++ b/packages/common/business_components/src/ui/map/lib/gis.edit.js @@ -0,0 +1,521 @@ +import GisApp from './gis.core'; +import Draw from 'ol/interaction/Draw.js'; +import Modify from 'ol/interaction/Modify.js'; +import Collection from 'ol/Collection.js'; +import Snap from 'ol/interaction/Snap.js'; +import { click, always } from 'ol/events/condition'; +import UndoRedo from 'ol-ext/interaction/UndoRedo.js'; +import Transform from 'ol-ext/interaction/Transform.js'; +import { gAlert } from './util'; +import VectorSource from 'ol/source/Vector.js'; +import VectorLayer from 'ol/layer/Vector'; + +const GisOlEditLayer = function (options) { + this.initialize(options); +}; + +GisOlEditLayer.prototype = { + defaults: {}, + editFlag: false, + draw: null, + modify: null, + snap: null, + undoredo: null, + transform: null, + drawType: { + point: 'Point', + line: 'MultiLineString', + poligon: 'MultiPolygon', + }, + selectedLayerId: null, //셀렉트 박스에서 선택된 레이어 ID + targetElement: null, + editFeatureInfo: {}, + addFeatureNo: 1, + initialize: function (options) { + Object.assign(this.defaults, options); + }, + /** + * [공통] Interaction 추가 + */ + addInteration: function () { + this.setUndoInteraction(); + }, + /** + * [공통] Interaction 삭제 + */ + removeInteraction: function () { + this.removeDraw(); + this.removeModify(); + this.removeSnap(); + this.clearUndoRedoFeature(); + }, + /** + * [공통] Interaction Active 컨트롤 + */ + setInteractionActive: function (active) { + this.setDrawActive(active); + this.setModifyActive(active); + this.setActiveUndo(active); + }, + /** + * [그리기] 기능 추가 + */ + setDraw: function (lyrId) { + if (!lyrId) return '레이어를 선택해주십시오.'; + const core = GisApp.Module.core; + const layer = core.getLayerById(lyrId); + const layerCode = GisApp.layerCode[lyrId]; + if (!layer) return '트리에서 레이어 체크 후 추가 가능합니다.'; + const lyrTy = layerCode.gobjTy; + + //초기화 + this.removeModify(); + this.removeTransform(); + if (GisApp.Module.select) { + GisApp.Module.select.setActive(false); + } + + const originalSource = layer.getSource(); + const drawSource = new VectorSource({ + features: originalSource.getFeatures(), // 기존 feature들 복사 + }); + + // 새로운 임시 레이어 추가 (draw 전용) + const drawLayer = new VectorLayer({ + source: drawSource, + minZoom: 1, // 이 레이어만 minZoom을 1로 설정 + style: layer.getStyle(), + }); + core.map.addLayer(drawLayer); + + //그리기 기능 추가 + const me = this; + + if (me.draw) { + core.map.removeInteraction(me.draw); + } + const drawType = this.drawType[lyrTy]; + this.draw = new Draw({ + type: drawType, + source: drawSource, + condition: function (evt) { + return true; + }, + }); + core.map.addInteraction(this.draw); + + //그리기 기능 이벤트 + this.draw.on('drawstart', function (event) {}); + + const drawFeatureCallback = function (event) { + me.setActiveUndo(true); + const drawFeature = event.feature; + drawFeature.setId(lyrId + '_feature_' + me.addFeatureNo++); + + const properties = {}; + properties['lyr_id'] = lyrId; + drawFeature.setProperties(properties); + + me.setAddFeatureUndo(layer, drawFeature); + me.setActiveUndo(false); + }; + + this.draw.on('drawend', function (event) { + drawFeatureCallback(event); + }); + + /*drawSource.listeners_.addfeature = null; + let addfeatureCallback = function(event){ + } + drawSource.on('addfeature', addfeatureCallback);*/ + }, + /** + * [그리기] 기능 제거 + */ + removeDraw: function () { + let core = GisApp.Module.core; + core.map.removeInteraction(this.draw); + if (GisApp.Module.select) { + GisApp.Module.select.setActive(true); + } + }, + /** + * [그리기] 기능 활성화 + */ + setDrawActive: function (active) { + if (this.draw) { + this.draw.setActive(active); + if (!active) { + /* $('#selectEditLayer').css('display', 'none'); + $('.btn_edit_add').removeClass('active'); */ + } + } + }, + /** + * [히스토리] UndoInteraction 추가 + */ + setUndoInteraction: function () { + let core = GisApp.Module.core; + if (!this.undoredo) { + this.undoredo = new UndoRedo(); + core.map.addInteraction(this.undoredo); + } + }, + /** + * [히스토리] UndoInteraction 제거 + */ + removeUndoInteraction: function () { + let core = GisApp.Module.core; + if (this.undoredo) { + core.map.removeInteraction(this.undoredo); + this.undoredo = null; + } + }, + /** + * [히스토리] UndoInteraction 활성화 + */ + setActiveUndo: function (active) { + if (this.undoredo) { + this.undoredo.setActive(active); + } + }, + /** + * [히스토리] 피처 추가 + */ + setAddFeatureUndo: function (layer, addFeature) { + if (this.undoredo) { + let obj = {}; + obj.target = layer.getSource(); + obj.feature = addFeature; + obj.type = 'addfeature'; + obj.feature.set('uploadType', 'addFeature'); + this.undoredo._onAddRemove(obj); + } + }, + /** + * [히스토리] 피처 삭제 + */ + setRemoveFeatureUndo: function (layer, removeFeature) { + if (this.undoredo) { + let obj = {}; + obj.target = layer.getSource(); + obj.feature = removeFeature; + obj.type = 'removefeature'; + if (obj.feature.get('uploadType') !== 'addFeature') { + obj.feature.set('uploadType', 'removeFeature'); + } + this.undoredo._onAddRemove(obj); + } + }, + /** + * [히스토리] 이전 + */ + undo: function () { + if (this.undoredo) { + if (this.undoredo.hasUndo() === 0) { + gAlert('INFO', 'NO_PREV_HISTORY'); + return; + } + this.setActiveUndo(true); + this.undoredo.undo(); + this.setActiveUndo(false); + } + }, + /** + * [히스토리] 다음 + */ + redo: function () { + if (this.undoredo) { + if (this.undoredo.hasRedo() === 0) { + gAlert('INFO', 'NO_NEXT_HISTORY'); + return; + } + this.setActiveUndo(true); + this.undoredo.redo(); + this.setActiveUndo(false); + } + }, + /** + * [히스토리] 초기화 + */ + clearUndoRedo: function () { + if (this.undoredo) { + const undoStack = this.undoredo._undoStack.array_; + for (let i in undoStack) { + const undo = undoStack[i]; + if (undo.feature) { + undo.feature.set('uploadType', null); + } + } + this.undoredo.clear(); + } + }, + /** + * [히스토리] 피처 초기화 + */ + clearUndoRedoFeature: function () { + if (this.undoredo) { + this.setActiveUndo(true); + const undoStack = [...this.undoredo._undoStack.array_]; + for (let i in undoStack) { + const undo = undoStack[i]; + if (undo.feature) { + this.undoredo.undo(); + } + } + this.setActiveUndo(false); + this.undoredo.clear(); + } + }, + /** + * [삭제] 피처 삭제 + */ + deleteFeature: function () { + //초기화 + this.removeDraw(); + this.removeModify(); + this.removeTransform(); + + //삭제 기능 실행 + let me = this; + let core = GisApp.Module.core; + let select = GisApp.Module.select; + select.getSelectedFeatures().forEach(function (selectedFeature) { + let selectedFeatureId = selectedFeature.getId(); + let layers = core.map.getLayers(); + for (let i in layers.array_) { + let layer = layers.array_[i]; + if (layer.get('mdfcnYn') === 'Y' && select.getVisible(layer)) { + layer + .getSource() + .getFeatures() + .forEach(function (feature) { + let featureId = feature.getId(); + if (selectedFeatureId === featureId) { + me.setActiveUndo(true); + feature.setProperties({ lyr_id: layer.get('id') }); + layer.getSource().removeFeature(feature); + me.setRemoveFeatureUndo(layer, feature); + me.setActiveUndo(false); + } + }); + } + } + }); + }, + /** + * [수정] 피처 수정 기능 셋팅 + */ + setModify: function () { + //초기화 + this.removeDraw(); + this.removeTransform(); + + //수정기능 추가 + let me = this; + let core = GisApp.Module.core; + let select = GisApp.Module.select; + let selectedFeatures = select.getSelectedFeatures(); + + this.modify = new Modify({ + features: selectedFeatures, + }); + core.map.addInteraction(this.modify); + + select.selectInteraction.on('change:active', function () { + selectedFeatures.forEach(function (each) { + selectedFeatures.remove(each); + }); + }); + + this.modify.on('modifystart', function (event) {}); + + this.modify.on('modifyend', function (event) { + let features = event.features.getArray(); + for (let i = 0; i < features.length; i++) { + let feature = features[i]; + let layer = select.selectInteraction.getLayer(feature); + feature.setProperties({ lyr_id: layer.get('id') }); + } + }); + }, + /** + * [수정] 피처 수정 기능 제거 + */ + removeModify: function () { + if (this.modify) { + let core = GisApp.Module.core; + core.map.removeInteraction(this.modify); + this.modify = null; + } + }, + /** + * [수정] 기능 활성화 + */ + setModifyActive: function (active) { + if (this.modify) { + this.modify.setActive(active); + } + }, + /** + * [크기조절] 피처 크기조절 기능 셋팅 + */ + setTransform: function () { + //초기화 + this.removeDraw(); + this.removeModify(); + + let me = this; + let core = GisApp.Module.core; + let select = GisApp.Module.select; + + this.transform = new Transform({ + enableRotatedTransform: false, + addCondition: click, + filter: function (f, l) { + return f.getGeometry().getType() !== 'Point'; + }, + // layers: [vector], + hitTolerance: select.defaults.hitTolerance, + translateFeature: true, + scale: true, + rotate: true, + keepAspectRatio: always, + translate: true, + stretch: true, + }); + this.transform.on(['rotateend', 'translateend', 'scaleend'], function (event) { + let features = event.features.getArray(); + for (let i = 0; i < features.length; i++) { + let feature = features[i]; + let layer = feature.getLayer(); + feature.setProperties({ lyr_id: layer.get('id') }); + } + }); + + core.map.addInteraction(this.transform); + }, + /** + * [크기조절] 피처 크기조절 기능 제거 + */ + removeTransform: function () { + if (this.transform) { + let core = GisApp.Module.core; + core.map.removeInteraction(this.transform); + this.transform = null; + } + }, + /** + * [크기조절] 피처 크기조절 기능 활성화 + */ + setTransformActive: function (active) { + if (this.transform) { + this.transform.setActive(active); + } + }, + /** + * [스냅] 기능 추가 + */ + setSnap: function (active) { + //초기화 + this.removeSnap(); + + //스냅 기능 추가 + let core = GisApp.Module.core; + let select = GisApp.Module.select; + core.map.removeInteraction(this.snap); + let features = []; + let layers = core.map.getLayers(); + for (let i in layers.array_) { + let layer = layers.array_[i]; + if (select.getVisible(layer) && layer.get('type') !== 'base') { + if (layer.getSource().getFeatures().length > 0) { + features = features.concat(layer.getSource().getFeatures()); + } + } + } + let snapCollection = new Collection(features); + this.snap = new Snap({ + features: snapCollection, + pixelTolerance: 30, + }); + core.map.addInteraction(this.snap); + }, + /** + * [스냅] 기능 제거 + */ + removeSnap: function () { + if (this.snap) { + let core = GisApp.Module.core; + core.map.removeInteraction(this.snap); + this.snap = null; + } + }, + /** + * [스냅] 스냅에 피처 추가 + */ + addSnap: function (layers) { + let me = this; + if (me.snap) { + for (let i in layers) { + let source = layers[i].getSource(); + source.listeners_.addfeature = null; + source.on('addfeature', function (event) { + me.snap.addFeature(event.feature); + }); + } + } + }, + /** + * [스냅] 기능 활성화 + */ + setSnapActive: function (active) { + if (this.snap) { + this.snap.setActive(active); + } + }, + + /** + * [저장] 피처 수정 정보 셋팅 + */ + setEditFeatureInfo: function (flag, feature) { + const featureId = feature.getId(); + const properties = feature.getProperties(); + + //신규 추가 후 삭제시에 editFeatureInfo에서 제거 + if (properties.uploadType === 'addFeature' && flag === 'delete') { + delete this.editFeatureInfo[featureId]; + return; + } + + //좌표값 WKT포멧으로 변경 + const pGeom = GisApp.Module.core.convertFeatureGeomToWKT(feature); + const param = { + flag, + ...properties, + geom: pGeom, + }; + this.editFeatureInfo[featureId] = param; + }, + getEditFeatureInfo: function () { + const undoStack = this.undoredo._undoStack.array_; + this.editFeatureInfo = {}; + for (let i in undoStack) { + const undo = undoStack[i]; + if (undo.feature) { + const feature = undo.feature; + const properties = feature.getProperties(); + if (properties.uploadType === 'addFeature') { + this.setEditFeatureInfo('insert', feature); + } else if (properties.uploadType === 'removeFeature') { + this.setEditFeatureInfo('delete', feature); + } else if (undo.type === 'changegeometry') { + this.setEditFeatureInfo('update', feature); + } + } + } + return this.editFeatureInfo; + }, +}; + +export default GisOlEditLayer; diff --git a/packages/common/business_components/src/ui/map/lib/gis.layer.js b/packages/common/business_components/src/ui/map/lib/gis.layer.js new file mode 100644 index 000000000..3e0f6d1a1 --- /dev/null +++ b/packages/common/business_components/src/ui/map/lib/gis.layer.js @@ -0,0 +1,557 @@ +import Feature from 'ol/Feature.js'; +import GeoJSON from 'ol/format/GeoJSON.js'; +import Point from 'ol/geom/Point.js'; +import VectorLayer from 'ol/layer/Vector'; +import VectorImageLayer from 'ol/layer/VectorImage.js'; +import { all, bbox } from 'ol/loadingstrategy'; +import { transform } from 'ol/proj'; +import Cluster from 'ol/source/Cluster.js'; +import VectorSource from 'ol/source/Vector.js'; +import VectorTile from 'ol/source/VectorTile.js'; +import CircleStyle from 'ol/style/Circle.js'; +import Fill from 'ol/style/Fill.js'; +import Icon from 'ol/style/Icon.js'; +import Stroke from 'ol/style/Stroke.js'; +import Style from 'ol/style/Style.js'; +import Text from 'ol/style/Text.js'; +import { createXYZ } from 'ol/tilegrid'; +import GisApp from './gis.core.js'; +import GisOlStyle from './gis.style.js'; +import Tile from 'ol/layer/Tile.js'; +import TileWMS from 'ol/source/TileWMS.js'; +import gisCore from './gis.core.js'; + +/** + * GisApp.layer + * @requires gis.core.js + * @requires gis.style.js + */ +const GisOlLayer = function (options) { + this.initialize(options); +}; + +GisOlLayer.prototype = { + layers: [], + cesiumLayers: [], + defaults: {}, + storageNm: 'hasudo:', + defaultLayerOptions: { + name: '', + zIndex: 2, + opacity: 1, + flterSql: null, + minZoom: '15', + }, + initialize: function (options) { + Object.assign(this.defaults, options); + this.gisStyle = new GisOlStyle(); + GisApp.Module.style = this.gisStyle; + }, + gisStyle: null, + /** + * [사용]빈 레이어 생성 + */ + makeLayer: function (options) { + let layerOptions = { + ...this.defaultLayerOptions, + ...options, + minZoom: options?.minViewSize ? options?.minViewSize : options?.minZoom, + zIndex: options?.drawOrdr ? options?.drawOrdr : 1, + }; + let returnLayer = new VectorImageLayer(layerOptions); + this.layers.push(returnLayer); + return returnLayer; + }, + /** + * gisStyle 반환 + */ + getGisStyle: function () { + return this.gisStyle; + }, + /** + * [사용]WFS 벡터 소스 반환 + */ + getVectorSourceGeoserver: function (options) { + const url = GisApp.GEOSERVER_URL; + const toProj = GisApp.TO_PROJ; + const lyrTyNm = options.lyrTyNm; + const params = {}; + params['TYPENAME'] = this.storageNm + lyrTyNm; + params['SERVICE'] = 'WFS'; + params['VERSION'] = '1.1.0'; + params['REQUEST'] = 'GetFeature'; + params['SRS'] = toProj; + params['OUTPUTFORMAT'] = 'application/json'; + if (options?.flterSql && options?.flterSql?.trim() !== 'null') { + params['CQL_FILTER'] = options?.flterSql; + } + const strParams = new URLSearchParams(params).toString(); + let source = new VectorSource({ + format: new GeoJSON(), + url: function (extent) { + return url + '?' + strParams; + }, + strategy: all, + }); + return source; + }, + /** + * [사용]WFS 벡터 소스 반환 + */ + getVectorTileSource: function (options) { + let url = GisApp.GEOSERVER_URL; + let toProj = GisApp.TO_PROJ; + let params = {}; + params['TYPENAME'] = this.storageNm + options.typeName; + params['SERVICE'] = 'WFS'; + params['VERSION'] = '1.1.0'; + params['REQUEST'] = 'GetFeature'; + params['SRS'] = toProj; + params['OUTPUTFORMAT'] = 'application/json'; + let strParams = new URLSearchParams(params).toString(); + + let source = new VectorTile({ + format: new GeoJSON(), + + tileUrlFunction: function (tileCoord, pixelRatio, projection) { + let filterStr = '&CQL_FILTER=bbox(GEOM,' + source.getTileGrid().getTileCoordExtent(tileCoord).join(',') + ",'" + projection.getCode() + "')"; + if (options.flterSql) { + filterStr += ' AND ' + options.flterSql; + } + let paramWithUrl = url + '?' + strParams + filterStr; + return paramWithUrl; + }, + tileGrid: createXYZ, + }); + + return source; + }, + /** + * [사용]WFS 레이어 생성 + */ + getWfsLayer: function (option) { + option = option || {}; + //레이어 생성 + let returnLayer = this.makeLayer(option); + //레이어 소스 생성 + let source = this.getVectorSourceGeoserver(option); + returnLayer.setSource(source); + //스타일 추가 + returnLayer.setStyle(this.gisStyle.getLayerStyleFunction(returnLayer, option)); + returnLayer.set('id', option.id + ''); + return returnLayer; + }, + /** + * [사용]WMS 레이어 생성 + */ + getWmsLayer: function (options) { + let layerOptions = Object.assign({}, this.defaultLayerOptions, options); + let url = GisApp.GEOSERVER_WMS_URL; + let toProj = GisApp.TO_PROJ; + const lyrTyNm = options.lyrTyNm; + let params = {}; + params['LAYERS'] = this.storageNm + lyrTyNm; + params['SERVICE'] = 'WMS'; + params['VERSION'] = '1.1.1'; + params['REQUEST'] = 'GetMap'; + params['SRS'] = toProj; + params['FORMAT'] = 'image/png'; + params['TILED'] = true; + const wmsLayer = new Tile({ + ...layerOptions, + id: layerOptions?.id, + source: new TileWMS({ + url, + params, + serverType: 'geoserver', + }), + }); + wmsLayer.setZIndex(options.draw0rdr); + wmsLayer.setMinZoom(options.minViewSize); + this.layers.push(wmsLayer); + return wmsLayer; + }, + /** + * [사용]WFS 레이어 생성 + */ + getWfsEmptyLayer: function (option) { + option = option || {}; + //레이어 생성 + let returnLayer = this.makeLayer(option); + let vectorSource = new VectorSource({ + strategy: all, + }); + returnLayer.setSource(vectorSource); + //스타일 추가 + returnLayer.setStyle(this.gisStyle.getLayerStyle(returnLayer, option)); + returnLayer.set('id', option.id + ''); + let map = GisApp.Module.core.map; + map.addLayer(returnLayer); + return returnLayer; + }, + /** + * 피처 리스트로 레이어 생성 + */ + getLayerByFeature: function (feature, layerOptions, layerStyle) { + if (!layerOptions || !layerOptions.id) { + return null; + } + let vectorSource = new VectorSource({ + strategy: all, + }); + if (feature) { + if (Object.prototype.toString.call(feature).slice(8, -1).indexOf('Object') > -1) { + vectorSource.addFeature(feature); + } else { + vectorSource.addFeatures(feature); + } + } + GisApp.Module.core.deleteLayer(layerOptions.id); + layerOptions.source = vectorSource; + let layer = new VectorImageLayer(layerOptions); + layer.setStyle(layerStyle); + layer.set('id', layerOptions.id + ''); + return layer; + }, + /** + * 피처 하이라이트 + */ + highLightFeature: function (hlFeature, layerOptions, styleOptions, customStyleFunction) { + const color = styleOptions.color || 'rgba(135, 116, 243, 0.4)'; + const width = styleOptions.width || 10; + const featureKey = styleOptions.featureKey || ''; + const fitExtent = layerOptions.fitExtent || 'true'; + + const styleFunction = function (feature, resolution) { + const featureStyle = feature.getStyle(); + if (featureStyle) return null; + const style = new Style({ + image: new CircleStyle({ + fill: new Fill({ + color: color, + }), + radius: width, + }), + stroke: new Stroke({ + color: color, + width: width, + }), + }); + + const getFeatureText = function (feature) { + if (featureKey) { + let text = feature.get(featureKey); + if (text) return text; + } + return ''; + }; + const text = new Text({ + stroke: new Stroke({ + color: 'white', + width: 2, + }), + fill: new Fill({ + color: 'black', + }), + font: '12px Verdana', + text: getFeatureText(feature), + }); + style.setText(text); + return style; + }; + let actualStyleFunction = customStyleFunction || styleFunction; + GisApp.Module.core.deleteLayer(layerOptions.id); + let layer = this.getLayerByFeature(hlFeature, layerOptions, actualStyleFunction); + if (hlFeature && hlFeature.length > 0) { + let map = GisApp.Module.core.map; + map.addLayer(layer); + if (fitExtent === 'true') { + GisApp.Module.core.fitExtent(layer.getSource().getExtent()); + } + } + return layer; + }, + /** + * 마커 레이어 맵에 추가 + */ + addMarkerLayer: function (lat, lon, fromProj, layerOptions) { + let p = transform([Number(lon), Number(lat)], fromProj, GisApp.TO_PROJ); + let feature = new Feature({ + geometry: new Point(p), + }); + let layer = this.getMarkerLayer(layerOptions); + layer.getSource().addFeature(feature); + let map = GisApp.Module.core.map; + map.addLayer(layer); + GisApp.Module.core.fitExtent(feature.getGeometry().getExtent()); + return layer; + }, + /** + * 마커 레이어 생성 + */ + getMarkerLayer: function (layerOptions) { + let imgSrc = GisApp.CONTEXT_PATH + 'images/marker/marker-icon.png'; + let style = new Style({ + image: new Icon({ + anchor: [0.5, 46], + anchorXUnits: 'fraction', + anchorYUnits: 'pixels', + src: imgSrc, + }), + }); + let layer = this.getLayerByFeature(null, layerOptions, style); + return layer; + }, + /** + * 피처 생성 + */ + getFeatureCoord: function (coord) { + let feature = new Feature({ + geometry: new Point(coord), + }); + return feature; + }, + /** + * 피처 생성(lat,lon) + */ + getFeatureLonLat: function (lat, lon) { + let p = transform([Number(lon), Number(lat)], GisApp.FROM_PROJ, GisApp.TO_PROJ); + let feature = new Feature({ + geometry: new Point(p), + }); + return feature; + }, + addPolygonLayer: function (features, option) { + const map = GisApp.Module.core.map; + option = Object.assign( + { + gobjTy: 'lineLabelPoligon', + minZoom: option?.minZoom || '10', + lblAtrbNm: option?.lblAtrbNm || 'fclty_nm', + lnWdth: option?.lnWdth || '2', + lnColor: option?.lnColor || 'rgba(0, 0, 0, 1)', + polgnColor: option?.polgnColor || 'rgba(255, 0, 0, 0.2)', + clickPsblYn: option?.clickPsblYn || 'N', + fondNm: '18px "맑은 고딕",Malgun Gothic', + }, + option + ); + features[0].set('overLayId', option?.overlayId || null); + features[0].set('html', option?.html || null); + //레이어 생성 + let vectorLayer = this.makeLayer(option); + //레이어 소스 생성 + const vectorSource = new VectorSource({}); + vectorSource.addFeatures(features); + vectorLayer.setSource(vectorSource); + //스타일 추가 + vectorLayer.setStyle(this.gisStyle.getLayerStyleFunction(vectorLayer, option)); + vectorLayer.set('id', option?.id + ''); + GisApp.Module.core.deleteLayer(option?.id); + map.addLayer(vectorLayer); + }, + addLayerByLyrTyNm: function (id, lyrTyNm, addQurryFlter) { + const map = GisApp.Module.core.map; + const layerCode = GisApp.layerCode; + const matched = Object.values(layerCode).find(obj => obj.lyrTyNm === lyrTyNm); + matched['id'] = id; + let layer = GisApp.Module.core.getLayerById(matched?.id); + if (layer) { + layer.setVisible(true); + } else { + const queryFlter = layerCode?.flterSql || '' + ' ' + (addQurryFlter || ''); + matched['flterSql'] = queryFlter; + layer = this.getWfsLayer(matched); + GisApp.Module.core.deleteLayer(id); + map.addLayer(layer); + } + }, + /** + * 클러스터 레이어 맵에 추가 + */ + addClusterLayer: function (features, { layerId, color }) { + const map = GisApp.Module.core.map; + const vectorSource = new VectorSource({}); + vectorSource.addFeatures(features); + const clusterLayerId = layerId + 'Cluster'; + const vectorLayerId = layerId + 'Vector'; + // 클러스터 레이어 생성 + const clusterLayer = new VectorLayer({ + id: clusterLayerId, + source: new Cluster({ + distance: 100, // 클러스터링 거리 + source: vectorSource, + geometryFunction: function (feature) { + let geometry = feature.getGeometry(); + let type = geometry.getType(); + let coordinates; + if (type === 'Point') { + coordinates = geometry.getCoordinates(); + } else if (type === 'LineString' || type === 'MultiLineString') { + let totalX = 0; + let totalY = 0; + let totalPoints = 0; + geometry.getCoordinates().forEach(line => { + line.forEach(point => { + totalX += point[0]; + totalY += point[1]; + totalPoints++; + }); + }); + coordinates = [totalX / totalPoints, totalY / totalPoints]; + } + return new Point(coordinates); + }, + }), + style: feature => { + const clusterSize = feature.get('features').length; + return new Style({ + image: new CircleStyle({ + radius: 10, + fill: new Fill({ color }), + }), + stroke: new Stroke({ + color, + width: 10, + }), + text: new Text({ + text: clusterSize.toString(), + fill: new Fill({ + color: 'white', + }), + stroke: new Stroke({ + color: 'black', + width: 4, + }), + }), + }); + }, + }); + + const vectorLayer = new VectorLayer({ + id: vectorLayerId, + zIndex: 1, + source: vectorSource, + style: new Style({ + image: new CircleStyle({ + radius: 10, + fill: new Fill({ color }), + }), + stroke: new Stroke({ + color, + width: 10, + }), + }), + }); + GisApp.Module.core.deleteLayer(clusterLayerId); + GisApp.Module.core.deleteLayer(vectorLayerId); + map.addLayer(clusterLayer); + map.addLayer(vectorLayer); + function handleZoomEnd() { + const findClusterLayer = map + ?.getLayers() + ?.getArray() + .find(function (layer) { + return layer.get('id') === clusterLayerId; + }); + const findVectorLayer = map + ?.getLayers() + ?.getArray() + .find(function (layer) { + return layer.get('id') === vectorLayerId; + }); + if (!findClusterLayer) { + GisApp.Module.core.deleteLayer(vectorLayerId); + } + if (!findVectorLayer) { + GisApp.Module.core.deleteLayer(vectorLayerId); + } + const zoomLevel = map.getView().getZoom(); + if (zoomLevel >= 18) { + vectorLayer.setVisible(true); + clusterLayer.setVisible(false); + } else { + clusterLayer.setVisible(true); + vectorLayer.setVisible(false); + } + if (!findClusterLayer && !findVectorLayer) { + map.getView().un('change:resolution', handleZoomEnd); + } + } + // 줌 이벤트 핸들러 등록 + map.getView().un('change:resolution', handleZoomEnd); + map.getView().on('change:resolution', handleZoomEnd); + handleZoomEnd(); + }, + setLayersToCesium: function (ol3dInstance) { + let me = this; + //const { Cesium } = window; + const cesiumScene = ol3dInstance.getCesiumScene(); // Cesium Scene 가져오기 + me.cesiumLayers.forEach(cesiumLayer => { + cesiumScene.imageryLayers.remove(cesiumLayer); + }); + const layers = GisApp.Module.core.map + .getAllLayers() + .filter(layer => layer.get('type') !== 'base') + .sort((a, b) => { + const orderA = a.getProperties().drawOrdr || 0; + const orderB = b.getProperties().drawOrdr || 0; + return orderA - orderB; // 낮은 값이 먼저 오도록 정렬 + }); + + layers.forEach(layer => { + const properties = layer.getProperties(); + let cesiumLayer = null; + if (properties?.lyrTyNm) { + const visible = layer.getVisible(); + const lyrTyNm = properties.lyrTyNm; + // OpenLayers에서 레이어의 가시성 확인 + const { Cesium } = window; + const wmsUrl = GisApp.GEOSERVER_WMS_URL; + const wmsProvider = new Cesium.WebMapServiceImageryProvider({ + url: wmsUrl, + layers: GisApp.Module.layer.storageNm + lyrTyNm, // WMS 레이어 이름 + parameters: { + service: 'WMS', + version: '1.1.1', + request: 'GetMap', + format: 'image/png', + transparent: true, + styles: lyrTyNm, + }, + }); + cesiumLayer = cesiumScene.imageryLayers.addImageryProvider(wmsProvider); + cesiumLayer['properties'] = properties; + me.cesiumLayers.push(cesiumLayer); + if (properties?.visible) { + const zoomLevel = getOpenLayersZoom(cesiumScene); + cesiumLayer.show = zoomLevel >= Number(properties?.minZoom); + } else { + cesiumLayer.show = false; + } + } + }); + if (me.cesiumLayers.length > 0) { + cesiumScene._moveEndListener = () => { + const zoomLevel = getOpenLayersZoom(cesiumScene); // 대략적인 줌 계산 + me.cesiumLayers.forEach(cesiumLayer => { + const properties = cesiumLayer.properties; + if (properties?.visible) { + console.log('zoomLevel >= minZoom==', properties?.lyrTyNm, zoomLevel >= properties?.minZoom); + cesiumLayer.show = zoomLevel >= Number(properties?.minZoom); + } + }); + }; + cesiumScene.camera.moveEnd.addEventListener(cesiumScene._moveEndListener); + } + }, +}; + +function getOpenLayersZoom(cesiumScene) { + const cameraHeight = cesiumScene.camera.positionCartographic.height; + const maxResolution = 156543.03392804097; // OpenLayers 기본 해상도 (Google Mercator 기준) + const tileSize = 256; // 기본 타일 크기 + return Math.ceil(Math.log2(maxResolution / (cameraHeight / tileSize))); +} + +export default GisOlLayer; diff --git a/packages/common/business_components/src/ui/map/lib/gis.measure.js b/packages/common/business_components/src/ui/map/lib/gis.measure.js new file mode 100644 index 000000000..6fc427bed --- /dev/null +++ b/packages/common/business_components/src/ui/map/lib/gis.measure.js @@ -0,0 +1,204 @@ +import Style from 'ol/style/Style'; +import VectorLayer from 'ol/layer/Vector'; +import VectorSource from 'ol/source/Vector'; +import Fill from 'ol/style/Fill.js'; +import Stroke from 'ol/style/Stroke'; +import CircleStyle from 'ol/style/Circle'; +import { getLength, getArea } from 'ol/sphere'; +import Draw from 'ol/interaction/Draw'; +import Polygon from 'ol/geom/Polygon'; +import LineString from 'ol/geom/LineString'; +import { unByKey } from 'ol/Observable'; +import Overlay from 'ol/Overlay'; +import GisApp from './gis.core'; +//import DOMPurify from 'dompurify'; +const GisOlMeasure = function (options) { + GisApp.measure = {}; + GisApp.measure.helpTooltipElement = null; + GisApp.measure.helpTooltip = null; + GisApp.measure.sketch = null; + GisApp.measure.measureTooltipElement = null; + GisApp.measure.measureTooltip = null; + GisApp.measure.continueLineMsg = '선 그리기 종료를 위해 마우스를 더블클릭하세요.'; + GisApp.measure.Polygon = 'Polygon'; + GisApp.measure.LineString = 'LineString'; + + this.initialize(options); +}; + +GisOlMeasure.prototype = { + map: null, + draw: null, + vector: null, + type: null, + initialize: function (map) { + let me = this; + let source = new VectorSource(); + let vector = new VectorLayer({ + source: source, + style: new Style({ + fill: new Fill({ + color: 'rgba(255, 255, 255, 0.2)', + }), + stroke: new Stroke({ + color: '#ffcc33', + width: 2, + }), + image: new CircleStyle({ + radius: 7, + fill: new Fill({ + color: '#ffcc33', + }), + }), + }), + }); + me.map = map; + me.vector = vector; + me.map.addLayer(vector); + me.map.removeInteraction(this.draw); + }, + pointerMoveHandler: function (evt) { + if (evt.dragging) { + return; + } + let helpMsg = '시작점을 선택하세요'; + if (GisApp.measure.sketch) { + helpMsg = GisApp.measure.continueLineMsg; + } + if (GisApp.measure.helpTooltipElement) { + //GisApp.measure.helpTooltipElement.innerHTML = DOMPurify.sanitize(helpMsg); + GisApp.measure.helpTooltipElement.innerHTML = helpMsg; + GisApp.measure.helpTooltip.setPosition(evt.coordinate); + GisApp.measure.helpTooltipElement.classList.remove('hidden'); + } + }, + formatLength: function (line) { + let length = getLength(line); + let output; + if (length > 1000) { + output = Math.round((length / 1000) * 100) / 100 + ' ' + 'km'; + } else { + output = Math.round(length * 100) / 100 + ' ' + 'm'; + } + return output; + }, + formatArea: function (polygon) { + let area = getArea(polygon); + let output; + if (area > 10000) { + output = Math.round((area / 1000000) * 100) / 100 + ' ' + 'km<sup>2</sup>'; + } else { + output = Math.round(area * 100) / 100 + ' ' + 'm<sup>2</sup>'; + } + return output; + }, + addInteraction: function (type) { + let me = this; + me.map.removeInteraction(me.draw); + me.map.on('pointermove', me.pointerMoveHandler); + me.type = type; + + let source = me.vector.getSource(); + me.draw = new Draw({ + source: source, + type: type, + style: new Style({ + fill: new Fill({ + color: 'rgba(255, 255, 255, 0.2)', + }), + stroke: new Stroke({ + color: 'rgba(0, 0, 0, 0.5)', + lineDash: [10, 10], + width: 2, + }), + image: new CircleStyle({ + radius: 5, + stroke: new Stroke({ + color: 'rgba(0, 0, 0, 0.7)', + }), + fill: new Fill({ + color: 'rgba(255, 255, 255, 0.2)', + }), + }), + }), + }); + + me.map.addInteraction(me.draw); + me.createMeasureTooltip(); + me.createHelpTooltip(); + + let listener; + me.draw.on('drawstart', function (evt) { + let tooltipCoord = evt.coordinate; + GisApp.measure.sketch = evt.feature; + listener = GisApp.measure.sketch.getGeometry().on('change', function (evt) { + let geom = evt.target; + let output; + if (geom instanceof Polygon) { + output = me.formatArea(geom); + tooltipCoord = geom.getInteriorPoint().getCoordinates(); + } else if (geom instanceof LineString) { + output = me.formatLength(geom); + tooltipCoord = geom.getLastCoordinate(); + } + //GisApp.measure.measureTooltipElement.innerHTML = DOMPurify.sanitize(output); + GisApp.measure.measureTooltipElement.innerHTML = output; + GisApp.measure.measureTooltip.setPosition(tooltipCoord); + }); + }); + + me.draw.on('drawend', function () { + GisApp.measure.measureTooltipElement.className = 'ol-tooltip ol-tooltip-static'; + GisApp.measure.measureTooltip.setOffset([0, -7]); + GisApp.measure.sketch = null; + // unset tooltip so that a new one can be created + GisApp.measure.measureTooltipElement = null; + me.createMeasureTooltip(); + unByKey(listener); + me.removeInteraction(); + }); + }, + removeInteraction: function () { + this.map.removeInteraction(this.draw); + this.map.removeOverlay(GisApp.measure.helpTooltip); + }, + removeFeature: function () { + let source = this.vector.getSource(); + let features = this.vector.getSource().getFeatures(); + for (let i in features) { + let feature = features[i]; + source.removeFeature(feature); + } + }, + createHelpTooltip: function () { + let me = this; + if (GisApp.measure.helpTooltipElement) { + GisApp.measure.helpTooltipElement.parentNode.removeChild(GisApp.measure.helpTooltipElement); + } + GisApp.measure.helpTooltipElement = document.createElement('div'); + GisApp.measure.helpTooltipElement.className = 'ol-tooltip hidden'; + GisApp.measure.helpTooltip = new Overlay({ + element: GisApp.measure.helpTooltipElement, + offset: [15, 0], + positioning: 'center-left', + }); + me.map.addOverlay(GisApp.measure.helpTooltip); + }, + createMeasureTooltip: function () { + let me = this; + if (GisApp.measure.measureTooltipElement) { + GisApp.measure.measureTooltipElement.parentNode.removeChild(GisApp.measure.measureTooltipElement); + } + GisApp.measure.measureTooltipElement = document.createElement('div'); + GisApp.measure.measureTooltipElement.className = 'ol-tooltip ol-tooltip-measure'; + GisApp.measure.measureTooltip = new Overlay({ + id: 'measure', + element: GisApp.measure.measureTooltipElement, + offset: [0, -15], + positioning: 'bottom-center', + }); + me.map.addOverlay(GisApp.measure.measureTooltip); + }, +}; + +export default GisOlMeasure; diff --git a/packages/common/business_components/src/ui/map/lib/gis.select.js b/packages/common/business_components/src/ui/map/lib/gis.select.js new file mode 100644 index 000000000..7f52ab2a3 --- /dev/null +++ b/packages/common/business_components/src/ui/map/lib/gis.select.js @@ -0,0 +1,436 @@ +import GisApp from './gis.core'; +import _ from 'lodash'; +import Style from 'ol/style/Style.js'; +import { click, mouseOnly, never } from 'ol/events/condition'; +import DragBox from 'ol/interaction/DragBox.js'; +import Draw from 'ol/interaction/Draw.js'; +import Select from 'ol/interaction/Select.js'; +import VectorLayer from 'ol/layer/Vector.js'; +import Overlay from 'ol/Overlay.js'; +import VectorSource from 'ol/source/Vector.js'; +import CircleStyle from 'ol/style/Circle.js'; +import Fill from 'ol/style/Fill.js'; +import Stroke from 'ol/style/Stroke.js'; +//import DOMPurify from 'dompurify'; + +const GisOlSelect = function (map, options) { + GisApp.select = {}; + GisApp.select.helpTooltipElement = null; + GisApp.select.helpTooltip = null; + GisApp.select.helpMsg = null; + this.initialize(map, options); +}; + +GisOlSelect.prototype = { + map: null, + defaults: { + hitTolerance: 10, + useAreaControl: false, + }, + selectInteraction: null, + dragBox: null, + dragPolygon: null, + selectedFeatures: [], + selectFeatureFlag: false, + initialize: function (map, options) { + this.map = map; + _.extend(this.defaults, options); + this.setInteraction(); + this.pointerMoveEventHandler(); + }, + /** + * SELECT 이벤트 등록 + */ + setInteraction: function () { + let me = this; + let map = this.map; + if (this.selectInteraction) { + this.removeInteration(); + } + let baseStyle = new Style({ + image : new CircleStyle({ + fill : new Fill({ + color : 'rgba(135, 116, 243, 0.4)', + }), + radius : 30 + }), + stroke : new Stroke({ + color : 'rgba(135, 116, 243, 0.4)', + width : 10, + }) + }); + + this.selectInteraction = new Select({ + style: function (feature, resolution) { + let styles = []; + let layer = feature.getLayer(); + let styleFn = GisApp.Module.style.getLayerStyleFunction(layer, null); + + if (styleFn) { + const style = styleFn(feature, resolution); + if (Array.isArray(style)) { + styles.push(...style); + } else { + styles.push(style); + } + } + styles.push(baseStyle); + return styles; + }, + layers: function (layer) { + let clickPsblYn = layer?.get('clickPsblYn'); + return clickPsblYn === 'Y'; + }, + condition: click, + hitTolerance: me.defaults.hitTolerance, + }); + + map.addInteraction(this.selectInteraction); + this.selectedFeatures = this.selectInteraction.getFeatures(); + + // 선택된 피처 중심으로 지도 이동 + this.selectInteraction.on('select', function (e) { + const feature = e.selected[0]; + if (feature) { + const geometry = feature.getGeometry(); + if (geometry) { + const extent = geometry.getExtent(); + map.getView().fit(extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 16 + }); + } + } + }); + }, + /** + * SELECT 피처 초기화 + */ + reset: function () { + let me = this; + if (GisApp.Module.edit) { + GisApp.Module.edit.setDrawActive(false); + } + if (GisApp.Module.draw) { + GisApp.Module.draw.setDrawActive(false); + } + me.setActive(true); + me.removeHelpTooltip(); + + let map = this.map; + map.removeInteraction(me.dragBox); + map.removeInteraction(me.dragPolygon); + + this.selectInteraction.addCondition_ = never; + this.selectFeatureFlag = false; + + if (GisApp.Module.measure) { + GisApp.Module.measure.removeFeature(); + GisApp.Module.core.deleteOverlay('measure'); + } + + GisApp.Module.core.deleteLayer(GisApp.hLayerId); + GisApp.Module.core.deleteLayer(GisApp.hlLayerId); + + this.resetSelectedFeature(); + }, + /** + * 단일 SELECT 이벤트 등록 + */ + setSingleInteration: function () { + this.reset(); + this.selectInteraction.addCondition_ = click; + this.selectFeatureFlag = true; + }, + /** + * 박스 SELECT 이벤트 등록 + */ + setBoxInteration: function () { + //초기화 + this.reset(); + this.selectFeatureFlag = true; + + let me = this; + let map = this.map; + me.dragBox = new DragBox({ + condition: mouseOnly, + }); + map.addInteraction(this.dragBox); + GisApp.select.helpMsg = '직사각형 시작점을 클릭하고 드래그하세요.'; + me.createHelpTooltip(); + me.dragBox.on('boxend', function () { + let rotation = map.getView().getRotation(); + let oblique = rotation % (Math.PI / 2) !== 0; + let candidateFeatures = oblique ? [] : me.selectedFeatures; + let extent = me.dragBox.getGeometry().getExtent(); + let layers = map.getLayers(); + for (let i in layers.array_) { + let layer = layers.array_[i]; + if (layer?.get('clickPsblYn') == 'Y' && me.getVisible(layer)) { + layer.getSource().forEachFeatureIntersectingExtent(extent, function (feature) { + candidateFeatures.push(feature); + }); + } + } + + if (oblique) { + let anchor = [0, 0]; + let geometry = me.dragBox.getGeometry().clone(); + geometry.rotate(-rotation, anchor); + let extent$1 = geometry.getExtent(); + candidateFeatures.forEach(function (feature) { + let geometry = feature.getGeometry().clone(); + geometry.rotate(-rotation, anchor); + if (geometry.intersectsExtent(extent$1)) { + me.selectedFeatures.push(feature); + } + }); + } + me.selectInteraction.dispatchEvent({ + type: 'select', + selected: me.selectedFeatures, + deselected: [], + }); + map.removeInteraction(me.dragBox); + }); + + me.dragBox.on('boxstart', function () { + me.removeHelpTooltip(); + me.selectedFeatures.clear(); + }); + }, + /** + * 폴리곤 SELECT 이벤트 등록 + */ + setPolygonInteration: function () { + this.reset(); + this.selectFeatureFlag = true; + + let me = this; + let map = this.map; + + //폴리곤 레이어 생성 + let polygonSource = new VectorSource({ + useSpatialIndex: false, + }); + let polygonLayer = new VectorLayer({ + source: polygonSource, + }); + map.addLayer(polygonLayer); + + //폴리곤 draw 객체 생성 + me.dragPolygon = new Draw({ + source: polygonSource, + type: 'Polygon', + condition: mouseOnly, + }); + map.addInteraction(me.dragPolygon); + + //툴팁 추가 + GisApp.select.helpMsg = '시작점을 클릭하세요.'; + me.createHelpTooltip(); + + me.dragPolygon.on('drawstart', function () { + me.removeHelpTooltip(); + me.selectedFeatures.clear(); + }); + + me.dragPolygon.on('drawend', function (evt) { + setTimeout(function () { + let polygon = evt.feature.getGeometry(); + let layers = map.getLayers(); + for (let i in layers.array_) { + let layer = layers.array_[i]; + if (layer?.get('clickPsblYn') == 'Y' && me.getVisible(layer)) { + let features = layer.getSource().getFeatures(); + for (let j = 0; j < features.length; j++) { + if (polygon.intersectsExtent(features[j].getGeometry().getExtent())) { + me.selectedFeatures.push(features[j]); + } + } + } + } + me.selectInteraction.dispatchEvent({ + type: 'select', + selected: me.selectedFeatures, + deselected: [], + }); + map.removeInteraction(me.dragPolygon); + map.removeLayer(polygonLayer); + }, 300); + }); + return me.dragPolygon; + }, + /** + * 레이어 추가 + */ + addLayer: function (layer) { + if (layer) { + layer.set('clickPsblYn', 'Y'); + } + }, + /** + * 레이어 삭제 + */ + removeLayer: function (layer) { + if (layer) { + layer.set('clickPsblYn', 'N'); + } + }, + /** + * 선택된 객체 초기화 + */ + resetSelectedFeature: function () { + const selectedFeatures = this.selectedFeatures; + selectedFeatures.forEach(feature => { + selectedFeatures.remove(feature); + }); + }, + /** + * SELECT 이벤트 활성화 + */ + setActive: function (active) { + this.selectInteraction.setActive(active); + }, + /** + * SELECT 이벤트 삭제 + */ + removeInteration: function () { + let map = this.map; + map.removeInteraction(this.selectInteraction); + this.selectInteraction = null; + + //포인터 변경 삭제 + this.removePointerMoveEventHandler('pointerMoveForSelect'); + }, + /** + * 피처 위에 마우스 오버시 포인터 변경 + */ + pointerMoveEventHandler: function () { + let me = this; + let map = this.map; + let pointerMoveForSelect = function (evt) { + let hit = this.forEachFeatureAtPixel( + evt.pixel, + function (feature, layer) { + let returnFlag = true; + if (layer?.get('clickPsblYn') === 'N') { + returnFlag = false; + } + return returnFlag; + }, + { hitTolerance: me.defaults.hitTolerance } + ); + + if (hit && me.selectInteraction.getActive()) { + this.getTargetElement().style.cursor = 'pointer'; + } else { + this.getTargetElement().style.cursor = ''; + } + }; + map.on('pointermove', pointerMoveForSelect); + }, + /** + * 포인터 이벤트 삭제 + */ + removePointerMoveEventHandler: function (fnNm) { + GisApp.Module.core.removeMapEventFnNm('pointermove', fnNm); + }, + /** + * 선택된 피처 정보 반환(callback) + */ + getSelectedInfo: function (callback) { + let me = this; + this.selectInteraction.on('select', function (e) { + if (!e.auto) { + let features = me.selectInteraction.getFeatures(); + if(features.getArray().length == 0) return; + let layers = []; + for (let i in features.array_) { + let feature = features.array_[0]; + let layer = me.selectInteraction.getLayer(feature); + layers.push(layer); + } + callback(features.array_, layers); + } + }); + }, + /** + * 선택된 피처 정보 반환 + */ + getSelectedFeatures: function () { + return this.selectInteraction.getFeatures(); + }, + /** + * 마지막으로 선택된 피처 정보 반환 + */ + getLastSelectedFeatures: function () { + return this.selectInteraction.getFeatures(); + }, + /** + * 툴팁 생성 + */ + createHelpTooltip: function () { + let me = this; + let map = this.map; + GisApp.select.helpTooltipElement = document.createElement('div'); + GisApp.select.helpTooltipElement.className = 'ol-tooltip hidden'; + GisApp.select.helpTooltip = new Overlay({ + element: GisApp.select.helpTooltipElement, + offset: [15, 0], + positioning: 'center-left', + }); + map.addOverlay(GisApp.select.helpTooltip); + map.on('pointermove', me.pointerMoveTooltipHandler); + }, + /** + * 툴팁 삭제 + */ + removeHelpTooltip: function () { + if (GisApp.select.helpTooltipElement) { + let map = this.map; + this.removePointerMoveEventHandler('pointerMoveTooltipHandler'); + map.removeOverlay(GisApp.measure.helpTooltip); + GisApp.select.helpTooltipElement.remove(); + } + }, + /** + * 툴팁용 포인터 move 이벤트 + */ + pointerMoveTooltipHandler: function (evt) { + if (evt.dragging) { + return; + } + //GisApp.select.helpTooltipElement.innerHTML = DOMPurify.sanitize(GisApp.select.helpMsg); + GisApp.select.helpTooltipElement.innerHTML = GisApp.select.helpMsg; + GisApp.select.helpTooltip.setPosition(evt.coordinate); + GisApp.select.helpTooltipElement.classList.remove('hidden'); + }, + getVisible: function (layer) { + let maxZoom = layer.getMaxZoom(); + let minZoom = layer.getMinZoom(); + let map = this.map; + let curZoom = map.getView().getZoom(); + if (layer.getVisible() && maxZoom > curZoom && minZoom < curZoom) { + return true; + } + return false; + }, + /** + * 피처 셀렉트 + */ + selectFeature: function (feature, auto) { + let me = this; + me.selectedFeatures.clear(); + me.selectedFeatures.push(feature); + me.selectInteraction.dispatchEvent({ + type: 'select', + selected: me.selectedFeatures, + deselected: [], + auto: auto, + }); + }, +}; + +export default GisOlSelect; diff --git a/packages/common/business_components/src/ui/map/lib/gis.style.js b/packages/common/business_components/src/ui/map/lib/gis.style.js new file mode 100644 index 000000000..df9be2daa --- /dev/null +++ b/packages/common/business_components/src/ui/map/lib/gis.style.js @@ -0,0 +1,402 @@ +import GisApp from './gis.core.js'; +import Style from 'ol/style/Style.js'; +import Fill from 'ol/style/Fill.js'; +import Icon from 'ol/style/Icon.js'; +import Circle from 'ol/style/Circle.js'; +import Stroke from 'ol/style/Stroke.js'; +import Text from 'ol/style/Text.js'; +import Point from 'ol/geom/Point.js'; +import RegularShape from 'ol/style/RegularShape.js'; +import Overlay from 'ol/Overlay'; +import _ from 'lodash'; +//import DOMPurify from 'dompurify'; +import { createPortal } from 'react-dom'; +import ReactDOM from 'react-dom/client'; +import React from 'react'; + +const GisOlStyle = function () { + this.initialize(); +}; + +GisOlStyle.prototype = { + overlays: {}, + hiddenOverlays: {}, + initialize: function () { + this.layericonPath = GisApp.CONTEXT_LAYER_PATH; + }, + getRGBAToString: function (r, g, b, a) { + let rgba = 'rgba({r}, {g}, {b}, {a})'; + return rgba.replace('{r}', r).replace('{g}', g).replace('{b}', b).replace('{a}', a); + }, + getFillColor: function (layer) { + let r = layer.get('fillColorR'); + let g = layer.get('fillColorG'); + let b = layer.get('fillColorB'); + let a = layer.get('fillColorA'); + return this.getRGBAToString(r, g, b, a); + }, + getFontLineColor: function (layer) { + let r = layer.get('fontOutlineColorR'); + let g = layer.get('fontOutlineColorG'); + let b = layer.get('fontOutlineColorB'); + let a = layer.get('fontOutlineColorA'); + return this.getRGBAToString(r, g, b, a); + }, + getFontFillColor: function (layer) { + let r = layer.get('fontColorR'); + let g = layer.get('fontColorG'); + let b = layer.get('fontColorB'); + let a = layer.get('fontColorA'); + return this.getRGBAToString(r, g, b, a); + }, + point: function (layer, options, feature, resolution) { + let symbol = layer.get('iconNm'); + let maticKey = layer.get('maticKey'); + let maticVal1 = layer.get('maticVal1'); + let image; + let style = new Style({ + fill: new Fill(), + }); + + let text = this.getText(layer, feature, resolution); + style.setText(text); + + if (symbol) { + let rotation = 0; + if (feature) { + let angDir = feature.get('angDir'); + if (angDir) { + rotation = (angDir * Math.PI) / 180; + } + } + image = new Icon({ + crossOrigin: 'anonymous', + scale: 1.2, + src: this.layericonPath + symbol, + rotation: rotation, + }); + } else { + image = new Circle({ + fill: new Fill({ + color: '#4848B4', + }), + radius: 5, + }); + } + + const overlayHtml = feature.get('html'); + if (overlayHtml) { + this.setOverlay(layer, feature, overlayHtml); + } + + let baseResolution = 20; + let minScale = 0.3; + let maxScale = 1.5; + let scale = baseResolution / resolution > maxScale ? maxScale : baseResolution / resolution; + scale = Math.max(minScale, Math.min(maxScale, scale)); + image.setScale(scale); + style.setImage(image); + _.extend(style, options); + return style; + }, + line: function (layer, options, feature, resolution) { + let linewidth = layer.get('lnWdth'); + let lineColor = layer.get('lnColor'); + let style = new Style({ + stroke: new Stroke({ + color: lineColor, + width: linewidth, + }), + text: new Text(), + }); + + if (GisApp.Module.toolbar && GisApp.Module.toolbar.defaults.blockDiagramVisible) { + let blockDiagramLayerId = GisApp.Module.toolbar.getBlockDiagramLayerId(layer.get('lyrId')); + if (blockDiagramLayerId) { + style = this.getArrowStyle(feature, lineColor, linewidth); + } + } + return style; + }, + poligon: function (layer, options, feature, resolution) { + const style = new Style(); + const color = layer.get('polgnColor'); + const fill = new Fill({ + color, + }); + style.setFill(fill); + _.extend(style, options); + return style; + }, + lineLabelPoligon: function (layer, options, feature, resolution) { + let style = new Style(); + const polgnColor = layer.get('polgnColor'); + let fill = new Fill({ + color: polgnColor, + }); + let lnWdth = layer.get('lnWdth'); + let lineColor = layer.get('lnColor'); + let lineDash = layer.get('lineDash'); + let stroke = new Stroke({ + color: lineColor || '#000000', + width: lnWdth, + lineDash: lineDash ? [10, 5] : null, + }); + let text = this.getText(layer, feature, resolution); + if (text) { + style.setText(text); + } + const overlayHtml = feature.get('html'); + if (overlayHtml) { + this.setOverlay(layer, feature, overlayHtml); + } + style.setFill(fill); + style.setStroke(stroke); + _.extend(style, options); + return style; + }, + imagePoligon: function (layer, options, feature, resolution) { + let style = new Style(); + let symbol = layer.get('iconNm'); + let retGeom = feature.getGeometry().getInteriorPoint(); + let image = new Icon({ + crossOrigin: 'anonymous', + imgSize: [20, 20], + src: this.layericonPath + symbol, + }); + + let scale = 0.5 / resolution > 1 ? 1 : 0.5 / resolution; + image.setScale(1); + style.setGeometry(retGeom); + style.setImage(image); + + _.extend(style, options); + return style; + }, + setOverlay: function (layer, feature, overlayHtml) { + const layerFeature = layer.getSource().getFeatures()[0]; + const layerCoordinate = layerFeature ? layerFeature.getGeometry().getCoordinates() : null; + + const properties = feature.getProperties(); + const overLayId = feature?.get('overLayId'); + const overLayMinZoom = layer.get('overLayMinZoom') || '12'; + const overLayOffset = layer?.get('overLayOffset'); + let findoOverlay = this.overlays[overLayId]; + if (findoOverlay) { + const zoom = GisApp.Module.core.map.getView().getZoom(); + if (Number(overLayMinZoom) < zoom) { + findoOverlay.options.element.style.display = 'block'; + } else { + findoOverlay.options.element.style.display = 'none'; + } + return findoOverlay; + } + let overlayContainer = document.createElement('div'); + document.body.appendChild(overlayContainer); + + if (typeof overlayHtml === 'string') { + //const sanitizedHtml = DOMPurify.sanitize(overlayHtml); + overlayContainer.innerHTML = sanitizedHtml; + } + // React 컴포넌트일 경우 createPortal() 사용 + else if (React.isValidElement(overlayHtml)) { + ReactDOM.createRoot(overlayContainer).render(createPortal(overlayHtml, overlayContainer)); + } + + // 오버레이 생성 + let overlay = new Overlay({ + element: overlayContainer, + positioning: 'bottom-center', + stopEvent: false, + offset: overLayOffset ? overLayOffset : [0, 0], // 오프셋 설정 (위치 조정) + }); + this.overlays[overLayId] = overlay; + + // feature의 좌표 가져오기 + let geometry = feature.getGeometry(); + let coordinate; + // Point인 경우 + if (geometry.getType() === 'Point') { + coordinate = geometry.getCoordinates(); + } + // LineString 또는 MultiLineString인 경우 + else if (geometry.getType() === 'LineString' || geometry.getType() === 'MultiLineString') { + coordinate = geometry.getFlatMidpoint(); // 선의 중간 좌표를 가져옴 + } + // Polygon 또는 MultiPolygon인 경우 + else if (geometry.getType() === 'Polygon' || geometry.getType() === 'MultiPolygon') { + coordinate = geometry.getExtent(); + const center = [(coordinate[0] + coordinate[2]) / 2, (coordinate[1] + coordinate[3]) / 2]; + coordinate = center; + } else { + console.error('Unsupported geometry type: ' + geometry.getType()); + return; + } + overlay.setPosition(coordinate); + GisApp.Module.core.map.addOverlay(overlay); + const zoom = GisApp.Module.core.map.getView().getZoom(); + if (Number(overLayMinZoom) < zoom) { + overlay.options.element.style.display = 'block'; + } else { + overlay.options.element.style.display = 'none'; + } + }, + label: function (layer, options, feature, resolution) { + let textBaseline = layer.get('textBaseline') || 'bottom'; + let textAlign = layer.get('textAlign') || 'center'; + let fontExtnColor = layer.get('fontExtnColor'); + let fontExtnWdth = Number(layer.get('fontExtnWdth') || '3.5'); + let fontIntnColor = layer.get('fontIntnColor'); + let style = new Style(); + let prop = feature.getProperties(); + let label = prop[layer.get('lblAtrbNm')]; + + let text = new Text({ + stroke: new Stroke({ + width: fontExtnWdth, + color: fontExtnColor, + }), + fill: new Fill({ + color: fontIntnColor, + }), + font: layer.get('font'), + textBaseline: textBaseline, + textAlign: textAlign, + text: label, + }); + style.setStroke(null); + style.setFill(null); + style.setText(text); + _.extend(style, options); + + return style; + }, + getText: function (layer, feature, resolution) { + let textBaseline = layer.get('textBaseline') || 'middle'; + let textAlign = layer.get('textAlign') || 'center'; + let fontExtnColor = layer.get('fontExtnColor'); + let fontExtnWdth = Number(layer.get('fontExtnWdth') || '1'); + let fontIntnColor = layer.get('fontIntnColor'); + let minViewSize = layer.get('minViewSize'); + if (feature == null) { + return; + } + let prop = feature.getProperties(); + let label = prop[layer.get('lblAtrbNm')]; + let scaleFactor = 1 / resolution; // 해상도가 낮아질수록 글자 크기 증가 + const zoom = GisApp.Module.core.map.getView().getZoom(); + if (zoom < minViewSize - 3) { + label = ''; + } + let text = new Text({ + stroke: new Stroke({ + color: '#FFFFFF', // 흰색 테두리 + width: fontExtnWdth, + }), + fill: new Fill({ + color: fontIntnColor, + width: fontExtnWdth, + }), + font: `${layer.get('fontNm')}`, // 동적으로 폰트 크기 적용 + textBaseline: textBaseline, + textAlign: textAlign, + overflow: true, + text: label, + offsetY: 20, + }); + + return text; + }, + getArrowStyle: function (feature, lineColor, linewidth) { + let styles = [ + new Style({ + stroke: new Stroke({ + color: lineColor, + width: linewidth, + }), + }), + ]; + let geomLienString = GisApp.Module.core.getGeomLineOfFeature(feature); + if (geomLienString) { + let startCoord = geomLienString.getFirstCoordinate(); + let endCoord = geomLienString.getLastCoordinate(); + + let dx = endCoord[0] - startCoord[0]; + let dy = endCoord[1] - startCoord[1]; + let rotation = Math.atan2(dy, dx); + + let centerCoord = GisApp.Module.core.getCenterOfLineFeature(feature); + styles.push( + new Style({ + geometry: new Point(centerCoord), + image: new RegularShape({ + fill: new Fill({ color: lineColor }), + points: 3, + radius: 6, + rotation: -rotation, + angle: Math.PI / 2, + }), + }) + ); + } + return styles; + }, + //레이어 스타일 반환 + getLayerStyle: function (layer, options) { + let gobjTy = layer.get('gobjTy'); + if (!gobjTy) return; + let style = this[gobjTy](layer, options); + return style; + }, + getLayerStyleFunction: function (layer, options) { + let gobjTy = layer.get('gobjTy'); + let styleFunction = function (feature, resolution) { + return this[gobjTy](layer, options, feature, resolution); + }.bind(this); + return styleFunction; + }, + adjustOverlayPosition: function (overlay, coordinate) { + const overlays = this.overlays; + let adjustedCoordinate = [...coordinate]; // 초기 좌표 설정 + const minDistance = 100000; // 최소 거리 (필요시 조정) + let offset = overlay.getOffset() || [0, 0]; + + let closestOverlay = null; + let closestDistance = Infinity; + + // 모든 오버레이의 위치를 비교하여 가장 가까운 오버레이 찾기 + Object.values(overlays).forEach(existingOverlay => { + if (existingOverlay) { + const existingCoordinate = existingOverlay.getPosition(); + const existingOffset = existingOverlay.getOffset() || [0, 0]; + + // 현재 오버레이의 좌표 가져오기 + const overlayCoordinate = overlay.getPosition(); + if (!overlayCoordinate || !existingCoordinate) return; + + // 거리 계산 (유클리드 거리) + const distance = Math.sqrt(Math.pow(overlayCoordinate[0] - existingCoordinate[0], 2) + Math.pow(overlayCoordinate[1] - existingCoordinate[1], 2)); + + // 가장 가까운 오버레이 업데이트 + if (distance > 0 && distance < closestDistance) { + closestDistance = distance; + closestOverlay = existingOverlay; + } else { + offset = [50, 50]; + } + } + }); + + // 가장 가까운 오버레이가 있으면 offset 조정 + if (closestOverlay && closestDistance < minDistance) { + const closestOffset = closestOverlay.getOffset() || [0, 0]; + // X, Y 좌표를 기존 offset 값의 반대로 설정 + offset = [-closestOffset[0], -closestOffset[1]]; + } + + // 최종 offset 적용 + overlay.setOffset(offset); + }, +}; + +export default GisOlStyle; diff --git a/packages/common/business_components/src/ui/map/lib/images/marker/marker-icon-boon.png b/packages/common/business_components/src/ui/map/lib/images/marker/marker-icon-boon.png new file mode 100644 index 000000000..f65ce99f2 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/marker/marker-icon-boon.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/marker/marker-icon-hab.png b/packages/common/business_components/src/ui/map/lib/images/marker/marker-icon-hab.png new file mode 100644 index 000000000..2ef70877e Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/marker/marker-icon-hab.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/marker/marker-icon.png b/packages/common/business_components/src/ui/map/lib/images/marker/marker-icon.png new file mode 100644 index 000000000..950edf246 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/marker/marker-icon.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_area.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_area.png new file mode 100644 index 000000000..044562b55 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_area.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_area_on.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_area_on.png new file mode 100644 index 000000000..0129ff1bd Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_area_on.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_next.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_next.png new file mode 100644 index 000000000..075111dcf Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_next.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_next_on.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_next_on.png new file mode 100644 index 000000000..4fdbe3a00 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_next_on.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_prev.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_prev.png new file mode 100644 index 000000000..7e3fae7d2 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_prev.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_prev_on.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_prev_on.png new file mode 100644 index 000000000..fb2d55cfd Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_prev_on.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_print.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_print.png new file mode 100644 index 000000000..82596b83d Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_print.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_print_on.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_print_on.png new file mode 100644 index 000000000..bb7f27334 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_print_on.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_ruler.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_ruler.png new file mode 100644 index 000000000..501e460a4 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_ruler.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_ruler_on.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_ruler_on.png new file mode 100644 index 000000000..676a81bcc Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_ruler_on.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_save.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_save.png new file mode 100644 index 000000000..c057b9377 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_save.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_save_on.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_save_on.png new file mode 100644 index 000000000..bf8e6d623 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_save_on.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_sketch.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_sketch.png new file mode 100644 index 000000000..38a5f507f Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_sketch.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_sketch_on.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_sketch_on.png new file mode 100644 index 000000000..79fb54e03 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_sketch_on.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_zoomin.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_zoomin.png new file mode 100644 index 000000000..ac32fbab0 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_zoomin.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_zoomin_on.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_zoomin_on.png new file mode 100644 index 000000000..f258a965f Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_zoomin_on.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_zoomout.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_zoomout.png new file mode 100644 index 000000000..2ee0b5161 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_zoomout.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_zoomout_on.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_zoomout_on.png new file mode 100644 index 000000000..58dcbfdae Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_cont_zoomout_on.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_add.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_add.png new file mode 100644 index 000000000..fe4f0993f Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_add.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_add_on.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_add_on.png new file mode 100644 index 000000000..37a836298 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_add_on.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_del.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_del.png new file mode 100644 index 000000000..3bb372ab9 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_del.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_del_on.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_del_on.png new file mode 100644 index 000000000..89e558739 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_del_on.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_next.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_next.png new file mode 100644 index 000000000..df4913c90 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_next.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_next_on.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_next_on.png new file mode 100644 index 000000000..4e5540b55 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_next_on.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_prev.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_prev.png new file mode 100644 index 000000000..e94bf6d42 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_prev.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_prev_on.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_prev_on.png new file mode 100644 index 000000000..8645dedfa Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_prev_on.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_save.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_save.png new file mode 100644 index 000000000..5f01dd55c Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_save.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_save_on.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_save_on.png new file mode 100644 index 000000000..b035687d6 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_save_on.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_slt.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_slt.png new file mode 100644 index 000000000..aaf2bbd08 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_slt.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_slt_on.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_slt_on.png new file mode 100644 index 000000000..02d035a74 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_slt_on.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_snap.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_snap.png new file mode 100644 index 000000000..fc91d4304 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_snap.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_snap_on.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_snap_on.png new file mode 100644 index 000000000..f856d8fbe Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/btn_edit_snap_on.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/ico_gis_sprite.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/ico_gis_sprite.png new file mode 100644 index 000000000..3567cc6e4 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/ico_gis_sprite.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/icon-viewclose.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/icon-viewclose.png new file mode 100644 index 000000000..d93ce9c05 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/icon-viewclose.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/img_map1.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/img_map1.png new file mode 100644 index 000000000..7f96fcf5e Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/img_map1.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/img_map2.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/img_map2.png new file mode 100644 index 000000000..1a93f50e8 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/img_map2.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/img_map3.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/img_map3.png new file mode 100644 index 000000000..df80af6dc Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/img_map3.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/img_map4.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/img_map4.png new file mode 100644 index 000000000..02d015d12 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/img_map4.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/loading.gif b/packages/common/business_components/src/ui/map/lib/images/toolbar/loading.gif new file mode 100644 index 000000000..7c3683bb4 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/loading.gif differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/map_pin.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/map_pin.png new file mode 100644 index 000000000..b6cc2f6aa Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/map_pin.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/toggle_off.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/toggle_off.png new file mode 100644 index 000000000..d5dea9f67 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/toggle_off.png differ diff --git a/packages/common/business_components/src/ui/map/lib/images/toolbar/toggle_on.png b/packages/common/business_components/src/ui/map/lib/images/toolbar/toggle_on.png new file mode 100644 index 000000000..9cb573875 Binary files /dev/null and b/packages/common/business_components/src/ui/map/lib/images/toolbar/toggle_on.png differ diff --git a/packages/common/business_components/src/ui/map/lib/util.js b/packages/common/business_components/src/ui/map/lib/util.js new file mode 100644 index 000000000..9fe55869b --- /dev/null +++ b/packages/common/business_components/src/ui/map/lib/util.js @@ -0,0 +1,596 @@ + +//공통 +export const util = { + getOneToArrayByObjectKey: function (arr, key, value) { + let result = {}; + arr.forEach(item => { + if (item[key] === value) { + result = item; + } + }); + return result; + }, + groupByProperty: (arr, prop, parentProp, group) => { + const tree = []; + const mappedArr = {}; + let arrElem; + let mappedElem; + // 먼저 배열의 노드를 개체에 매핑하고 -> 해시 테이블을 생성 + for (let i = 0; i < arr.length; i++) { + arrElem = arr[i]; + if (group && arrElem.mnuGroup !== group) continue; + mappedArr[arrElem[prop]] = arrElem; + mappedArr[arrElem[prop]].children = []; + } + + for (const mnuCd in mappedArr) { + if (mappedArr.hasOwnProperty(mnuCd)) { + mappedElem = mappedArr[mnuCd]; + // 요소가 루트 수준에 없으면 자식의 부모 배열에 추가. + if (mappedElem[parentProp] && mappedArr[mappedElem[parentProp]]?.children) { + mappedArr[mappedElem[parentProp]].children.push(mappedElem); + } + // 요소가 루트 수준에 있는 경우 첫 번째 수준 요소 배열에 추가 + else { + tree.push(mappedElem); + } + } + } + return tree; + }, + getLowestChildren: function (obj) { + if (!obj.children || obj.children.length === 0) { + return obj; + } else { + return this.getLowestChildren(obj.children[0]); + } + }, + hasClass: (e, clazz) => { + return e.target.classList.contains(clazz); + }, + isNull: function (str) { + if (typeof str === 'undefined' || str === null || str === 'null' || str === '' || str === 'undefined') { + return true; + } + return false; + }, + nvl: function (str, returnVal) { + if (this.isNull(str)) { + return returnVal; + } + return str; + }, + isNumeric: function (str) { + const pattern = /\d/; + return pattern.test(str); + }, + isAlpha: function (str) { + const pattern = /[a-zA-Z]/; + return pattern.test(str); + }, + isAlphaNumeric: function (str) { + const pattern = /^[a-zA-Z0-9]+$/; + return pattern.test(str); + }, + isSpecialChar: function (str) { + const pattern = /[~!@#$%^&*()_+|<>?:{}]/; + return pattern.test(str); + }, + isKoreanAplhaNumeric: function (str) { + const pattern = /^[ㄱ-ㅎ|ㅏ-ㅣ|가-힣|a-zA-Z0-9]+$/; + return pattern.test(str); + }, + isValidPassword: function (str) { + const pattern = /^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!@#$%^&*(),.?":{}|<>])[a-zA-Z0-9!@#$%^&*(),.?":{}|<>]{8,20}$/; + return pattern.test(str); + }, + isValidExt: function (fileName, allowedExtensions) { + const fileExtension = fileName.split('.').pop().toLowerCase(); + return allowedExtensions.includes(fileExtension); + }, + isValidAllDate: function (dateString) { + const formats = ['YYYY', 'YYYYMM', 'YYYYMMDD']; + for (let format of formats) { + if (moment(dateString, format, true).isValid()) { + return true; + } + } + return false; + }, + isEmptyObject: function (param) { + return Object.keys(param).length === 0 && param.constructor === Object; + }, + castArrayObjectToList: function (array, key) { + var newArray = []; + for (var i = 0; i < array.length; i++) { + newArray.push(array[i][key]); + } + return newArray; + }, + arraysMatch: function (arr1, arr2) { + // 길이가 같은지 확인 + if (arr1.length !== arr2.length) return false; + + // 각 요소가 일치하는지 확인 + return arr1.every((element, index) => element === arr2[index]); + }, + //상위뎁스 객체들 조회 + findObjectByKeyAndValue: function ({ array, find: { key: findKey, value: findValue }, result: { key: resultKey }, parents = [] }) { + for (let i = 0; i < array.length; i++) { + let obj = array[i]; + let siblings = [...array]; + //siblings.splice(i, 1); // 현재 메뉴를 제거하여 형제 메뉴들만 남김 + let currentParents = [...parents, { [resultKey]: obj, siblings: siblings }]; + + if (obj[findKey] === findValue) { + let formattedResult = currentParents.reduce((acc, cur, idx) => { + acc[`${resultKey}${idx + 1}`] = { active: cur[resultKey], list: cur.siblings }; + return acc; + }, {}); + return formattedResult; + } + if (obj.children) { + let result = this.findObjectByKeyAndValue({ array: obj.children, find: { key: findKey, value: findValue }, result: { key: resultKey }, parents: currentParents }); + if (result !== null) { + return result; + } + } + } + return null; + }, + autoComma(val, returnVal) { + if (!val && val !== 0) return returnVal ? returnVal : ''; + + // 문자열로 변환하고, 숫자와 소수점만을 허용 + val = String(val).replace(/[^0-9.-]/g, ''); + + // 소수점을 기준으로 숫자를 분리 + let [integerPart, decimalPart] = val.split('.'); + + // 정수 부분에 대해 쉼표 추가 + integerPart = integerPart.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'); + + // 소수점 이하 숫자가 있다면 다시 합치기 + return decimalPart ? `${integerPart}.${decimalPart}` : integerPart; + }, + removeComma: function (str) { + str = String(str); + return str.replace(/[^\d]+/g, ''); + }, + removeSpaceAndTrim: function (str) { + if (str) { + str = String(str); + return str.replace(/\s/g, ''); + } else { + return str; + } + }, + isNotWhiteSpace: function (str) { + const hasWhitespace = /\s/.test(str); + return !hasWhitespace; + }, + //숫자만 남기고 모두 제거해서 반환 + retainOnlyNumbers: function (str) { + str = String(str); + return str.replace(/\D+/g, ''); + }, + proxyToObject: function (proxy) { + return JSON.parse(JSON.stringify(Object.assign({}, proxy))); + }, + dateFormatter: function ({ input, isHan = false }) { + const value = this.retainOnlyNumbers(input); + + if (value.length === 6) { + const date = moment(value, 'YYYYMM'); + return date.format(isHan ? 'YYYY년 MM월' : 'YYYY-MM'); + } else if (value.length === 8) { + const date = moment(value, 'YYYYMMDD'); + return date.format(isHan ? 'YYYY년 MM월 DD일' : 'YYYY-MM-DD'); + } else if (value.length === 14) { + const date = moment(value, 'YYYYMMDDHHmmss'); + return date.format(isHan ? 'YYYY년 MM월 DD일 HH시 mm분 ss초' : 'YYYY-MM-DD HH:MM:SS'); + } else { + return input; + } + }, + generateYearArray: function (num) { + const currentYear = moment().year(); + if (num <= 1 || isNaN(num)) { + return [(currentYear - 2).toString(), (currentYear - 1).toString(), currentYear.toString()]; + } + + const years = []; + for (let i = num - 1; i >= 0; i--) { + years.push((currentYear - i).toString()); + } + + return years; + }, + getFileId: function () { + let timeStamp = moment().format('YYYYMMDDHHmmssSSS'); // YYYYMMDDHHmmssSSS 형태로 타임스탬프를 생성 + return 'CFM_' + timeStamp.slice(-12); // slice(-12)는 문자열의 끝에서 12자리를 추출합니다. + }, + lpad: function (number, pad, length) { + let str = number.toString(); + while (str.length < length) { + str = pad + str; + } + return str; + }, + convertKeysToCamelCase: function (obj) { + if (obj === null || typeof obj !== 'object') { + return obj; + } + return Object.keys(obj).reduce((acc, key) => { + const camelCaseKey = key.replace(/([-_]\w)/g, match => match.replace(/[-_]/, '').toUpperCase()); + acc[camelCaseKey] = typeof obj[key] === 'object' ? this.convertKeysToCamelCase(obj[key]) : obj[key]; + return acc; + }, {}); + }, + blobUrlToBlob: async blobUrl => { + try { + const response = await fetch(blobUrl); + return await response.blob(); + } catch (error) { + console.error(error); + return new Blob(); + } + }, + hexToRgba: hex => { + const r = parseInt(hex.substring(1, 3), 16); + const g = parseInt(hex.substring(3, 5), 16); + const b = parseInt(hex.substring(5, 7), 16); + return `rgba(${r}, ${g}, ${b} , 0.5)`; + }, + rgbaToHex: rgba => { + /*rgba값을 가져와서 rgb 로 추출 후 hex 값 반환 */ + if (!rgba) return false; + const rgbArray = rgba.match(/\d+/g).slice(0, 3).map(Number); + return `#${((1 << 24) | (rgbArray[0] << 16) | (rgbArray[1] << 8) | rgbArray[2]).toString(16).slice(1).toUpperCase()}`; + }, + rgbAddOpacity: (rgb, opacity) => { + const rgbValues = rgb.match(/\d+/g); // rgb 값을 배열로 추출 + const colorWithOpacity = `rgba(${rgbValues[0]}, ${rgbValues[1]}, ${rgbValues[2]}, ${opacity})`; // rgba로 변환 + return colorWithOpacity; + }, + generateMonthArray: (pSmonth, pEmonth) => { + let startYear = Math.floor(pSmonth / 100); + let startMonth = pSmonth % 100; + let endYear = Math.floor(pEmonth / 100); + let endMonth = pEmonth % 100; + + let months = []; + + for (let year = startYear; year <= endYear; year++) { + let monthStart = year === startYear ? startMonth : 1; + let monthEnd = year === endYear ? endMonth : 12; + + for (let month = monthStart; month <= monthEnd; month++) { + let monthString = month < 10 ? `0${month}` : `${month}`; + months.push(`${year}${monthString}`); + } + } + + return months; + }, + findDuplicatesForArr: (arr, key) => { + const map = new Map(); + const duplicates = []; + + arr.forEach(item => { + const keyValue = item[key]; + if (map.has(keyValue)) { + if (!duplicates.includes(keyValue)) { + duplicates.push(keyValue); + } + } else { + map.set(keyValue, true); + } + }); + + return duplicates; + }, + createHtmlFromJson: (html, data) => { + if (!html || util.isEmptyObject(html)) return null; + const { classNm, headerKey, rows } = JSON.parse(html); + if (!rows) return null; + const tableRows = rows + .map(row => { + const value = data?.[row.key] ?? null; + let displayValue; + if (typeof value === 'number') { + displayValue = util.autoComma(value, 0); + } else if (typeof value === 'string') { + displayValue = util.nvl(value, '-'); + } else { + displayValue = '-'; + } + return ` + <tr> + <th>${row.th}</th> + <td>${displayValue}</td> + </tr> + `; + }) + .join(''); + return ` + <div class="${classNm}"> + <table> + <tr> + <th colspan="2" style="position: relative; text-align: center; background-color: #DEEAF3;"> + ${data?.[headerKey] ?? '-'} + </th> + </tr> + ${tableRows} + </table> + </div> + `; + }, +}; + +/** + * + * 다이렉트로 import하여 사용하기 위해 export로 수정중 + * + * */ + +//객체 배열에서 특정 key의 value인 것을 추출 +export const getArrayByObjectKey = function (arr, key, value) { + return arr.filter(item => item[key] === value); +}; + +//소수점 1자리를 기준으로 점을 언더스코어로 변경 +export const numberDotToUnderscore = function (num) { + if (Number.isInteger(num)) { + return num.toString(); + } else { + return num.toFixed(1).replace('.', '_'); + } +}; + +export const stripHTML = function (htmlString) { + const parser = new DOMParser(); + const doc = parser.parseFromString(htmlString, 'text/html'); + return doc.body.textContent || ''; +}; + +//URL에서 매개변수 추출 +export function getParameterByName(name, url = window.location.search) { + name = name.replace(/[\[\]]/g, '\\$&'); + var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'), + results = regex.exec(url); + if (!results) return null; + if (!results[2]) return ''; + return decodeURIComponent(results[2].replace(/\+/g, ' ')); +} + +export function dateFormatForDB({ date: isoString, format = 'YYYY-MM-DD HH:MI:SS' }) { + if (!isoString) return ''; + + let date = new Date(isoString); + + let year = date.getFullYear(); + let month = (date.getMonth() + 1).toString().padStart(2, '0'); + let day = date.getDate().toString().padStart(2, '0'); + + let hours = date.getHours().toString().padStart(2, '0'); + let minutes = date.getMinutes().toString().padStart(2, '0'); + let seconds = date.getSeconds().toString().padStart(2, '0'); + + return ( + format + // + .replace('YYYY', year) + .replace('MM', month) + .replace('DD', day) + .replace('HH', hours) + .replace('MI', minutes) + .replace('SS', seconds) + ); +} + +//blob 다운로드 +//blob 다운로드 +export function downloadFromBlob(res, data) { + const url = URL.createObjectURL(new Blob([res.data], { type: res.data.type })); + const link = document.createElement('a'); + + const contentDisposition = res.headers?.['content-disposition']; // 파일 이름 + let fileName = data?.fileName || 'export'; //기본 파일 이름 + + if (contentDisposition) { + const [fileNameMatch] = contentDisposition.split(';').filter(str => str.includes('filename')); + if (fileNameMatch) { + [, fileName] = fileNameMatch.split('='); + // 파일명이 "UTF-8''" 등으로 인코딩되어 있을 수 있으니 decodeURIComponent 추가 + fileName = decodeURIComponent(fileName); + } + } + + link.href = url; + // 브라우저 인코딩 관련 이슈 방지를 위해 파일명에 대한 추가 decodeURI 처리 + link.download = decodeURIComponent(fileName).replace(/"/g, '').trim(); + link.style.display = 'none'; + document.body.appendChild(link); + link.click(); + + link.remove(); + URL.revokeObjectURL(url); +} + +export function downloadBlobAsFile(blob, filename = 'export') { + // 브라우저에 따라 인코딩 처리 햔재는 파이어폭스만 + const url = URL.createObjectURL(blob); + const encodedFilename = encodeURIComponent(filename); + const downloadLink = document.createElement('a'); + + downloadLink.href = url; + downloadLink.download = decodeURIComponent(encodedFilename); + + document.body.appendChild(downloadLink); + downloadLink.click(); + + document.body.removeChild(downloadLink); + URL.revokeObjectURL(url); +} + +export function viewFile(res, data) { + // Blob URL 생성 + const url = URL.createObjectURL(new Blob([res.data], { type: res.data.type })); + + let fileType = null; + let fileName = data?.fileName || 'export'; // 기본 파일 이름 + + // PDF 및 이미지 타입의 MIME 타입 확인 + if (res.data.type === 'application/pdf') { + fileType = 'pdf'; + } else if (res.data.type.startsWith('image/')) { + fileType = 'img'; + } else if (res.data.type === 'application/octet-stream') { + // Content-Disposition 헤더에서 파일 이름 추출 + const contentDisposition = res.headers?.['content-disposition']; + if (contentDisposition) { + const fileNameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/); + if (fileNameMatch && fileNameMatch[1]) { + fileName = fileNameMatch[1].replace(/['"]/g, ''); // 파일 이름 추출 + fileName = decodeURIComponent(fileName); // URL 인코딩된 파일 이름을 디코딩 + } + } + + // 파일 확장자 추출 + const fileExtension = fileName.split('.').pop().toLowerCase(); + + // 확장자를 통해 파일 타입 결정 + if (fileExtension === 'pdf') { + fileType = 'pdf'; + } else if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp'].includes(fileExtension)) { + fileType = 'img'; + } else { + console.error('지원하지 않는 파일 형식입니다.'); + } + } + + // URL과 파일 타입 반환 + return { url, fileType, fileName }; +} + +export function getToday() { + return getDate('today'); +} + +export function getDate(type, diff) { + const date = new Date(); + + switch (type) { + case 'year': + date.setFullYear(date.getFullYear() + diff); + break; + case 'month': + date.setMonth(date.getMonth() + diff); + break; + case 'day': + date.setDate(date.getDate() + diff); + break; + case 'today': + date.setDate(date.getDate()); + break; + default: + console.log('잘못된 타입입니다. 올바른 타입: 년, 월, 일'); + return; + } + + const year = date.getFullYear(); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const day = date.getDate().toString().padStart(2, '0'); + + return `${year}${month}${day}`; +} + +//promise 처리 +export const byPromise = function (actions) { + return actions.reduce((promise, action) => { + return promise.then(async () => { + if (typeof action === 'function') { + return await action(); + } else { + return await action; + } + }); + }, Promise.resolve()); +}; + +export const getMessage = (msgGroup, msgKey, innerText, fix) => { + let resultString = GLOBAL.MESSAGE[msgGroup][msgKey]; + const prefix = fix?.prefix || ''; + const suffix = fix?.suffix || ''; + + if (innerText) { + for (const [key, value] of Object.entries(innerText)) { + const regex = new RegExp(`\\$\{${key}\}`, 'g'); + resultString = resultString.replace(regex, value); + } + } + + if (resultString) resultString = resultString.replace(/\\n/g, '\n'); + return resultString ? `${prefix}${resultString}${suffix}` : '안내 메시지 로딩 오류 - 관리자에게 문의하세요.'; +}; + +// global.js의 message를 읽어오는 alert +export const gAlert = (msgGroup, msgKey, innerText, fix) => { + alert(getMessage(msgGroup, msgKey, innerText, fix)); +}; + +// message를 confirm +export const gConfirm = (msgGroup, msgKey, innerText, fix) => { + return window.confirm(getMessage(msgGroup, msgKey, innerText, fix)); +}; + +// message를 함수 리턴 +export const gMessage = getMessage; + +// message를 save후 응답까지 처리 +export const gSaveCommonConfirm = async ({ message, api = async () => {}, param = {}, success, fail, prefix }) => { + try { + if (!gConfirm('CONFIRM', `${message}_QUEST`, null, { prefix })) return; + + const result = await api(param, { isAlert: false }); + + if (result?.code !== 200) return; + + const rep = message === 'UPLOAD' ? result.data.data : result.data; + + if (rep > 0) { + gAlert('INFO', `${message}_COMPLETE`); + await success?.(); + } else { + gAlert('ERROR', `${message}_FAIL`); + await fail?.(); + } + } catch (error) { + console.error('오류 발생:', error); // 시큐어코딩 관련 수정 : 오류 정보를 로그로 출력 + } +}; + +export const nullToValue = (text, { value = '-', prefix = '', suffix = '' } = {}) => { + if (!text || text === value) { + return value; + } + return `${prefix}${text}${suffix}`; +}; + +export const filterArrayOfObjectsByKeys = (arrayOfObjects, keys) => { + return arrayOfObjects.map(obj => { + return keys.reduce((filteredObj, key) => { + if (obj.hasOwnProperty(key)) { + filteredObj[key] = obj[key]; + } + return filteredObj; + }, {}); + }); +}; + +export const isMobile = () => { + const width = window.innerWidth > 0 ? window.innerWidth : window.screen.width; + + return width < GLOBAL.MOBILE_SIZE; +}; diff --git a/packages/common/business_components/src/ui/search_form/index.tsx b/packages/common/business_components/src/ui/search_form/index.tsx index b7588903c..860c99c69 100644 --- a/packages/common/business_components/src/ui/search_form/index.tsx +++ b/packages/common/business_components/src/ui/search_form/index.tsx @@ -5,5 +5,5 @@ function SearchDiv ({children}:{ children: React.ReactNode; }) { </div> ) } - + export { SearchDiv }; diff --git a/packages/common/business_components/src/ui/search_icon/index.tsx b/packages/common/business_components/src/ui/search_icon/index.tsx new file mode 100644 index 000000000..0ef0e17e3 --- /dev/null +++ b/packages/common/business_components/src/ui/search_icon/index.tsx @@ -0,0 +1,18 @@ +export default function SearchIcon() { + return ( + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + strokeWidth={1.5} + stroke="currentColor" + className="w-6 h-6 text-gray-700" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + d="M21 21l-4.35-4.35M17 11A6 6 0 105 11a6 6 0 0012 0z" + /> + </svg> + ); + } \ No newline at end of file diff --git a/packages/common/business_components/src/ui/table/index-2.tsx b/packages/common/business_components/src/ui/table/index-2.tsx new file mode 100644 index 000000000..f4f4163f8 --- /dev/null +++ b/packages/common/business_components/src/ui/table/index-2.tsx @@ -0,0 +1,111 @@ +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components"; +import { useEffect, useState } from "react"; +import UiPageNations from "./uiPageNations"; + +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; + cur?: number; +} + +type UiHeadProps = { + id: string; + title: string; + align?: string; +} + +export function UiTable2({ tableData, headlist, pageSize, total, children, headName, cellClick, onPageChange, className = '', resetPageNo = false, cur }: UiTableProps) { + const [idx, setIdx] = useState<number>(-1); + + const [curState, setCurState] = useState<number>(cur ?? 1); + useEffect(() => { + setCurState(cur ?? 1); + }, [cur]); + + useEffect(() => { + if (resetPageNo) { + setCurState(1); + onPageChange(1); + } + }, [onPageChange, resetPageNo]); + + const handlePageChange = (page: number) => { + setCurState(page); + onPageChange(page); + } + + const handleCellClick = (index: number, headId: string) => { + if (cellClick) { + cellClick(index, headId); + setIdx(index); + } + }; + + const startIdx = (curState - 1) * pageSize; + const endIdx = startIdx + pageSize; + const pageData = tableData.slice(startIdx, endIdx); + + return ( + <> + {headName ? ( + <div className="card-header"> + <h3 className="text-2xl font-medium text-label">{headName}</h3> + </div> + ) : null} + <div className={`card !m-[1.2rem] !p-0r w-full relative ${className}`}> + <div> + <Table variant="secondary" className="w-full"> + {/* 스크롤 관련 div 제거 */} + <TableHeader className="bg-white"> + {children ? ( + <>{children}</> + ) : ( + <TableRow> + {headlist?.map((head) => ( + <TableHead key={head.id} + className="min-w-[120px] max-w-[180px] w-[150px] + text-center whitespace-nowrap overflow-hidden + text-ellipsis bg-white"> + {head.title} + </TableHead> + ))} + </TableRow> + )} + </TableHeader> + <TableBody className='text-center'> + {pageData.map((el: { [key: string]: string | number | boolean | object }, index) => ( + <TableRow key={index + "row"}> + {headlist?.map((head) => ( + <TableCell + className={`${head.align === 'right' ? 'text-right' : head.align === 'center' ? 'text-center' : typeof el[head.id] === 'number' ? 'text-right' : 'text-center'} ${cellClick && idx === index ? '!font-[1000] cursor-pointer' : + cellClick ? 'cursor-pointer' : '' + }`} + onClick={() => handleCellClick(startIdx + index, head.id)} + key={head.id + index} + > + {typeof el[head.id] === 'number' ? String(el[head.id]).replace(/\B(?=(\d{3})+(?!\d))/g, ",") : String(el[head.id] ?? '')} + </TableCell> + ))} + </TableRow> + ))} + </TableBody> + </Table> + </div> + {/* 페이지네이션을 sticky 하단에 고정 */} + {total > 0 && ( + <div className="sticky left-0 right-0 bottom-0 bg-white z-20 border-t border-gray-200"> + <UiPageNations total={total} cur={curState} pageSize={pageSize} onPageChange={handlePageChange} /> + </div> + )} + </div> + </> + ); +} diff --git a/packages/common/business_components/tsconfig.json b/packages/common/business_components/tsconfig.json index 8b065d297..20f5a6648 100644 --- a/packages/common/business_components/tsconfig.json +++ b/packages/common/business_components/tsconfig.json @@ -30,5 +30,5 @@ "**/*.mjs", "**/*.jsx", "**/*.json" - ] +, "../../../apps/sn/app/map/SearchModal.tsx" ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 36971ffb1..88dda5dea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -255,6 +255,106 @@ importers: specifier: ^5 version: 5.6.3 + apps/sn: + dependencies: + '@common/assets': + specifier: workspace:* + version: link:../../packages/common/assets + '@common/business_components': + specifier: workspace:* + version: link:../../packages/common/business_components + '@common/components': + specifier: workspace:* + version: link:../../packages/common/components + '@frontend-opensource/use-react-hooks': + specifier: ^1.1.4 + version: 1.1.4 + '@hookform/resolvers': + specifier: ^4.1.3 + version: 4.1.3(react-hook-form@7.54.2) + '@radix-ui/react-label': + specifier: ^2.1.2 + version: 2.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-slot': + specifier: ^1.1.2 + version: 1.1.2(@types/react@18.3.18)(react@18.3.1) + '@tanstack/react-query': + specifier: ^5.67.1 + version: 5.67.1(react@18.3.1) + '@tanstack/react-table': + specifier: ^8.21.2 + version: 8.21.2(react-dom@18.3.1)(react@18.3.1) + axios: + specifier: ^1.8.1 + version: 1.8.1 + chart.js: + specifier: ^4.4.8 + version: 4.4.8 + dayjs: + specifier: ^1.11.13 + version: 1.11.13 + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 + next: + specifier: 15.1.7 + version: 15.1.7(react-dom@18.3.1)(react@18.3.1) + react-chartjs-2: + specifier: ^5.3.0 + version: 5.3.0(chart.js@4.4.8)(react@18.3.1) + react-hook-form: + specifier: ^7.54.2 + version: 7.54.2(react@18.3.1) + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.4.17) + zod: + specifier: ^3.24.2 + version: 3.24.2 + zustand: + specifier: ^5.0.3 + version: 5.0.3(@types/react@18.3.18)(react@18.3.1) + devDependencies: + '@eslint/eslintrc': + specifier: ^3 + version: 3.3.0 + '@faker-js/faker': + specifier: ^9.5.0 + version: 9.5.0 + '@types/js-cookie': + specifier: ^3.0.6 + version: 3.0.6 + '@types/node': + specifier: ^22.13.1 + version: 22.13.9 + '@types/webpack': + specifier: ^5.28.5 + version: 5.28.5 + eslint: + specifier: ^9 + version: 9.21.0 + eslint-config-next: + specifier: 15.1.7 + version: 15.1.7(eslint@9.21.0)(typescript@5.6.3) + postcss: + specifier: ^8 + version: 8.5.3 + prettier: + specifier: ^3.4.2 + version: 3.5.2 + prettier-plugin-tailwindcss: + specifier: ^0.6.11 + version: 0.6.11(prettier@3.5.2) + steiger: + specifier: ^0.5.5 + version: 0.5.5(typescript@5.6.3) + tailwindcss: + specifier: ^3.4.1 + version: 3.4.17 + typescript: + specifier: ^5 + version: 5.6.3 + apps/wr: dependencies: '@common/business_components': @@ -1455,6 +1555,7 @@ packages: /@faker-js/faker@9.5.0: resolution: {integrity: sha512-3qbjLv+fzuuCg3umxc9/7YjrEXNaKwHgmig949nfyaTx8eL4FAsvFbu+1JcFUj1YAXofhaDn6JdEUBTYuk0Ssw==} engines: {node: '>=18.0.0', npm: '>=9.0.0'} + deprecated: Please update to a newer version dev: true /@feature-sliced/filesystem@2.4.0: