diff --git a/.gitignore b/.gitignore index d0bb49da9..6b685ea40 100644 --- a/.gitignore +++ b/.gitignore @@ -18,5 +18,5 @@ apps/wr/.next/**/* apps/fo/.next/**/* # env -.env* +#.env* .qodo diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index c3f502a19..000000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# 디폴트 무시된 파일 -/shelf/ -/workspace.xml -# 에디터 기반 HTTP 클라이언트 요청 -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/apps/cs/app/(with-base-layout)/link/page.tsx b/apps/cs/app/(with-base-layout)/link/page.tsx new file mode 100644 index 000000000..c703d7fcf --- /dev/null +++ b/apps/cs/app/(with-base-layout)/link/page.tsx @@ -0,0 +1,2 @@ +import { LinkPage } from "@/pages/link"; +export default LinkPage; \ No newline at end of file diff --git a/apps/cs/app/(with-base-layout)/link/register/[id]/page.tsx b/apps/cs/app/(with-base-layout)/link/register/[id]/page.tsx new file mode 100644 index 000000000..b54d60cec --- /dev/null +++ b/apps/cs/app/(with-base-layout)/link/register/[id]/page.tsx @@ -0,0 +1,10 @@ +import { ModifyLinkPage as VModifyLinkPage } from "@/pages/link"; + +export default async function ModifyLinkPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + return ; +}; \ No newline at end of file diff --git a/apps/cs/app/(with-base-layout)/link/register/page.tsx b/apps/cs/app/(with-base-layout)/link/register/page.tsx new file mode 100644 index 000000000..a7be897af --- /dev/null +++ b/apps/cs/app/(with-base-layout)/link/register/page.tsx @@ -0,0 +1,2 @@ +import { RegistLinkPage } from "@/pages/link"; +export default RegistLinkPage; \ No newline at end of file diff --git a/apps/cs/app/(with-base-layout)/linkLog/page.tsx b/apps/cs/app/(with-base-layout)/linkLog/page.tsx new file mode 100644 index 000000000..bf10b7ba0 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/linkLog/page.tsx @@ -0,0 +1,2 @@ +import { LinkLogPage } from "@/pages/linkLog"; +export default LinkLogPage; \ No newline at end of file diff --git a/apps/cs/app/(with-base-layout)/linkParams/page.tsx b/apps/cs/app/(with-base-layout)/linkParams/page.tsx new file mode 100644 index 000000000..a9e250bac --- /dev/null +++ b/apps/cs/app/(with-base-layout)/linkParams/page.tsx @@ -0,0 +1,2 @@ +import { LinkParamsPage } from "@/pages/linkParams"; +export default LinkParamsPage; \ No newline at end of file diff --git a/apps/cs/app/(with-base-layout)/linkStats/page.tsx b/apps/cs/app/(with-base-layout)/linkStats/page.tsx new file mode 100644 index 000000000..78acc9fc3 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/linkStats/page.tsx @@ -0,0 +1,2 @@ +import { LinkStatsPage } from "@/pages/linkStats"; +export default LinkStatsPage; \ No newline at end of file diff --git a/apps/cs/app/(with-base-layout)/linkTable/page.tsx b/apps/cs/app/(with-base-layout)/linkTable/page.tsx new file mode 100644 index 000000000..df14e95e3 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/linkTable/page.tsx @@ -0,0 +1,2 @@ +import { LinkTablePage } from "@/pages/linkTable"; +export default LinkTablePage; \ No newline at end of file diff --git a/apps/cs/src/entities/link/api/link-service.ts b/apps/cs/src/entities/link/api/link-service.ts new file mode 100644 index 000000000..c607dd2c7 --- /dev/null +++ b/apps/cs/src/entities/link/api/link-service.ts @@ -0,0 +1,191 @@ +import axios from "axios"; +import { + type LinkService, + type LinkSearchParam, + type LinkList, + type LinkFormData, + type LinkInfo, + type LinkLogList, + type LinkStatsList, + type LinkVrblList, + type LinkVrblFormData, + type LinkVrbl, + type LinkTable, + type LinkTableColumnsList, + type LinkTableColumns, + type LinkTableColData, + type LinkTableFormData, +} from "../model/link-interface"; + +export class LinkApiService implements LinkService { + public async getLinkList(params: LinkSearchParam): Promise { + const response = await axios.get( + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, + { params } + ); + return response.data; + } + + public async getLinkInfo(linkSn: string): Promise { + const response = await axios.get( + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkInfo/${linkSn}`, + ); + return response.data; + } + + public async linkQuickStart(linkSn: string): Promise { + const response = await axios.patch( + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/linkQuickStart/${linkSn}`, + ); + return response.data; + } + + public async updateLinkInfo(data: LinkFormData): Promise { + await axios.put( + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/updateLinkInfo`, + data, + ); + } + + public async insertLinkInfo(data: LinkFormData): Promise { + await axios.post( + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/insertLinkInfo`, + data, + ); + } + + public async deleteLink(linkSn: string): Promise { + await axios.delete( + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/deleteLink/${linkSn}`, + ); + } + + public async getLinkLogList(params: LinkSearchParam): Promise { + const response = await axios.get( + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/selectLinkLoglist`, + { params } + ); + return response.data; + } + + public async getLinkStatsList(params: LinkSearchParam): Promise { + const response = await axios.get( + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/selectLinkStats`, + { params } + ); + return response.data; + } + + public async getLinkVrblList(linkSn: string): Promise { + const response = await axios.get( + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkVrblList/${linkSn}`, + ); + return response.data; + } + + public async insertLinkVrbl(data: LinkVrblFormData): Promise { + const response = await axios.post( + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/insertLinkVrbl`, + data, + ); + + return response.data; + } + + public async deleteLinkVrbl(data: string[]): Promise { + await axios.delete( + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/deleteLinkVrbl`, + { + data + } + ); + } + + public async getLinkVrblInfo(linkVrblSn: string): Promise { + const response = await axios.get( + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkVrblInfo/${linkVrblSn}`, + ); + return response.data; + } + + public async updateLinkVrblInfo(data: LinkVrblFormData): Promise { + await axios.put( + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/updateLinkVrblInfo`, + data, + ); + } + + public async getLinkTableInfo(linkSn: string): Promise { + const response = await axios.get( + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo/${linkSn}`, + ); + return response.data; + } + + public async getLinkTableColList(linkSn: string): Promise { + const response = await axios.get( + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList/${linkSn}`, + ); + return response.data; + } + + public async getLinkTableColInfo(params: { colSn: string; tblSn:string; }): Promise { + const response = await axios.get( + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColInfo`, + { params } + ); + return response.data; + } + + public async insertLinkTableCol(data: LinkTableColData): Promise { + await axios.post( + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/insertLinkTableCol`, + data + ); + } + + public async deleteLinkTableCol(data: string[]): Promise { + await axios.delete( + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/deleteLinkTableCol`, + { + data + }, + ); + } + + public async updateLinkTableCol(data: LinkTableColData): Promise { + axios.put( + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/updateLinkTableCol`, + data, + ); + } + + public async insertLinkTableInfo(data: LinkTableFormData): Promise { + await axios.post( + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/insertLinkTableInfo`, + data + ); + } + + public async updateLinkTableInfo(data: LinkTableFormData): Promise { + await axios.put( + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/updateLinkTableInfo`, + data + ); + } + + public async countLinkTblNm(tblNm: string): Promise<{count: number, tbl_sn: string}> { + const response = await axios.get<{count: number, tbl_sn: string}>( + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/countLinkTblNm/${tblNm}`, + ); + return response.data; + } + + public async updateLinkResultCode(linkSn: string): Promise { + await axios.patch( + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/updateLinkResultCode/${linkSn}`, + ); + } +} + +export const LinkApi = new LinkApiService(); \ No newline at end of file diff --git a/apps/cs/src/entities/link/index.ts b/apps/cs/src/entities/link/index.ts new file mode 100644 index 000000000..89d56aaa0 --- /dev/null +++ b/apps/cs/src/entities/link/index.ts @@ -0,0 +1,18 @@ +export { LinkApi } from "./api/link-service"; +export { + type LinkSearchParam, + type Link, + type LinkFormData, + type LinkLog, + type LinkStats, + type LinkVrbl, + type LinkVrblFormData, + type LinkTable, + type LinkTableColumns, + type LinkTableColData, + type LinkTableFormData, + linkFormSchema, + linkVrblFormSchema, + LinkTableColumnsSchema, + LinkTableSchema, +} from "./model/link-interface" \ No newline at end of file diff --git a/apps/cs/src/entities/link/model/link-interface.ts b/apps/cs/src/entities/link/model/link-interface.ts new file mode 100644 index 000000000..c4493a6c0 --- /dev/null +++ b/apps/cs/src/entities/link/model/link-interface.ts @@ -0,0 +1,235 @@ +import { z } from "zod"; +import type { PageableParams, PageableList } from "@/shared/model"; + +export interface Link { + link_sn: string; + link_nm: string; + link_clsf_cd: string; + link_url: string; + link_srvr_ip: string; + link_db_cd: string; + link_key: string; + link_id: string; + link_pswd: string; + link_cycle_stng: string; + srvc_usg: string; + last_link_rslt_cd: string; + last_link_excn_dt: string; + next_link_excn_dt: string; + last_link_err_cd: string; + mdfcn_dt: string; + mdfr: string; + link_batch_time: string; +} + +export interface LinkLog { + rownum: number; + link_sn: string; + link_nm: string; + link_rslt_cd: string; + err_cn: string; + link_err_cd: string; + log_crt_dt: string; +} + +export interface LinkInfo extends LinkFormData { + link_sn: string; + link_nm: string; + link_clsf_cd: string; + link_url: string; + link_srvr_ip: string; + link_db_cd: string; + link_key: string; + link_id: string; + link_pswd: string; + srvc_usg: string; + last_link_rslt_cd: string; + last_link_excn_dt: string; + next_link_excn_dt: string; + last_link_err_cd: string; + mdfcn_dt: string; + mdfr: string; + link_batch_time: number; +} + +export interface LinkStats { + link_sn: string; + link_nm: string; + count: number; +} + +export interface LinkVrbl { + link_vrbl_sn: string; + link_vrbl_nm: string; + link_vrbl_vl: string; + link_vrbl_cd: string; + link_vrbl_expln: string; + mdfcn_dt: string; + mdfr: string; +} + +export interface LinkTable { + link_nm: string; + link_sn: string; + tbl_sn: string; + sou_tbl_nm: string; + sou_tbl_korn_nm: string; + sou_tbl_expln: string; + trsf_tbl_nm: string; + trsf_tbl_korn_nm: string; + trsf_tbl_expln: string; +} + +export interface LinkTableColumns { + tbl_sn: string; + col_sn: string; + sou_col_nm: string; + sou_col_korn_nm: string; + sou_col_data_frm: string; + sou_col_sz: number; + trsf_col_nm: string; + trsf_col_korn_nm: string; + trsf_col_data_frm: string; + trsf_col_sz: number + pk_yn: string; + col_seq: number; + mdfcn_dt: string; + mdfr: string; +} + +export const linkFormSchema = z.object({ + link_clsf_cd: z.string().max(8).nullable().refine((val) => { + // 빈 값이나 "none" 값이면 false 반환 + return val && val !== "none"; + }, { + message: "연계 분류를 선택해주세요" + }), + link_sn: z.string().max(50).nullable().optional(), + link_nm: z.string().max(40, { message: '연계명은 40자 이하로 입력해 주세요.' }).nonempty({ message: '연계명은 필수입니다.' }), + link_url: z.string().max(1000, { message: '연계 URL은 1000자 이하로 입력해 주세요.' }).nonempty({ message: '연계 URL은 필수입니다.' }), + link_srvr_ip: z.string().max(20).nullable().optional(), + link_db_cd: z.string().max(10).nullable().optional(), + link_key: z.string().max(100, { message: '연계 KEY는 100자 이하로 입력해 주세요.' }).nonempty({ message: '연계 KEY는 필수입니다.' }), + link_id: z.string().max(20).nullable().optional(), + link_pswd: z.string().max(128).nullable().optional(), + srvc_usg: z.string().max(40).nullable().optional(), + last_link_rslt_cd: z.string().max(4).nullable().optional(), + last_link_excn_dt: z.string().max(40).nullable().optional(), + next_link_excn_dt: z.string().max(40).nullable().optional(), + last_link_err_cd: z.string().max(10).nullable().optional(), + mdfcn_dt: z.string().nullable().optional(), + mdfr: z.string().nullable().optional(), + link_batch_time: z.coerce.number().nullable().optional().refine((val) => { + if (val == null) return true; + return val >= 5; + }, { + message: "연계 주기는 5분 이상이어야 합니다." + }), +}); + +export const linkVrblFormSchema = z.object({ + link_sn: z.string().max(50).nullable().optional(), + link_vrbl_sn: z.string().max(50).nullable().optional(), + link_vrbl_nm: z.string().max(40, { message: '변수명은 40자 이하로 입력해 주세요.' }).nonempty({ message: '변수명은 필수입니다.' }), + link_vrbl_expln: z.string().max(4000, { message: '설명은 400자 이하로 입력해 주세요.' }).nullable().optional(), + link_vrbl_cd: z.string().max(10, { message: '변수코드는 10자 이하로 입력해 주세요.' }).nullable().optional(), + link_vrbl_vl: z.string().max(1000, { message: '변수값은 1000자 이하로 입력해 주세요.' }).nonempty({ message: '변수값은 필수입니다.' }), + mdfcn_dt: z.string().nullable().optional(), + mdfr: z.string().nullable().optional(), +}); + +export const LinkTableColumnsSchema = z.object({ + link_sn: z.string().max(50).nullable().optional(), + tbl_sn: z.string().max(50).nullable().optional(), + col_sn: z.string().max(50).nullable().optional(), + sou_col_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nullable().optional(), + sou_col_korn_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nullable().optional(), + sou_col_data_frm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nullable().optional(), + sou_col_sz: z.coerce.number().nullable().optional().refine((val) => { + if (val == null) return true; + return val.toString().replace('.', '').length <= 14; + }, { + message: '최대 14자리까지 입력 가능합니다.', + }), + pk_yn: z.string().max(1).nonempty(), + trsf_col_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nonempty({ message: '변환컬럼명은 필수입니다.' }), + trsf_col_korn_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nonempty({ message: '변환컬럼 한글명은 필수입니다.' }), + trsf_col_data_frm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nonempty({ message: '변환컬럼 데이터 형식은 필수입니다.' }), + trsf_col_sz: z.coerce.number().nullable().optional().refine((val) => { + if (val == null) return true; + return val.toString().replace('.', '').length <= 14; + }, { + message: '최대 14자리까지 입력 가능합니다.', + }), + col_seq: z.coerce.number().nullable().optional().refine((val) => { + if (val == null) return true; + return val > 0; + }, { + message: "컬럼 매칭 순서는 0 이상이어야 합니다." + }), + mdfcn_dt: z.string().nullable().optional(), + mdfr: z.string().nullable().optional(), +}); + +export const LinkTableSchema = z.object({ + link_sn: z.string().max(50).nullable().optional(), + tbl_sn: z.string().max(50).nullable().optional(), + link_nm: z.string().max(50).nullable().optional(), + sou_tbl_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nullable().optional(), + sou_tbl_korn_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nullable().optional(), + sou_tbl_expln: z.string().max(2000, { message: '설명은 2000자 이하로 입력해 주세요.' }).nullable().optional(), + trsf_tbl_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nonempty({ message: '변환테이블명은 필수입니다.' }), + trsf_tbl_korn_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nonempty({ message: '변환테이블 한글명은 필수입니다.' }), + trsf_tbl_expln: z.string().max(2000, { message: '설명은 2000자 이하로 입력해 주세요.' }).nullable().optional(), + mdfcn_dt: z.string().nullable().optional(), + mdfr: z.string().nullable().optional(), +}); + +interface LinkStatsListObject { + linkSuccStats: LinkStats[] | null; + linkFailStats: LinkStats[] | null; +} + +export interface LinkSearchParam extends PageableParams { + linkSn?: string; + linkNm?: string; + startDate?: string; + endDate?: string; + isSucc?: string; +} + +export type LinkFormData = z.infer; +export type LinkVrblFormData = z.infer; +export type LinkTableColData = z.infer; +export type LinkTableFormData = z.infer; +export type LinkList = PageableList; +export type LinkLogList = PageableList; +export type LinkStatsList = LinkStatsListObject; +export type LinkVrblList = PageableList; +export type LinkTableColumnsList = PageableList; + +export interface LinkService { + getLinkList(params: LinkSearchParam): Promise; + getLinkInfo(params: string): Promise; + linkQuickStart(params: string): Promise; + updateLinkInfo(data: LinkFormData): Promise; + deleteLink(params: string): Promise; + getLinkLogList(params: LinkSearchParam): Promise; + getLinkStatsList(params: LinkSearchParam): Promise; + getLinkVrblList(params: string): Promise; + insertLinkVrbl(data: LinkVrblFormData): Promise; + deleteLinkVrbl(data: string[]): Promise; + getLinkVrblInfo(params: string): Promise; + updateLinkVrblInfo(data: LinkVrblFormData): Promise; + getLinkTableInfo(params: string): Promise; + getLinkTableColList(params: string): Promise; + getLinkTableColInfo(params: { colSn: string; tblSn: string; }): Promise; + insertLinkTableCol(data: LinkTableColData): Promise; + deleteLinkTableCol(data: string[]): Promise; + updateLinkTableCol(data: LinkTableColData): Promise; + insertLinkTableInfo(data: LinkTableFormData): Promise; + updateLinkTableInfo(data: LinkTableFormData): Promise; + insertLinkInfo(data: LinkFormData): Promise; + countLinkTblNm(tblNm: string): Promise<{ count: number, tbl_sn: string }>; + updateLinkResultCode(linkSn: string): Promise; +} \ No newline at end of file diff --git a/apps/cs/src/pages/link/index.ts b/apps/cs/src/pages/link/index.ts new file mode 100644 index 000000000..6eac7d15c --- /dev/null +++ b/apps/cs/src/pages/link/index.ts @@ -0,0 +1,3 @@ +export { ModifyLinkPage } from "./ui/modify-page"; +export { RegistLinkPage } from "./ui/regist-page"; +export { LinkPage } from "./ui/link-page"; \ No newline at end of file diff --git a/apps/cs/src/pages/link/model/table-columns.tsx b/apps/cs/src/pages/link/model/table-columns.tsx new file mode 100644 index 000000000..5b1710fec --- /dev/null +++ b/apps/cs/src/pages/link/model/table-columns.tsx @@ -0,0 +1,88 @@ +import dayjs from "dayjs"; +import { createColumnHelper } from "@tanstack/react-table"; +import { type Link } from "@/entities/link"; +import QuickStartButton from "../ui/QuickStartButton"; + +const columnHelper = createColumnHelper(); + +const convertMinutesToPeriod = (minutes: number): string => { + if (!minutes) return ''; + + const year = Math.floor(minutes / (525600)); // 365일 * 24시간 * 60분 + minutes = minutes % 525600; + + const month = Math.floor(minutes / (43200)); // 30일 * 24시간 * 60분 + minutes = minutes % 43200; + + const day = Math.floor(minutes / 1440); // 24시간 * 60분 + minutes = minutes % 1440; + + const hour = Math.floor(minutes / 60); + const minute = minutes % 60; + + const parts = []; + if (year > 0) parts.push(`${year}년`); + if (month > 0) parts.push(`${month}개월`); + if (day > 0) parts.push(`${day}일`); + if (hour > 0) parts.push(`${hour}시간`); + if (minute > 0) parts.push(`${minute}분`); + + return parts.join(' '); +}; + +const columns = [ + columnHelper.accessor((_, index) => index + 1, { + id: 'no', + header: 'NO', + }), + columnHelper.accessor('link_sn', { + id: 'link_sn', + header: '연계일련번호', + }), + columnHelper.accessor('link_nm', { + id: 'link_nm', + header: '연계 명', + }), + columnHelper.accessor('link_url', { + id: 'link_url', + header: '연계 URL', + cell: (info) => (info.getValue() ? info.getValue() : '-'), + }), + columnHelper.accessor('link_srvr_ip', { + id: 'link_srvr_ip', + header: '연계서버 IP', + cell: (info) => (info.getValue() ? info.getValue() : '-'), + }), + columnHelper.accessor('link_batch_time', { + id: 'link_batch_time', + header: '연계 주기', + cell: (info) => convertMinutesToPeriod(Number(info.getValue())), + }), + columnHelper.accessor('srvc_usg', { + id: 'srvc_usg', + header: '서비스용도', + cell: (info) => (info.getValue() ? info.getValue() : '-'), + }), + columnHelper.display({ + id: 'last_link_rslt_cd', + header: '연계상태', + cell: (info) => { + const resultCd = info.row.original.last_link_rslt_cd; + return {resultCd === 'SUCC' ? '성공' : (resultCd === 'FAIL' ? '실패' : '대기')} + }, + }), + columnHelper.accessor('last_link_excn_dt', { + id: 'last_link_excn_dt', + header: '최종연계일', + cell: (info) => (info.getValue() ? dayjs(info.getValue()).format('YYYY-MM-DD HH:mm') : '-'), + }), + columnHelper.display({ + id: 'quick_start_btn', + header: '즉시 실행', + cell: (info) => { + return + } + }) +]; + +export { columns }; \ No newline at end of file diff --git a/apps/cs/src/pages/link/ui/QuickStartButton.tsx b/apps/cs/src/pages/link/ui/QuickStartButton.tsx new file mode 100644 index 000000000..a322ee678 --- /dev/null +++ b/apps/cs/src/pages/link/ui/QuickStartButton.tsx @@ -0,0 +1,36 @@ +import { useMutation } from "@tanstack/react-query"; +import { LinkApi } from "@/entities/link"; +import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { Button } from "@common/components"; + +const QuickStartButton: React.FC<{ linkSn: string }> = ({ linkSn }) => { + const { setMessage: alert } = useAlertStore((state) => state); + + const { mutateAsync: updateQuickStart } = useMutation({ + mutationFn: (linkSn: string) => LinkApi.linkQuickStart(linkSn), + onSuccess: (result) => { + if (result === 'succ') { + alert('즉시 실행 요청이 완료되었습니다.'); + } else { + alert('기존 요청이 실행 대기중입니다.'); + } + }, + onError: () => { + alert('즉시 실행 요청을 실패했습니다.'); + }, + }); + + return ( + + ); +}; + +export default QuickStartButton; \ No newline at end of file diff --git a/apps/cs/src/pages/link/ui/link-form.tsx b/apps/cs/src/pages/link/ui/link-form.tsx new file mode 100644 index 000000000..05a3da0f1 --- /dev/null +++ b/apps/cs/src/pages/link/ui/link-form.tsx @@ -0,0 +1,590 @@ +import { + Button, + Input, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Select, + SelectTrigger, + SelectContent, + SelectItem, + SelectValue, +} from "@common/components"; +import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form"; +import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; +import type { UseFormReturn } from "react-hook-form"; +import { type LinkFormData } from "@/entities/link"; +import dayjs from "dayjs"; +import { useRouter } from "next/navigation"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { useState } from "react"; + + +type LinkFormProps = { + form: UseFormReturn; + linkSn?: string; + updateLinkInfo?: (data: LinkFormData) => void; + handleInsertLink?: (data: LinkFormData) => void; + deleteLink?: () => void; + updateLinkResultCode?: () => void; +} + +const LinkForm = ({ form, linkSn, updateLinkInfo, handleInsertLink, deleteLink, updateLinkResultCode }: LinkFormProps) => { + const isModifyMode = !!linkSn; + const router = useRouter(); + + const { confirm } = useConfirm(); + const [onConfirm, setOnConfirm] = useState(false); + + const handleConfirm = async (callback: () => Promise) => { + setOnConfirm(true); + await callback(); + setOnConfirm(false); + }; + + const handleSubmit = async (data: LinkFormData) => { + const confirmMsg = !!linkSn ? '수정 하시겠습니까?' : '저장 하시겠습니까?'; + + await handleConfirm(async () => { + + if (await confirm(confirmMsg)) { + if (linkSn) { + updateLinkInfo?.(data); + } else { + handleInsertLink?.(data); + } + } + }); + }; + + const handleDelete = async () => { + await handleConfirm(async () => { + if (await confirm('삭제하시겠습니까?')) { + deleteLink?.(); + } + }) + }; + + const convertMinutesToPeriod = (minutes: number) => { + if (!minutes) return { + year: 0, + month: 0, + day: 0, + hour: 0, + minute: 0 + }; + + const year = Math.floor(minutes / (525600)); // 365일 * 24시간 * 60분 + minutes = minutes % 525600; + + const month = Math.floor(minutes / (43200)); // 30일 * 24시간 * 60분 + minutes = minutes % 43200; + + const day = Math.floor(minutes / 1440); // 24시간 * 60분 + minutes = minutes % 1440; + + const hour = Math.floor(minutes / 60); + const minute = minutes % 60; + + const timeParts = { + year: year, + month: month, + day: day, + hour: hour, + minute: minute, + }; + + return timeParts; + }; + + return ( + <> +
+ +
+ + + + + + + {isModifyMode && + + 연계 일련번호 + + ( + + + + + + )} + /> + + + } + + * 연계 명 + + ( + + + + + {form.formState.errors.link_nm && ( +

+ {form.formState.errors.link_nm.message} +

+ )} +
+ )} + /> +
+
+ + * 연계 분류 + + ( + + + + + {form.formState.errors.link_clsf_cd && ( +

+ {form.formState.errors.link_clsf_cd.message} +

+ )} +
+ )} + /> +
+
+ + * 연계 URL + + ( + + + + + {form.formState.errors.link_url && ( +

+ {form.formState.errors.link_url.message} +

+ )} +
+ )} + /> +
+
+ + 연계서버 IP + + ( + + + + + + )} + /> + + + + * 연계 KEY + + ( + + + + + {form.formState.errors.link_key && ( +

+ {form.formState.errors.link_key.message} +

+ )} +
+ )} + /> +
+
+ + 연계 ID + + ( + + + + + + )} + /> + + + + 연계 비밀번호 + + ( + + + + + + )} + /> + + + + 서비스 용도 + + ( + + + + + + )} + /> + + + + 연계 주기 + + { + const timeParts = convertMinutesToPeriod(Number(field.value)); + return ( + + +
+
+ { + const total = (Number(e.target.value) * 525600) + + (timeParts.month * 43200) + + (timeParts.day * 1440) + + (timeParts.hour * 60) + + timeParts.minute; + field.onChange(total); + }} + /> + 년 +
+
+ { + const total = (timeParts.year * 525600) + + (Number(e.target.value) * 43200) + + (timeParts.day * 1440) + + (timeParts.hour * 60) + + timeParts.minute; + field.onChange(total); + }} + /> + 월 +
+
+ { + const total = (timeParts.year * 525600) + + (timeParts.month * 43200) + + (Number(e.target.value) * 1440) + + (timeParts.hour * 60) + + timeParts.minute; + field.onChange(total); + }} + /> + 일 +
+
+ { + const total = (timeParts.year * 525600) + + (timeParts.month * 43200) + + (timeParts.day * 1440) + + (Number(e.target.value) * 60) + + timeParts.minute; + field.onChange(total); + }} + /> + 시간 +
+
+ { + const total = (timeParts.year * 525600) + + (timeParts.month * 43200) + + (timeParts.day * 1440) + + (timeParts.hour * 60) + + Number(e.target.value); + field.onChange(total); + }} + /> + 분 +
+
+
+ {form.formState.errors.link_batch_time && ( +

+ {form.formState.errors.link_batch_time.message} +

+ )} +
+ ); + }} + /> +
+
+ {isModifyMode && + <> + + 연계 상태 + + { + return ( + + +
+ + +
+
+
+ ); + }} + /> +
+
+ + 최종 연계 실행일 + + ( + + + + + + )} + /> + + + + 다음 연계 실행일 + + ( + + + + + + )} + /> + + + + 수정자 + + ( + + + + + + )} + /> + + + + 수정일시 + + ( + + + + + + )} + /> + + + + } +
+
+
+
+ + + {isModifyMode && ( + + )} +
+
+ + + + ) +}; + +export default LinkForm; \ No newline at end of file diff --git a/apps/cs/src/pages/link/ui/link-page.tsx b/apps/cs/src/pages/link/ui/link-page.tsx new file mode 100644 index 000000000..95a9cc043 --- /dev/null +++ b/apps/cs/src/pages/link/ui/link-page.tsx @@ -0,0 +1,134 @@ +"use client"; + +import "@common/assets/styles/grid.css"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { Button, Input, Label, LoadingWithAnimation } from "@common/components"; +import { getCoreRowModel } from "@tanstack/react-table"; +import { LinkApi, type Link, type LinkSearchParam } from "@/entities/link"; +import { useQuery } from "@tanstack/react-query"; +import { columns } from "../model/table-columns"; +import { useQueryClient } from "@tanstack/react-query"; +import { useDataTable } from "@/shared/lib"; + +function LinkPage() { + const router = useRouter(); + const queryClient = useQueryClient(); + + const [linkSn, setLinkSn] = useState(''); + const [linkNm, setLinkNm] = useState(''); + + const [linkSearchParam, setLinkSearchParam] = useState({ + linkSn: '', + linkNm: '' + }); + + const query = useQuery({ + queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, linkSearchParam], + queryFn: () => LinkApi.getLinkList(linkSearchParam), + }); + + const handleResetSearchParam = () => { + setLinkSn(''); + setLinkNm(''); + setLinkSearchParam({ + ...linkSearchParam, + linkSn: '', + linkNm: '', + }); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLinkSearchParam({ + ...linkSearchParam, + linkSn, + linkNm + }); + }; + + const handleRowClick = (link: Link) => { + queryClient.removeQueries({ + queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`], + }); + queryClient.removeQueries({ + queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkInfo`], + }); + router.push(`link/register/${link.link_sn}`); + }; + + const handleClickRegist = () => { + router.push('link/register'); + }; + + const { tableComponent } = useDataTable({ + query, + tableOptions: { + columns, + manualPagination: true, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + }, + dataTableOptions: { + onRowClick: handleRowClick, + viewPagination: true, + }, + }); + + return ( +
+
+
+ + setLinkSn(e.target.value)} + autoComplete="off" + /> + + setLinkNm(e.target.value)} + autoComplete="off" + /> + + +
+
+
+
+

연계 목록

+
+ +
+
+
+ {!query.isLoading ? +
{tableComponent}
+ : +
+
+ +
+
+ } +
+
+
+ ); +} + +export { LinkPage }; \ No newline at end of file diff --git a/apps/cs/src/pages/link/ui/modify-page.tsx b/apps/cs/src/pages/link/ui/modify-page.tsx new file mode 100644 index 000000000..594cb4c49 --- /dev/null +++ b/apps/cs/src/pages/link/ui/modify-page.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { LinkApi, linkFormSchema, type LinkFormData } from "@/entities/link"; +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import LinkForm from "./link-form"; +import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { useEffect } from "react"; +import { getUserInfo } from "../../../../../../packages/common/components/src/api"; +import { AUTH_USER_ID } from "@/shared/config"; +import { getCookie } from "@common/components"; + +function ModifyLinkPage({ linkSn }: { linkSn: string }) { + const router = useRouter(); + const queryClient = useQueryClient(); + const { setMessage: alert } = useAlertStore((state) => state); + const userUniqId = getCookie(AUTH_USER_ID) ?? ""; + + // 컴포넌트 마운트 시 캐시 초기화 + useEffect(() => { + queryClient.removeQueries({ + queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkInfo`], + }); + }, [queryClient]); + + const { data: linkInfo, refetch } = useQuery({ + queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkInfo`], + queryFn: () => LinkApi.getLinkInfo(linkSn), + enabled: !!linkSn, + }); + + const { mutateAsync: updateLinkInfo } = useMutation({ + mutationFn: (data: LinkFormData) => LinkApi.updateLinkInfo({ + ...data, + link_clsf_cd: data.link_clsf_cd !== 'none' ? data.link_clsf_cd : null, + mdfr: userUniqId + }), + onSuccess: () => { + alert('수정되었습니다.'); + router.push('/link'); + } + }); + + const { mutateAsync: deleteLink } = useMutation({ + mutationFn: () => LinkApi.deleteLink(linkSn), + onSuccess: () => { + alert('삭제되었습니다.', router.back); + } + }); + + const { mutateAsync: updateLinkResultCode } = useMutation({ + mutationFn: () => LinkApi.updateLinkResultCode(linkSn), + onSuccess: () => { + alert('대기로 전환되었습니다.'); + refetch(); + } + }); + + const fetchUserInfo = async () => { + const userData = await getUserInfo(); + return userData.userName; + }; + + const form = useForm({ + resolver: zodResolver(linkFormSchema), + defaultValues: {}, + }); + + useEffect(() => { + const applyFormData = async () => { + if (linkInfo) { + let newData = { ...linkInfo }; + + if (linkInfo.mdfr) { + const userName = await fetchUserInfo(); + newData = { + ...linkInfo, + mdfr: userName, + }; + } + + form.reset(newData); + + } + }; + + applyFormData(); + }, [linkInfo]); + + return ( + <> +
+
+

연계정보 수정

+
+ +
+ + + ); +}; + +export { ModifyLinkPage }; \ No newline at end of file diff --git a/apps/cs/src/pages/link/ui/regist-page.tsx b/apps/cs/src/pages/link/ui/regist-page.tsx new file mode 100644 index 000000000..403aefa24 --- /dev/null +++ b/apps/cs/src/pages/link/ui/regist-page.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { LinkApi, type LinkFormData, linkFormSchema } from "@/entities/link"; +import LinkForm from "./link-form"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import dayjs from "dayjs"; +import { useMutation } from "@tanstack/react-query"; +import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { useRouter } from "next/navigation"; +import { AUTH_USER_ID } from "@/shared/config"; +import { getCookie } from "@common/components"; + +function RegistLinkPage() { + const { setMessage: alert } = useAlertStore((state) => state); + const router = useRouter(); + const userUniqId = getCookie(AUTH_USER_ID) ?? ""; + + const form = useForm({ + resolver: zodResolver(linkFormSchema), + defaultValues: { + link_nm: '', + link_url: '', + link_srvr_ip: '', + link_db_cd: '', + link_key: '', + link_id: '', + link_pswd: '', + srvc_usg: '', + link_batch_time: 5, + link_clsf_cd: 'none', + }, + }); + + const { mutate: insertLinkInfo } = useMutation({ + mutationFn: (data: LinkFormData) => LinkApi.insertLinkInfo(data), + onSuccess: () => { + alert('저장되었습니다.'); + router.push('/link'); + } + }); + + const handleInsertLink = (data: LinkFormData) => { + const insertData = { + ...data, + link_clsf_cd: data.link_clsf_cd !== 'none' ? data.link_clsf_cd : null, + next_link_excn_dt: dayjs().add(Number(data.link_batch_time), 'minute').format('YYYY-MM-DD HH:mm:ss'), + mdfr: userUniqId, + }; + + insertLinkInfo(insertData); + }; + + return ( +
+
+

연계 등록

+
+ +
+ ); +}; + +export { RegistLinkPage }; \ No newline at end of file diff --git a/apps/cs/src/pages/linkLog/index.ts b/apps/cs/src/pages/linkLog/index.ts new file mode 100644 index 000000000..53ef1798e --- /dev/null +++ b/apps/cs/src/pages/linkLog/index.ts @@ -0,0 +1 @@ +export { LinkLogPage } from './ui/link-log-page'; \ No newline at end of file diff --git a/apps/cs/src/pages/linkLog/model/table-columns.tsx b/apps/cs/src/pages/linkLog/model/table-columns.tsx new file mode 100644 index 000000000..30e7fa702 --- /dev/null +++ b/apps/cs/src/pages/linkLog/model/table-columns.tsx @@ -0,0 +1,55 @@ +import dayjs from "dayjs"; +import { createColumnHelper } from "@tanstack/react-table"; +import { type LinkLog } from "@/entities/link"; + +const columnHelper = createColumnHelper(); + +const columns = [ + columnHelper.accessor('rownum', { + id: 'rownum', + header: 'NO', + }), + columnHelper.accessor('link_sn', { + id: 'link_sn', + header: '연계일련번호', + }), + columnHelper.accessor('link_nm', { + id: 'link_nm', + header: '연계 명', + }), + columnHelper.display({ + id: 'link_rslt_cd', + header: '연계실행결과', + cell: (info) => { + const isSucc = info.row.original.link_rslt_cd.trim() === 'SUCC'; + return {isSucc ? '성공' : '실패'} + }, + }), + columnHelper.accessor('link_err_cd', { + id: 'link_err_cd', + header: '연계오류코드', + cell: (info) => (info.getValue() ? info.getValue() : '-'), + }), + columnHelper.accessor('err_cn', { + id: 'err_cn', + header: '연계오류메세지', + cell: (info) => ( +
+ {info.getValue() || '-'} +
+ ), + }), + columnHelper.accessor('log_crt_dt', { + id: 'log_crt_dt', + header: '연계실행일자', + cell: (info) => { + const date = dayjs(info.getValue()); + return date.format('YYYY-MM-DD HH:mm'); + } + }), +]; + +export { columns }; \ No newline at end of file diff --git a/apps/cs/src/pages/linkLog/ui/link-log-page.tsx b/apps/cs/src/pages/linkLog/ui/link-log-page.tsx new file mode 100644 index 000000000..dd39a4726 --- /dev/null +++ b/apps/cs/src/pages/linkLog/ui/link-log-page.tsx @@ -0,0 +1,189 @@ +"use client"; + +import "@common/assets/styles/grid.css"; + +import { useState } from "react"; +import { Button, Input, Label, DatePicker, Select, SelectTrigger, SelectContent, SelectItem, SelectValue, LoadingWithAnimation } from "@common/components"; +import { useQuery } from "@tanstack/react-query"; + +import { getCoreRowModel, type PaginationState, type RowSelectionState, } from "@tanstack/react-table"; +import { LinkApi, type LinkSearchParam } from "@/entities/link"; +import { columns } from "../model/table-columns"; +import dayjs from "dayjs"; +import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { useDataTable } from "@/shared/lib"; + +function LinkLogPage() { + const { setMessage: alert } = useAlertStore((state) => state); + + const [linkSn, setLinkSn] = useState(''); + const [linkNm, setLinkNm] = useState(''); + const [isSucc, setIsSucc] = useState('ALL'); + const [startDate, setStartDate] = useState(dayjs().subtract(7, "day").format('YYYY-MM-DD')); + const [endDate, setEndDate] = useState(dayjs().format('YYYY-MM-DD')); + + const [linkSearchParam, setLinkSearchParam] = useState({ + linkSn: '', + linkNm: '', + isSucc: '', + startDate: '', + endDate: '', + }); + + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 100, + }); + + const [rowSelection, setRowSelection] = useState({}); + + const query = useQuery({ + queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, linkSearchParam, pagination], + queryFn: () => + LinkApi.getLinkLogList({ + ...linkSearchParam, + page: pagination.pageIndex, + size: pagination.pageSize, + isSucc, + startDate, + endDate, + }), + }); + + const { tableComponent } = useDataTable({ + query, + tableOptions: { + columns, + state: { + pagination, + rowSelection, + }, + manualPagination: true, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + }, + dataTableOptions: { + viewPagination: true, + }, + }); + + const handleResetSearchParam = () => { + setIsSucc('ALL'); + setLinkNm(''); + setLinkSn(''); + setStartDate(dayjs().subtract(7, "day").format('YYYY-MM-DD')); + setEndDate(dayjs().format('YYYY-MM-DD')); + setLinkSearchParam({ + ...linkSearchParam, + linkSn: '', + linkNm: '', + isSucc: 'ALL', + startDate: dayjs().subtract(7, "day").format('YYYY-MM-DD'), + endDate: dayjs().format('YYYY-MM-DD'), + }); + }; + + const handleSubmit = async (e?: React.FormEvent) => { + e?.preventDefault(); + + if (startDate && endDate) { + + if (startDate > endDate) { + alert('종료일이 시작일보다 이전일 수 없습니다.'); + return; + } else { + setPagination({ + ...pagination, + pageIndex: 0, + }); + setLinkSearchParam({ + ...linkSearchParam, + linkSn, + linkNm, + isSucc, + startDate, + endDate, + }); + } + } else { + alert('조회 날짜를 선택해주세요.'); + return; + } + }; + + return ( +
+
+
+ + setLinkSn(e.target.value)} + autoComplete="off" + /> + + setLinkNm(e.target.value)} + autoComplete="off" + /> + + + + setStartDate(dayjs(e).format('YYYY-MM-DD'))} /> + + setEndDate(dayjs(e).format('YYYY-MM-DD'))} /> + + + +
+
+
+

로그 목록 (검색 조건에 따른 건수 : {query.data?.totalElements.toLocaleString('ko-KR') ?? 0} )

+
+
+ {query.isLoading ? +
+
+ +
+
+ : +
{tableComponent}
+ } +
+
+
+ ) +}; + +export { LinkLogPage }; \ No newline at end of file diff --git a/apps/cs/src/pages/linkParams/index.ts b/apps/cs/src/pages/linkParams/index.ts new file mode 100644 index 000000000..bd233390f --- /dev/null +++ b/apps/cs/src/pages/linkParams/index.ts @@ -0,0 +1 @@ +export { LinkParamsPage } from './ui/link-params-page'; \ No newline at end of file diff --git a/apps/cs/src/pages/linkParams/model/table-columns.tsx b/apps/cs/src/pages/linkParams/model/table-columns.tsx new file mode 100644 index 000000000..a71a74e9d --- /dev/null +++ b/apps/cs/src/pages/linkParams/model/table-columns.tsx @@ -0,0 +1,26 @@ +import { createColumnHelper } from "@tanstack/react-table"; +import { type Link } from "@/entities/link"; + +const columnHelper = createColumnHelper(); + +const columns = [ + columnHelper.accessor((_, index) => index + 1, { + id: 'no', + header: 'NO', + }), + columnHelper.accessor('link_sn', { + id: 'link_sn', + header: '연계 일련번호', + }), + columnHelper.accessor('link_nm', { + id: 'link_nm', + header: '연계 명', + }), + columnHelper.accessor('srvc_usg', { + id: 'srvc_usg', + header: '서비스용도', + cell: (info) => (info.getValue() ? info.getValue() : '-'), + }), +]; + +export { columns }; \ No newline at end of file diff --git a/apps/cs/src/pages/linkParams/model/vrbl-table-columns.tsx b/apps/cs/src/pages/linkParams/model/vrbl-table-columns.tsx new file mode 100644 index 000000000..26ccc7303 --- /dev/null +++ b/apps/cs/src/pages/linkParams/model/vrbl-table-columns.tsx @@ -0,0 +1,98 @@ +import { createColumnHelper } from "@tanstack/react-table"; +import { type LinkVrbl } from "@/entities/link"; +import { Checkbox } from "@common/components"; + +const columnHelper = createColumnHelper(); + +function vrblColumns({ + selectedVrblIds, + setSelectedVrblIds, +}: { + selectedVrblIds: string[]; + setSelectedVrblIds: React.Dispatch>; +}) { + return [ + columnHelper.display({ + id: "select", + header: ({ table }) => { + const allRows = table.getRowModel().rows; + const allSelected = + allRows.length > 0 && + allRows.every((row) => + selectedVrblIds.includes(row.original.link_vrbl_sn) + ); + + return ( +
+ { + if (checked) { + setSelectedVrblIds(allRows.map((row) => row.original.link_vrbl_sn)); + } else { + setSelectedVrblIds([]); + } + }} + /> +
+ ); + }, + cell: ({ row }) => { + const id = row.original.link_vrbl_sn; + const checked = selectedVrblIds.includes(id); + + return ( +
+ e.stopPropagation()} + onCheckedChange={(checked) => { + setSelectedVrblIds((prev) => + checked ? [...prev, id] : prev.filter((x) => x !== id) + ); + }} + /> +
+ ); + }, + }), + + columnHelper.accessor("link_vrbl_sn", { + id: "link_vrbl_sn", + header: "연계 변수 일련번호", + }), + columnHelper.accessor("link_vrbl_nm", { + id: "link_vrbl_nm", + header: "변수 명", + }), + columnHelper.accessor("link_vrbl_cd", { + id: "link_vrbl_cd", + header: "변수코드", + cell: (info) => (info.getValue() ? info.getValue() : '-'), + }), + columnHelper.accessor("link_vrbl_vl", { + id: "link_vrbl_vl", + header: "변수 값", + cell: (info) => (info.getValue() === 'now' ? '요청일자' : info.getValue() === 'nowMonth' ? '요청월' : info.getValue()), + }), + columnHelper.accessor("link_vrbl_expln", { + id: "link_vrbl_expln", + header: "설명", + cell: (info) => ( +
+ {info.getValue() || '-'} +
+ ), + }), + ]; +} + +export { vrblColumns }; \ No newline at end of file diff --git a/apps/cs/src/pages/linkParams/ui/link-params-dialog.tsx b/apps/cs/src/pages/linkParams/ui/link-params-dialog.tsx new file mode 100644 index 000000000..cff72a030 --- /dev/null +++ b/apps/cs/src/pages/linkParams/ui/link-params-dialog.tsx @@ -0,0 +1,351 @@ +'use client'; + +import { + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogClose, + Button, + Input, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Checkbox, + RadioGroup, + RadioGroupItem, + Label +} from "@common/components"; +import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form"; +import { type LinkVrblFormData } from "@/entities/link"; +import dayjs from "dayjs"; +import type { UseFormReturn } from "react-hook-form"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { useState, useEffect } from "react"; +import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; + +type LinkParamsDialogProps = { + selectedLink: { + linkSn: string; + linkNm: string; + }; + form: UseFormReturn; + insertLinkVrbl: (data: LinkVrblFormData) => void; + dialogOpen: boolean; + onOpenChange: (val: boolean) => void; + updateLinkVrbl: (data: LinkVrblFormData) => void; +} + +function LinkParamsDialog({ selectedLink, form, insertLinkVrbl, dialogOpen, onOpenChange, updateLinkVrbl }: LinkParamsDialogProps) { + + const isModifyMode = !!form.getValues('link_vrbl_sn'); + + const { confirm } = useConfirm(); + const [onConfirm, setOnConfirm] = useState(false); + const [isToday, setIsToday] = useState(false); + const [checkNowMonth, setCheckNowMonth] = useState('now'); + + const handleConfirm = async (callback: () => Promise) => { + setOnConfirm(true); + await callback(); + setOnConfirm(false); + }; + + function replaceEmptyWithNull>(data: T): T { + const result = {} as T; + for (const [key, value] of Object.entries(data)) { + (result as Record)[key] = value === '' ? null : value; + } + return result as T; + }; + + const handleSubmit = async (data: LinkVrblFormData) => { + const confirmMsg = isModifyMode ? '수정 하시겠습니까?' : '저장 하시겠습니까?'; + + await handleConfirm(async () => { + const cleanData = replaceEmptyWithNull(data); + + if (await confirm(confirmMsg)) { + if (isModifyMode) { + updateLinkVrbl(isToday ? { ...cleanData, link_vrbl_vl: checkNowMonth } : cleanData); + } else { + insertLinkVrbl(isToday ? { ...cleanData, link_vrbl_vl: checkNowMonth } : cleanData); + } + } + }); + }; + + const handleCheckTodayValue = (check: boolean | string) => { + if (check) { + setIsToday(true); + form.setValue('link_vrbl_vl', (checkNowMonth === 'now' ? '요청일자' : '요청월')); + } else { + setIsToday(false); + form.setValue('link_vrbl_vl', ''); + } + }; + + useEffect(() => { + if (!dialogOpen) { + setIsToday(false); + setCheckNowMonth('now'); + form.reset({ + link_vrbl_nm: '', + link_vrbl_expln: '', + link_vrbl_cd: '', + link_vrbl_vl: '', + }); + } + + if (isModifyMode && (form.getValues('link_vrbl_vl') === 'now' || form.getValues('link_vrbl_vl') === 'nowMonth')) { + setIsToday(true); + setCheckNowMonth(form.getValues('link_vrbl_vl')); + } + + }, [dialogOpen]); + + return ( + { + onOpenChange(open); + }} + > + + + + + + + {selectedLink.linkNm} ({selectedLink.linkSn}) + + +
+ +
+ + + + + + + {isModifyMode && + + 변수 일련번호 + + ( + + + + + + )} + /> + + + } + + * 변수 명 + + ( + + + + + {form.formState.errors.link_vrbl_nm && ( +

+ {form.formState.errors.link_vrbl_nm.message} +

+ )} +
+ )} + /> +
+
+ + 변수 설명 + + ( + + + + + {form.formState.errors.link_vrbl_expln && ( +

+ {form.formState.errors.link_vrbl_expln.message} +

+ )} +
+ )} + /> +
+
+ + 변수 코드 + + ( + + + + + {form.formState.errors.link_vrbl_cd && ( +

+ {form.formState.errors.link_vrbl_cd.message} +

+ )} +
+ )} + /> +
+
+ + * 변수 값 + + ( + +
+ handleCheckTodayValue(check)} /> + +
+ {isToday && +
+ { + setCheckNowMonth(value); + form.setValue('link_vrbl_vl', (value === 'now' ? '요청일자' : '요청월')); + }} + > +
+ + +
+
+ + +
+
+
+ } + + + + {form.formState.errors.link_vrbl_vl && ( +

+ {form.formState.errors.link_vrbl_vl.message} +

+ )} +
+ )} + /> +
+
+ {isModifyMode && + <> + + 수정자 + + ( + + + + + + )} + /> + + + + 수정일시 + + ( + + + + + + )} + /> + + + + } +
+
+
+ + + + + + +
+ + +
+
+ ) +}; + +export { LinkParamsDialog }; \ No newline at end of file diff --git a/apps/cs/src/pages/linkParams/ui/link-params-page.tsx b/apps/cs/src/pages/linkParams/ui/link-params-page.tsx new file mode 100644 index 000000000..b3806e712 --- /dev/null +++ b/apps/cs/src/pages/linkParams/ui/link-params-page.tsx @@ -0,0 +1,405 @@ +'use client'; + +import { useState, useMemo } from "react"; +import { + Button, + DataTable, + Input, + Label, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + LoadingWithAnimation, +} from "@common/components"; +import { getCoreRowModel, useReactTable, type PaginationState, type RowSelectionState, } from "@tanstack/react-table"; +import { LinkApi, type Link, type LinkSearchParam, type LinkVrblFormData, linkVrblFormSchema } from "@/entities/link"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { columns } from "../model/table-columns"; +import { vrblColumns } from "../model/vrbl-table-columns"; +import { type LinkVrbl } from "@/entities/link"; +import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { LinkParamsDialog } from "./link-params-dialog"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { getUserInfo } from "../../../../../../packages/common/components/src/api"; +import { AUTH_USER_ID } from "@/shared/config"; +import { getCookie } from "@common/components"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; +import { useDataTable } from "@/shared/lib"; + +function LinkParamsPage() { + const queryClient = useQueryClient(); + + const [linkSn, setLinkSn] = useState(''); + const [linkNm, setLinkNm] = useState(''); + + const [linkSearchParam, setLinkSearchParam] = useState({ + linkSn: '', + linkNm: '' + }); + + const [selectedLink, setSelectedLink] = useState<{ + linkSn: string; + linkNm: string; + }>({ + linkSn: '', + linkNm: '' + }); + + const [rowSelection, setRowSelection] = useState({}); + + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 100, + }); + + const { confirm } = useConfirm(); + + const [selectedVrblIds, setSelectedVrblIds] = useState([]); + const [isDBdata, setIsDBData] = useState(false); + const [dialogOpen, setDialogOpen] = useState(false); + const [onConfirm, setOnConfirm] = useState(false); + + const userUniqId = getCookie(AUTH_USER_ID) ?? ""; + const { setMessage: alert } = useAlertStore((state) => state); + + const dbIdList = ['link0001', 'link0002']; + + // Link 목록 쿼리 + const query = useQuery({ + queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, linkSearchParam], + queryFn: () => LinkApi.getLinkList({ + ...linkSearchParam, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + }); + + // LinkVrbl 목록 쿼리 - 별도 관리 + const linkVrblQuery = useQuery({ + queryKey: ['linkVrblList', selectedLink.linkSn], + queryFn: () => LinkApi.getLinkVrblList(selectedLink.linkSn), + enabled: !!selectedLink.linkSn && !dbIdList.includes(selectedLink.linkSn), + staleTime: 0, // 항상 최신 데이터를 가져오도록 설정 + }); + + const handleResetSearchParam = () => { + setSelectedLink({ + linkSn: '', + linkNm: '' + }); + + setLinkSn(''); + setLinkNm(''); + setLinkSearchParam({ + ...linkSearchParam, + linkSn: '', + linkNm: '' + }); + }; + + const handleConfirm = async (callback: () => Promise) => { + setOnConfirm(true); + await callback(); + setOnConfirm(false); + }; + + const { mutateAsync: insertLinkVrbl } = useMutation({ + mutationFn: async (data: LinkVrblFormData) => + await LinkApi.insertLinkVrbl({ + ...data, + link_sn: selectedLink.linkSn, + mdfr: userUniqId, + }), + onSuccess: async () => { + // 쿼리 무효화를 통한 최신 데이터 가져오기 + await queryClient.invalidateQueries({ + queryKey: ['linkVrblList', selectedLink.linkSn] + }); + setDialogOpen(false); + alert('저장되었습니다.'); + } + }); + + const { mutateAsync: updateLinkVrbl } = useMutation({ + mutationFn: async (data: LinkVrblFormData) => await LinkApi.updateLinkVrblInfo({ + ...data, + link_sn: selectedLink.linkSn, + mdfr: userUniqId, + }), + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: ['linkVrblList', selectedLink.linkSn] + }); + setDialogOpen(false); + alert('저장되었습니다.'); + } + }); + + const { mutateAsync: deleteLinkVrbl } = useMutation({ + mutationFn: async (data: string[]) => await LinkApi.deleteLinkVrbl(data), + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: ['linkVrblList', selectedLink.linkSn] + }); + setSelectedVrblIds([]); + alert('삭제되었습니다.'); + } + }); + + // linkVrblQuery의 데이터를 사용 + const tableData = useMemo(() => { + return linkVrblQuery.data?.content ?? []; + }, [linkVrblQuery.data]); + + const vrblCols = useMemo( + () => + vrblColumns({ + selectedVrblIds, + setSelectedVrblIds, + }), + [selectedVrblIds] + ); + + const vrblTable = useReactTable({ + data: tableData, + columns: vrblCols, + getCoreRowModel: getCoreRowModel(), + }); + + const form = useForm({ + resolver: zodResolver(linkVrblFormSchema), + defaultValues: {}, + }); + + const handleRowClick = async (link: Link) => { + setSelectedLink({ + linkSn: link.link_sn, + linkNm: link.link_nm, + }); + + setSelectedVrblIds([]); + + if (dbIdList.includes(link.link_clsf_cd)) { + setIsDBData(true); + } else { + setIsDBData(false); + } + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + setSelectedLink({ + linkSn: '', + linkNm: '' + }); + + setLinkSearchParam({ + ...linkSearchParam, + linkSn, + linkNm + }); + }; + + const fetchUserInfo = async () => { + const userData = await getUserInfo(); + return userData.userName; + }; + + const handleDeleteParam = async () => { + if (selectedVrblIds.length === 0) { + alert('삭제할 파라미터를 선택해 주세요.'); + } else { + await handleConfirm(async () => { + if (await confirm('선택한 파라미터 정보를 삭제하시겠습니까?')) { + deleteLinkVrbl(selectedVrblIds); + } + }); + } + }; + + const handleVrblRowClick = async (data: LinkVrbl) => { + const linkVrblInfo = await LinkApi.getLinkVrblInfo(data.link_vrbl_sn); + + if (linkVrblInfo) { + let newData = { ...linkVrblInfo }; + + if (linkVrblInfo.mdfr) { + const userName = await fetchUserInfo(); + newData = { + ...linkVrblInfo, + mdfr: userName, + }; + } + + form.reset(newData); + } + setDialogOpen(true); + }; + + const { tableComponent } = useDataTable({ + query, + tableOptions: { + columns, + state: { + pagination, + rowSelection, + }, + manualPagination: true, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + }, + dataTableOptions: { + onRowClick: handleRowClick, + viewPagination: true, + }, + }); + + // LinkVrbl 데이터 표시 로직 + const renderLinkVrblContent = () => { + if (isDBdata) { + return ( +
+ 변수를 지원하지 않는 연계 데이터입니다. +
+ ); + } + + if (!selectedLink.linkSn) { + return ( +
+ 연계 데이터를 선택해주세요. +
+ ); + } + + if (linkVrblQuery.isLoading) { + return ( +
+
+ +
+
+ ); + } + + return ( + <> +
+ {tableData.length > 0 && ( + + )} + +
+ {tableData.length > 0 ? ( +
+ +
+ ) : ( +
+ 등록된 연계 변수가 없습니다. +
+ )} + + ); + }; + + return ( +
+
+
+ + setLinkSn(e.target.value)} + autoComplete="off" + /> + + setLinkNm(e.target.value)} + autoComplete="off" + /> + + +
+
+
+
+
+

연계 목록

+
+
+ {!query.isLoading ? ( +
{tableComponent}
+ ) : ( +
+
+ +
+
+ )} +
+
+
+
+

연계 변수 정보

+
+
+ + + + + + + + 연계 일련번호 + + {selectedLink.linkSn ? selectedLink.linkSn : '-'} + + + + 연계 명 + + {selectedLink.linkNm ? selectedLink.linkNm : '-'} + + + +
+ {renderLinkVrblContent()} +
+
+
+ +
+ ); +} + +export { LinkParamsPage }; \ No newline at end of file diff --git a/apps/cs/src/pages/linkStats/index.ts b/apps/cs/src/pages/linkStats/index.ts new file mode 100644 index 000000000..4e3241a79 --- /dev/null +++ b/apps/cs/src/pages/linkStats/index.ts @@ -0,0 +1 @@ +export { LinkStatsPage } from './ui/link-stats-page'; \ No newline at end of file diff --git a/apps/cs/src/pages/linkStats/ui/link-stats-page.tsx b/apps/cs/src/pages/linkStats/ui/link-stats-page.tsx new file mode 100644 index 000000000..f6168ba76 --- /dev/null +++ b/apps/cs/src/pages/linkStats/ui/link-stats-page.tsx @@ -0,0 +1,378 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import dayjs from "dayjs"; +import { LinkApi, type LinkStats, type LinkSearchParam } from "@/entities/link"; +import { Button, Label, DatePicker, LoadingWithAnimation } from "@common/components"; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Tooltip, + Legend, + type ChartOptions, + type Chart, +} from "chart.js"; +import { Line } from "react-chartjs-2"; +import { getCoreRowModel, type PaginationState, type RowSelectionState, } from "@tanstack/react-table"; +import { columns } from "@/pages/linkLog/model/table-columns"; +import { useDataTable } from "@/shared/lib"; +import { useQuery } from "@tanstack/react-query"; + +interface LinkStatsListObject { + linkSuccStats: LinkStats[] | null; + linkFailStats: LinkStats[] | null; +} + +function LinkStatsPage() { + + const succChartRef = useRef | null>(null); + const failChartRef = useRef | null>(null); + + const [startDate, setStartDate] = useState(dayjs().subtract(7, "day").format('YYYY-MM-DD')); + const [endDate, setEndDate] = useState(dayjs().format('YYYY-MM-DD')); + + const [rowSelection, setRowSelection] = useState({}); + + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 100, + }); + + const [linkStatsList, setLinkStatsList] = useState(null); + const [logQueryParam, setLogQueryParam] = useState<{ + isSucc: 'SUCC' | 'FAIL'; + linkSn?: string; + linkNm?: string; + } | null>(null); + + const query = useQuery({ + queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, logQueryParam, pagination], + queryFn: () => + LinkApi.getLinkLogList({ + ...logQueryParam, + page: pagination.pageIndex, + size: pagination.pageSize, + startDate, + endDate, + }), + enabled: !!logQueryParam, + }); + + + const { tableComponent } = useDataTable({ + query, + tableOptions: { + columns, + state: { + pagination, + rowSelection, + }, + manualPagination: true, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + }, + dataTableOptions: { + viewPagination: true, + }, + }); + + const [linkSearchParam, setLinkSearchParam] = useState({ + linkSn: '', + linkNm: '', + startDate: '', + endDate: '', + }); + + ChartJS.register( + LineElement, + CategoryScale, + LinearScale, + PointElement, + Tooltip, + Legend + ); + + const succData = { + labels: linkStatsList?.linkSuccStats?.map((e) => e.link_nm), + datasets: [ + { + label: '연계 성공', + data: linkStatsList?.linkSuccStats?.map((e) => e.count), + fill: false, + borderColor: 'rgb(53, 162, 235)', + backgroundColor: 'rgb(53, 162, 235)', + tension: 0.1, + pointRadius: 6, + pointHoverRadius: 8, + }, + ], + }; + + const succOptions: ChartOptions<'line'> = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'top', + }, + }, + scales: { + y: { + min: 0, + }, + }, + }; + + const failData = { + labels: linkStatsList?.linkFailStats?.map((e) => e.link_nm), + datasets: [ + { + label: '연계 실패', + data: linkStatsList?.linkFailStats?.map((e) => e.count), + fill: false, + borderColor: 'rgb(255, 99, 132)', + backgroundColor: 'rgb(255, 99, 132)', + tension: 0.1, + pointRadius: 6, + pointHoverRadius: 8, + }, + ], + }; + + const failOptions: ChartOptions<'line'> = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'top', + }, + }, + scales: { + y: { + min: 0, + }, + }, + }; + + const handleResetSearchParam = async () => { + setLinkSearchParam({ + linkSn: '', + linkNm: '', + startDate: dayjs().subtract(7, "day").format('YYYY-MM-DD'), + endDate: dayjs().format('YYYY-MM-DD'), + }); + + setLogQueryParam({ + isSucc: 'SUCC', + linkNm: '', + linkSn: '', + }); + + setStartDate(dayjs().subtract(7, "day").format('YYYY-MM-DD')); + setEndDate(dayjs().format('YYYY-MM-DD')); + + const data = await LinkApi.getLinkStatsList({ + ...linkSearchParam, + linkSn: '', + linkNm: '', + startDate: dayjs().subtract(7, "day").format('YYYY-MM-DD'), + endDate: dayjs().format('YYYY-MM-DD'), + }); + setLinkStatsList(data); + }; + + const handleSubmit = async (e?: React.FormEvent) => { + e?.preventDefault(); + + if (startDate && endDate) { + + if (startDate > endDate) { + alert('종료일이 시작일보다 이전일 수 없습니다.'); + return; + } else { + const data = await LinkApi.getLinkStatsList({ + ...linkSearchParam, + startDate, + endDate + }); + setLinkStatsList(data); + setLogQueryParam({ + isSucc: 'SUCC', + linkNm: '', + linkSn: '', + }); + setLinkSearchParam({ + ...linkSearchParam, + startDate, + endDate + }); + } + } else { + alert('조회 날짜를 선택해주세요.'); + return; + } + }; + + const handleSuccChartClick = async (event: React.MouseEvent) => { + if (!succChartRef.current) return; + + const chart = succChartRef.current; + + const points = chart.getElementsAtEventForMode( + event.nativeEvent, + "nearest", + { intersect: true }, + false + ); + + if (points[0] && linkStatsList?.linkSuccStats) { + const index = points[0].index; + setPagination({ + ...pagination, + pageIndex: 0, + pageSize: 100, + }); + setLogQueryParam({ + isSucc: 'SUCC', + linkNm: linkStatsList.linkSuccStats[index]?.link_nm, + linkSn: linkStatsList.linkSuccStats[index]?.link_sn, + }); + } + }; + + const handleFailChartClick = async (event: React.MouseEvent) => { + if (!failChartRef.current) return; + + const chart = failChartRef.current; + + const points = chart.getElementsAtEventForMode( + event.nativeEvent, + "nearest", + { intersect: true }, + false + ); + + if (points[0] && linkStatsList?.linkFailStats) { + const index = points[0].index; + setPagination({ + ...pagination, + pageIndex: 0, + pageSize: 100, + }); + setLogQueryParam({ + isSucc: 'FAIL', + linkNm: linkStatsList.linkFailStats[index]?.link_nm, + linkSn: linkStatsList.linkFailStats[index]?.link_sn, + }); + } + }; + + const handleSubmitToday = async () => { + const data = await LinkApi.getLinkStatsList({ + ...linkSearchParam, + startDate: dayjs().format('YYYY-MM-DD'), + endDate: dayjs().format('YYYY-MM-DD'), + }); + setEndDate(dayjs().format('YYYY-MM-DD')); + setStartDate(dayjs().format('YYYY-MM-DD')); + setLinkStatsList(data); + setLinkSearchParam({ + ...linkSearchParam, + startDate: dayjs().format('YYYY-MM-DD'), + endDate: dayjs().format('YYYY-MM-DD'), + }); + setLogQueryParam({ + isSucc: 'SUCC', + linkNm: '', + linkSn: '', + }); + }; + + useEffect(() => { + const fetchData = async () => { + const data = await LinkApi.getLinkStatsList({ + ...linkSearchParam, + startDate, + endDate + }); + setLinkStatsList(data); + }; + + setLinkSearchParam({ + ...linkSearchParam, + startDate, + endDate + }); + + fetchData(); + }, []); + + return ( +
+
+
+ + setStartDate(dayjs(e).format('YYYY-MM-DD'))} /> + + setEndDate(dayjs(e).format('YYYY-MM-DD'))} /> + + + + +
+
+
+
+

조회 일자 : {`${linkSearchParam.startDate} ~ ${linkSearchParam.endDate}`}

+
+
+ +
+
+ +
+
+
+
+

+ {logQueryParam?.linkNm ? `${logQueryParam.linkNm} 연계 ${logQueryParam.isSucc === 'FAIL' ? '실패' : '성공'} 로그 목록 (${linkSearchParam.startDate} ~ ${linkSearchParam.endDate}) - ${query.data?.totalElements.toLocaleString('ko-KR') ?? 0}건` : '로그 목록'} +

+
+
+ {logQueryParam?.linkNm ? + query.isLoading ? +
+
+ +
+
+ : +
{tableComponent}
+ : +
+ 차트 데이터를 선택해주세요. +
+ } +
+
+
+
+ ) +}; + +export { LinkStatsPage }; \ No newline at end of file diff --git a/apps/cs/src/pages/linkTable/index.ts b/apps/cs/src/pages/linkTable/index.ts new file mode 100644 index 000000000..4677f1b52 --- /dev/null +++ b/apps/cs/src/pages/linkTable/index.ts @@ -0,0 +1 @@ +export { LinkTablePage} from "./ui/link-table-page"; \ No newline at end of file diff --git a/apps/cs/src/pages/linkTable/model/table-columns.tsx b/apps/cs/src/pages/linkTable/model/table-columns.tsx new file mode 100644 index 000000000..8c15ecd4b --- /dev/null +++ b/apps/cs/src/pages/linkTable/model/table-columns.tsx @@ -0,0 +1,103 @@ +import { createColumnHelper } from "@tanstack/react-table"; +import { type LinkTableColumns } from "@/entities/link"; +import { Checkbox } from "@common/components"; + +const columnHelper = createColumnHelper(); + +function tableColumns({ selectedColIds, setSelectedColIds }: { + selectedColIds: string[]; + setSelectedColIds: React.Dispatch>; +}) { + return [ + columnHelper.display({ + id: "select", + header: ({ table }) => { + const allRows = table.getRowModel().rows; + const allSelected = + allRows.length > 0 && + allRows.every((row) => + selectedColIds.includes(row.original.col_sn) + ); + + return ( +
+ { + if (checked) { + setSelectedColIds(allRows.map((row) => row.original.col_sn)); + } else { + setSelectedColIds([]); + } + }} + /> +
+ ); + }, + cell: ({ row }) => { + const id = row.original.col_sn; + const checked = selectedColIds.includes(id); + + return ( +
+ e.stopPropagation()} + onCheckedChange={(checked) => { + setSelectedColIds((prev) => + checked ? [...prev, id] : prev.filter((x) => x !== id) + ); + }} + /> +
+ ); + }, + }), + columnHelper.accessor('col_sn', { + id: 'col_sn', + header: '컬럼 일련번호' + }), + columnHelper.accessor('sou_col_nm', { + id: 'sou_col_nm', + header: '원천컬럼명', + cell: (info) => info.getValue() ? info.getValue() : '-', + }), + columnHelper.accessor('sou_col_korn_nm', { + id: 'sou_col_korn_nm', + header: '원천컬럼 한글명', + cell: (info) => info.getValue() ? info.getValue() : '-', + }), + columnHelper.accessor('sou_col_sz', { + id: 'sou_col_sz', + header: '원천컬럼 크기', + cell: (info) => info.getValue() ? info.getValue() : '-', + }), + columnHelper.accessor('trsf_col_nm', { + id: 'trsf_col_nm', + header: '변환컬럼명', + cell: (info) => info.getValue() ? info.getValue() : '-', + }), + columnHelper.accessor('trsf_col_korn_nm', { + id: 'trsf_col_korn_nm', + header: '변환컬럼 한글명', + cell: (info) => info.getValue() ? info.getValue() : '-', + }), + columnHelper.accessor('trsf_col_sz', { + id: 'trsf_col_sz', + header: '변환컬럼 크기', + cell: (info) => info.getValue() ? info.getValue() : '-', + }), + columnHelper.accessor('pk_yn', { + id: 'pk_yn', + header: 'PK 여부', + cell: (info) => info.getValue() === 'Y' ? '예' : '아니오', + }), + ] +} + +export { tableColumns }; \ No newline at end of file diff --git a/apps/cs/src/pages/linkTable/ui/link-table-dialog.tsx b/apps/cs/src/pages/linkTable/ui/link-table-dialog.tsx new file mode 100644 index 000000000..a566a7d95 --- /dev/null +++ b/apps/cs/src/pages/linkTable/ui/link-table-dialog.tsx @@ -0,0 +1,503 @@ +'use client'; + +import { + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogClose, + Button, + Input, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Select, + SelectTrigger, + SelectContent, + SelectItem, + SelectValue, + Checkbox +} from "@common/components"; +import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form"; +import { type LinkTableColData, type LinkTable } from "@/entities/link"; +import dayjs from "dayjs"; +import type { UseFormReturn } from "react-hook-form"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { useState, useEffect } from "react"; +import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; + +interface LinkTableDialogProps { + dialogOpen: boolean; + onOpenChange: (val: boolean) => void; + form: UseFormReturn; + tableInfo: LinkTable; + insertLinkTableCol: (data: LinkTableColData) => void; + updateLinkTableCol: (data: LinkTableColData) => void; +} + +function LinkTableDialog({ dialogOpen, onOpenChange, form, tableInfo, insertLinkTableCol, updateLinkTableCol }: LinkTableDialogProps) { + + const isModifyMode = !!form.getValues('col_sn'); + + const { confirm } = useConfirm(); + const [onConfirm, setOnConfirm] = useState(false); + const [useColSeq, setUseColSeq] = useState(false); + + const handleConfirm = async (callback: () => Promise) => { + setOnConfirm(true); + await callback(); + setOnConfirm(false); + }; + + const handleSubmit = async (data: LinkTableColData) => { + const confirmMsg = isModifyMode ? '수정 하시겠습니까?' : '저장 하시겠습니까?'; + + await handleConfirm(async () => { + if (await confirm(confirmMsg)) { + if (!isModifyMode) { + insertLinkTableCol({ + ...data, + link_sn: tableInfo.link_sn, + tbl_sn: tableInfo.tbl_sn, + }); + } else { + updateLinkTableCol({ + ...data, + link_sn: tableInfo.link_sn, + tbl_sn: tableInfo.tbl_sn, + }); + } + } + }); + }; + + const handleCheckColSeqValue = (check: boolean | string) => { + if (check) { + setUseColSeq(false); + form.setValue('col_seq', null); + } else { + setUseColSeq(true); + } + }; + + useEffect(() => { + if (!dialogOpen) { + setUseColSeq(false); + form.reset({ + tbl_sn: '', + col_sn: '', + sou_col_nm: '', + sou_col_korn_nm: '', + sou_col_data_frm: '', + sou_col_sz: null, + trsf_col_nm: '', + trsf_col_korn_nm: '', + trsf_col_data_frm: '', + trsf_col_sz: null, + col_seq: null, + pk_yn: 'N', + mdfcn_dt: '', + mdfr: '', + }); + } + + if (isModifyMode && form.getValues('col_seq')) { + setUseColSeq(true); + } + + }, [dialogOpen]); + + return ( + { + onOpenChange(open); + }} + > + + + + + + + {tableInfo.trsf_tbl_korn_nm} ({tableInfo.tbl_sn}) + + +
+ +
+ + + + + + + {isModifyMode && + + 컬럼 일련번호 + + ( + + + + + + )} + /> + + + } + + * 원천컬럼명 + + ( + + + + + {form.formState.errors.sou_col_nm && ( +

+ {form.formState.errors.sou_col_nm.message} +

+ )} +
+ )} + /> +
+
+ + * 원천컬럼 한글명 + + ( + + + + + {form.formState.errors.sou_col_korn_nm && ( +

+ {form.formState.errors.sou_col_korn_nm.message} +

+ )} +
+ )} + /> +
+
+ + * 원천컬럼 데이터 형식 + + ( + + + + + {form.formState.errors.sou_col_data_frm && ( +

+ {form.formState.errors.sou_col_data_frm.message} +

+ )} +
+ )} + /> +
+
+ + 원천컬럼 크기 + + ( + + + { + const onlyDigits = e.target.value.replace(/\D/g, "").slice(0, 14); + field.onChange(onlyDigits); // react-hook-form에 반영 + }} + /> + +

+ ※ 14자리까지 입력 가능합니다. +

+
+ )} + /> +
+
+ + PK 여부 + + ( + + + + + + )} + /> + + + + * 변환컬럼명 + + ( + + + + + {form.formState.errors.trsf_col_nm && ( +

+ {form.formState.errors.trsf_col_nm.message} +

+ )} +
+ )} + /> +
+
+ + * 변환컬럼 한글명 + + ( + + + + + {form.formState.errors.trsf_col_korn_nm && ( +

+ {form.formState.errors.trsf_col_korn_nm.message} +

+ )} +
+ )} + /> +
+
+ + * 변환컬럼 데이터 형식 + + ( + + + + + {form.formState.errors.trsf_col_data_frm && ( +

+ {form.formState.errors.trsf_col_data_frm.message} +

+ )} +
+ )} + /> +
+
+ + 변환컬럼 크기 + + ( + + + { + const onlyDigits = e.target.value.replace(/\D/g, "").slice(0, 14); + field.onChange(onlyDigits); // react-hook-form에 반영 + }} + /> + +

+ ※ 14자리까지 입력 가능합니다. +

+
+ )} + /> +
+
+ + 컬럼 매칭 순서 + + ( + +
+ handleCheckColSeqValue(check)} /> + +
+ + { + const onlyDigits = e.target.value.replace(/\D/g, "").slice(0, 3); + field.onChange(onlyDigits); // react-hook-form에 반영 + }} + /> + +

+ ※ 3자리까지 입력 가능합니다. +

+
+ )} + /> +
+
+ {isModifyMode && + <> + + 수정자 + + ( + + + + + + )} + /> + + + + 수정일시 + + ( + + + + + + )} + /> + + + + } +
+
+
+ + + + + + +
+ + +
+
+ ) +} + +export { LinkTableDialog }; \ No newline at end of file diff --git a/apps/cs/src/pages/linkTable/ui/link-table-page.tsx b/apps/cs/src/pages/linkTable/ui/link-table-page.tsx new file mode 100644 index 000000000..11f478b71 --- /dev/null +++ b/apps/cs/src/pages/linkTable/ui/link-table-page.tsx @@ -0,0 +1,710 @@ +'use client'; + +import { useState, useMemo } from "react"; +import { + Button, + DataTable, + Input, + Label, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + LoadingWithAnimation, +} from "@common/components"; +import { getCoreRowModel, type PaginationState, type RowSelectionState, useReactTable } from "@tanstack/react-table"; +import { LinkApi, type Link, type LinkSearchParam, type LinkTable, type LinkTableColumns, type LinkTableColData, type LinkTableFormData, LinkTableColumnsSchema, LinkTableSchema } from "@/entities/link"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { columns } from "../../linkParams/model/table-columns"; +import { tableColumns } from "../model/table-columns"; +import { LinkTableDialog } from "./link-table-dialog"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { getUserInfo } from "../../../../../../packages/common/components/src/api"; +import { AUTH_USER_ID } from "@/shared/config"; +import { getCookie } from "@common/components"; +import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; +import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form"; +import { useDataTable } from "@/shared/lib"; + +function LinkTablePage() { + const queryClient = useQueryClient(); + + const userUniqId = getCookie(AUTH_USER_ID) ?? ""; + + const [linkSn, setLinkSn] = useState(''); + const [linkNm, setLinkNm] = useState(''); + + const [linkSearchParam, setLinkSearchParam] = useState({ + linkSn: '', + linkNm: '' + }); + + const [selectedTableInfo, setSelectedTableInfo] = useState(null); + const [tableColList, setTableColList] = useState([]); + const [dialogOpen, setDialogOpen] = useState(false); + const [selectedColIds, setSelectedColIds] = useState([]); + + const [rowSelection, setRowSelection] = useState({}); + + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 100, + }); + + const { setMessage: alert } = useAlertStore((state) => state); + + const { confirm } = useConfirm(); + const [onConfirm, setOnConfirm] = useState(false); + + const handleConfirm = async (callback: () => Promise) => { + setOnConfirm(true); + await callback(); + setOnConfirm(false); + }; + + const query = useQuery({ + queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, linkSearchParam], + queryFn: () => LinkApi.getLinkList({ + ...linkSearchParam, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + }); + + const { mutateAsync: insertLinkTableCol } = useMutation({ + mutationFn: async (data: LinkTableColData) => await LinkApi.insertLinkTableCol({ + ...data, + mdfr: userUniqId, + }), + onSuccess: async () => { + if (selectedTableInfo) { + await queryClient.invalidateQueries({ + queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`] + }); + await queryClient.invalidateQueries({ + queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`] + }); + + const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn); + const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn); + + setSelectedTableInfo(tableData); + setTableColList(tableColList.content); + setSelectedColIds([]); + setDialogOpen(false); + alert('저장되었습니다.'); + } + } + }); + + const { mutateAsync: updateLinkTableCol } = useMutation({ + mutationFn: async (data: LinkTableColData) => await LinkApi.updateLinkTableCol({ + ...data, + mdfr: userUniqId, + }), + onSuccess: async () => { + if (selectedTableInfo) { + await queryClient.invalidateQueries({ + queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`] + }); + await queryClient.invalidateQueries({ + queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`] + }); + + const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn); + const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn); + + setSelectedTableInfo(tableData); + setTableColList(tableColList.content); + setSelectedColIds([]); + setDialogOpen(false); + alert('수정되었습니다.'); + } + } + }); + + const { mutateAsync: deleteLinkTableCol } = useMutation({ + mutationFn: async (data: string[]) => await LinkApi.deleteLinkTableCol(data), + onSuccess: async () => { + if (selectedTableInfo) { + await queryClient.invalidateQueries({ + queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`] + }); + await queryClient.invalidateQueries({ + queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`] + }); + + const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn); + const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn); + + setSelectedTableInfo(tableData); + setTableColList(tableColList.content); + setSelectedColIds([]); + setDialogOpen(false); + alert('삭제되었습니다.'); + } + } + }); + + const { mutateAsync: insertLinkTableInfo } = useMutation({ + mutationFn: async (data: LinkTableFormData) => await LinkApi.insertLinkTableInfo(data), + onSuccess: async () => { + if (selectedTableInfo) { + await queryClient.invalidateQueries({ + queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`] + }); + await queryClient.invalidateQueries({ + queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`] + }); + + const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn); + const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn); + + tableForm.reset({ + link_sn: tableData.link_sn, + link_nm: tableData.link_nm, + tbl_sn: tableData.tbl_sn ?? "", + sou_tbl_nm: tableData.sou_tbl_nm ?? "", + sou_tbl_korn_nm: tableData.sou_tbl_korn_nm ?? "", + sou_tbl_expln: tableData.sou_tbl_expln ?? "", + trsf_tbl_expln: tableData.trsf_tbl_expln ?? "", + trsf_tbl_nm: tableData.trsf_tbl_nm ?? "", + trsf_tbl_korn_nm: tableData.trsf_tbl_korn_nm ?? "", + }); + setSelectedTableInfo(tableData); + setTableColList(tableColList.content); + setSelectedColIds([]); + alert('저장되었습니다.'); + } + } + }); + + const { mutateAsync: updateLinkTableInfo } = useMutation({ + mutationFn: async (data: LinkTableFormData) => await LinkApi.updateLinkTableInfo(data), + onSuccess: async () => { + if (selectedTableInfo) { + await queryClient.invalidateQueries({ + queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`] + }); + await queryClient.invalidateQueries({ + queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`] + }); + + const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn); + const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn); + + setSelectedTableInfo(tableData); + setTableColList(tableColList.content); + setSelectedColIds([]); + alert('수정되었습니다.'); + } + } + }); + + const tableCol = useMemo( + () => + tableColumns({ selectedColIds, setSelectedColIds }), + [selectedColIds] + ); + + const colTable = useReactTable({ + data: tableColList, + columns: tableCol, + getCoreRowModel: getCoreRowModel(), + }); + + const form = useForm({ + resolver: zodResolver(LinkTableColumnsSchema), + defaultValues: { + pk_yn: "N" + } + }); + + const tableForm = useForm({ + resolver: zodResolver(LinkTableSchema), + defaultValues: {}, + }); + + const handleResetSearchParam = async () => { + setSelectedTableInfo(null); + setTableColList([]); + setLinkSn(''); + setLinkNm(''); + setLinkSearchParam({ + ...linkSearchParam, + linkSn: '', + linkNm: '', + }); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setSelectedTableInfo(null); + setTableColList([]); + + setLinkSearchParam({ + ...linkSearchParam, + linkSn, + linkNm + }); + }; + + const handleRowClick = async (data: Link) => { + const tableData = await LinkApi.getLinkTableInfo(data.link_sn); + const tableColList = await LinkApi.getLinkTableColList(data.link_sn); + + setSelectedTableInfo(tableData); + setTableColList(tableColList.content); + setSelectedColIds([]); + + tableForm.reset({ + link_sn: tableData.link_sn, + link_nm: tableData.link_nm, + tbl_sn: tableData.tbl_sn ?? "", + sou_tbl_nm: tableData.sou_tbl_nm ?? "", + sou_tbl_korn_nm: tableData.sou_tbl_korn_nm ?? "", + sou_tbl_expln: tableData.sou_tbl_expln ?? "", + trsf_tbl_expln: tableData.trsf_tbl_expln ?? "", + trsf_tbl_nm: tableData.trsf_tbl_nm ?? "", + trsf_tbl_korn_nm: tableData.trsf_tbl_korn_nm ?? "", + }); + }; + + const handleColumnRowClick = async (data: LinkTableColumns) => { + const colInfo = await LinkApi.getLinkTableColInfo({ colSn: data.col_sn, tblSn: data.tbl_sn }); + + if (colInfo) { + let newData = { ...colInfo }; + + if (colInfo.mdfr) { + const userName = await fetchUserInfo(); + newData = { + ...colInfo, + mdfr: userName, + }; + } + + form.reset(newData); + + } + setDialogOpen(true) + }; + + const fetchUserInfo = async () => { + const userData = await getUserInfo(); + return userData.userName; + }; + + const handleDeleteParam = async () => { + if (selectedColIds.length === 0) { + alert('삭제할 컬럼을 선택해 주세요.'); + } else { + await handleConfirm(async () => { + if (await confirm('선택한 컬럼 정보를 삭제하시겠습니까?')) { + deleteLinkTableCol(selectedColIds); + } + }); + } + }; + + const handleTableSubmit = async (data: LinkTableFormData) => { + const regTblInfo = await LinkApi.countLinkTblNm(data.trsf_tbl_nm); + const confirmMsg = (regTblInfo.count > 0 && data.trsf_tbl_nm !== selectedTableInfo?.trsf_tbl_nm) ? `이미 사용중인 테이블 명입니다. 해당 테이블 명으로 ${!data.tbl_sn ? '저장' : '수정'}하시겠습니까?` : `${!data.tbl_sn ? '저장' : '수정'}하시겠습니까?`; + if (!data.tbl_sn) { + await handleConfirm(async () => { + if (await confirm(confirmMsg)) { + insertLinkTableInfo({ + ...data, + mdfr: userUniqId, + tbl_sn: (regTblInfo.count > 0) ? regTblInfo.tbl_sn : "", + }); + } + }); + } else { + await handleConfirm(async () => { + if (await confirm(confirmMsg)) { + updateLinkTableInfo({ + ...data, + tbl_sn: (regTblInfo.count > 0) ? regTblInfo.tbl_sn : "", + mdfr: userUniqId, + }); + } + }); + } + + }; + + const { tableComponent } = useDataTable({ + query, + tableOptions: { + columns, + state: { + pagination, + rowSelection, + }, + manualPagination: true, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + }, + dataTableOptions: { + onRowClick: handleRowClick, + viewPagination: true, + }, + }); + + return ( +
+
+
+ + setLinkSn(e.target.value)} + autoComplete="off" + /> + + setLinkNm(e.target.value)} + autoComplete="off" + /> + + +
+
+
+
+
+

연계 목록

+
+
+ {!query.isLoading ? +
{tableComponent}
+ : +
+
+ +
+
+ } +
+
+
+
+

연계 테이블 정보

+
+
+
+ + {selectedTableInfo && +
+ +
+ } + + + + + + + + 연계 일련번호 + + {selectedTableInfo ? + ( + + + + + + )} + /> + : + - + } + + + + 연계 명 + + {selectedTableInfo ? + ( + + + + + + )} + /> + : + - + } + + + + 원천 테이블 명 + + {selectedTableInfo ? + ( + + + + + {tableForm.formState.errors.sou_tbl_nm && ( +

+ {tableForm.formState.errors.sou_tbl_nm.message} +

+ )} +
+ )} + /> + : + - + } +
+
+ + 원천 테이블 한글명 + + {selectedTableInfo ? + ( + + + + + {tableForm.formState.errors.sou_tbl_korn_nm && ( +

+ {tableForm.formState.errors.sou_tbl_korn_nm.message} +

+ )} +
+ )} + /> + : + - + } +
+
+ + 원천 테이블 설명 + + {selectedTableInfo ? + ( + + + + + {tableForm.formState.errors.sou_tbl_expln && ( +

+ {tableForm.formState.errors.sou_tbl_expln.message} +

+ )} +
+ )} + /> + : + - + } +
+
+ + * 변환 테이블 명 + + {selectedTableInfo ? + ( + + + + + {tableForm.formState.errors.trsf_tbl_nm && ( +

+ {tableForm.formState.errors.trsf_tbl_nm.message} +

+ )} +
+ )} + /> + : + - + } +
+
+ + * 변환 테이블 한글명 + + {selectedTableInfo ? + ( + + + + + {tableForm.formState.errors.trsf_tbl_korn_nm && ( +

+ {tableForm.formState.errors.trsf_tbl_korn_nm.message} +

+ )} +
+ )} + /> + : + - + } +
+
+ + 변환 테이블 설명 + + {selectedTableInfo ? + ( + + + + + {tableForm.formState.errors.trsf_tbl_expln && ( +

+ {tableForm.formState.errors.trsf_tbl_expln.message} +

+ )} +
+ )} + /> + : + - + } +
+
+
+
+
+ + {selectedTableInfo ? + <> +
+ {tableColList.length > 0 && + + } + {selectedTableInfo.tbl_sn && + + } +
+ {selectedTableInfo.tbl_sn ? + (tableColList.length > 0 ? +
+ +
+ : +
+ 등록된 테이블 컬럼이 없습니다. +
) + : +
+ 테이블 정보를 등록해주세요. +
+ } + + : +
+ 연계 데이터를 선택해주세요. +
+ } +
+
+
+ +
+ ) +}; + +export { LinkTablePage }; \ No newline at end of file diff --git a/apps/dm/.env.development b/apps/dm/.env.development new file mode 100644 index 000000000..622545fd6 --- /dev/null +++ b/apps/dm/.env.development @@ -0,0 +1,19 @@ +NEXT_PUBLIC_SITE_ID = DM +NEXT_PUBLIC_PORT = 3000 +NEXT_PUBLIC_PROXY_HOST = http://localhost:3000 +NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 +NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload +NEXT_PUBLIC_CLAIM_NAME = Authorization +NEXT_PUBLIC_AUTH_USER_ID = token-id +NEXT_PUBLIC_ACCESS_TOKEN = access-token +NEXT_PUBLIC_REFRESH_TOKEN = refresh-token + + + +NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API +# NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성 +#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정 + + diff --git a/apps/dm/.env.local b/apps/dm/.env.local new file mode 100644 index 000000000..f691fb313 --- /dev/null +++ b/apps/dm/.env.local @@ -0,0 +1,19 @@ +NEXT_PUBLIC_SITE_ID = DM +NEXT_PUBLIC_CLAIM_NAME = Authorization +NEXT_PUBLIC_AUTH_USER_ID = token-id +NEXT_PUBLIC_ACCESS_TOKEN = access-token +NEXT_PUBLIC_REFRESH_TOKEN = refresh-token +NEXT_PUBLIC_PORT = 11035 +#NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11035 +NEXT_PUBLIC_PROXY_HOST = http://localhost:3000/ +NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 +NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver +NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver +NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload +#NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API +NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성 +#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정 + + diff --git a/apps/dm/.env.production b/apps/dm/.env.production new file mode 100644 index 000000000..7576a7dfb --- /dev/null +++ b/apps/dm/.env.production @@ -0,0 +1,17 @@ +NEXT_PUBLIC_SITE_ID = DM +NEXT_PUBLIC_PORT = 11035 +NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11035 +NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/api/v1 +NEXT_PUBLIC_FO_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/operation-service/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000 +NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload +NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver +NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl +NEXT_PUBLIC_CLAIM_NAME = Authorization +NEXT_PUBLIC_AUTH_USER_ID = token-id +NEXT_PUBLIC_ACCESS_TOKEN = access-token +NEXT_PUBLIC_REFRESH_TOKEN = refresh-token + +NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/sewerage-service/api + + diff --git a/apps/dm/.gitignore b/apps/dm/.gitignore new file mode 100644 index 000000000..c1b3c4e38 --- /dev/null +++ b/apps/dm/.gitignore @@ -0,0 +1,39 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# 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) + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/apps/dm/app/(base-layout)/layout.tsx b/apps/dm/app/(base-layout)/layout.tsx new file mode 100644 index 000000000..5caa088b0 --- /dev/null +++ b/apps/dm/app/(base-layout)/layout.tsx @@ -0,0 +1,16 @@ +"use client"; +import Base from "../base"; +import "../globals.css"; +import { StoreProvider } from "../store"; + +export default function BaseLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/apps/dm/app/(base-layout)/pages/sdmMng/sdmApplyList/page.tsx b/apps/dm/app/(base-layout)/pages/sdmMng/sdmApplyList/page.tsx new file mode 100644 index 000000000..212eca6e7 --- /dev/null +++ b/apps/dm/app/(base-layout)/pages/sdmMng/sdmApplyList/page.tsx @@ -0,0 +1,205 @@ +"use client" +import { Table, TableBody, TableCell, TableRow } from '@common/components/ui/table'; + +import { useDmStore } from '@/app/store'; +import { ContentsDiv, Title, UiModal, UiTable } from '@common/business_components/ui'; +import ListDiv from '@common/business_components/ui/list_div'; +import { useEffect, useState } from 'react'; +import { Toaster } from 'react-hot-toast'; +import SdmLogin from '../sdmLogin/page'; +import type { DemandCompType } from '@/app/store/sdmMng'; +import TransValueFormat from '@/app/common/transValueFormat'; +import { useRouter } from 'next/navigation'; + +const TABLE_STYLES = { + header: "text-center border border-[var(--border)] bg-[rgb(234,243,251)] text-[rgb(100,119,132)] whitespace-pre-line", + cell: "text-right whitespace-pre-line", + fullWidth: "text-center border border-[var(--border)] bg-[rgb(234,243,251)] text-[rgb(100,119,132)] whitespace-pre-line", + table: "fixed w-full" +} as const; + +type SdmApplyProps = { + sdmNo : string; +} + +export default function SdmApplyList({}: SdmApplyProps) { + + const router = useRouter(); + + const {getSdmInfoStatusList, Sdm, Common} = useDmStore((state) => state); + const {successLogin, sdmInfoStatusList, upHeadSdmInfoStatus} = Sdm; + const {selectSidoData, selectSigunData} = Common; + const [isLogin, setIsLogin] = useState(false); + const [isLoginStorageInfo, setIsLoginStorageInfo] = useState(false); + const [sdmApplyDetail, setSdmApplyDetail] = useState({} as DemandCompType); + const [transformedList, setTransformedList] = useState([]); + + useEffect(() => { + console.log('sessionStorage : ',sessionStorage.getItem('user')); + const user = sessionStorage.getItem('user'); + if (user) { + setIsLoginStorageInfo(true); + } + }, []); + + useEffect(() => { + if (successLogin || isLoginStorageInfo) { + setIsLogin(true); + const user = sessionStorage.getItem('user'); + if (user) { + try { + const userData = JSON.parse(user); + getSdmInfoStatusList(userData.dmdInfoId); + } catch (error) { + console.error('Error parsing user data:', error); + } + } + } + }, [successLogin, getSdmInfoStatusList, isLoginStorageInfo]); + + // 데이터 변환을 위한 별도의 useEffect + useEffect(() => { + if (sdmInfoStatusList.length > 0) { + const newTransformedList = sdmInfoStatusList.map((item: DemandCompType) => ({ + ...item, + ctpvCd: selectSidoData.find(sido => sido.val === item.ctpvCd?.trim())?.text || '', + sggCd: selectSigunData.find(sigun => sigun.val === item.sggCd?.trim())?.text || '' + })); + setTransformedList(newTransformedList); + } + }, [sdmInfoStatusList, selectSidoData, selectSigunData]); + + const onClickSdmStatus = (index: number | undefined) => { + if (index !== undefined) { + const selectedStatus = transformedList[index]; + if (selectedStatus) { + setSdmApplyDetail(selectedStatus); + } + } + } + + return ( + <> + + <ContentsDiv> + <ListDiv> + <div className="overflow-y-scroll h-[250px]"> + <div className="h-[110%] w-[99%]"> + <UiTable + headName='수요정보 신청현황' + tableData={transformedList} + headlist={upHeadSdmInfoStatus} + pageSize={0} + total={0} + cellClick={(index) => onClickSdmStatus(index)} + onPageChange={() => {}} + /> + </div> + </div> + </ListDiv> + <ListDiv> + <div className="card-header"> + <h3 className="text-2xl font-medium text-label">수요신청 상세현황</h3> + </div> + <div className={`card !m-[1.2rem] !p-0r`}> + <Table variant="secondary" style={{ tableLayout: 'fixed', width: '100%' }}> + <TableBody> + <TableRow> + <TableCell colSpan={1} className={TABLE_STYLES.header}>요구시기</TableCell> + <TableCell colSpan={2} className={TABLE_STYLES.cell}>{sdmApplyDetail?.dmndTmng?.toString()}</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>요구수량(m³/일)</TableCell> + <TableCell colSpan={2} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.dmndQty} format="number"/>}</TableCell> + </TableRow> + <TableRow> + <TableCell colSpan={1} className={TABLE_STYLES.header}>사용용도</TableCell> + <TableCell colSpan={2} className={TABLE_STYLES.cell}>{sdmApplyDetail?.useUsgCd}</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>희망단가(원/m³)</TableCell> + <TableCell colSpan={2} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.wntUntprc} format="number"/>}</TableCell> + </TableRow> + <TableRow> + <TableCell colSpan={6} className={TABLE_STYLES.fullWidth}>요구수량</TableCell> + </TableRow> + <TableRow> + <TableCell colSpan={1} className={TABLE_STYLES.header}>총대장균군수(개/100ml)</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.grssClfg} format="number"/>}</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>결합잔류염소(mg/L)​</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.combResdch} format="number"/>}</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>탁도(NTU)</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.trbt} format="number"/>}</TableCell> + </TableRow> + <TableRow> + <TableCell colSpan={1} className={TABLE_STYLES.header}>생물학적 산소요구량(BOD)(mg/L)</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.blgcOxygDmndQnt} format="number"/>}</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>냄새</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.smll} format="number"/>}</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>색도(도)</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.crmty} format="number"/>}</TableCell> + </TableRow> + <TableRow> + <TableCell colSpan={1} className={TABLE_STYLES.header}>총질소(T-N)(mg/L)</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.tn} format="number"/>}</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>총인(T-P)(mg/L)​</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.tph} format="number"/>}</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>수소이온농도(pH)​</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.ph} format="number"/>}</TableCell> + </TableRow> + <TableRow> + <TableCell colSpan={1} className={TABLE_STYLES.header}>염화물(mgCL/L)​</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.clIonQnt} format="number"/>}</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>전기전도도(㎲/cm)​</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.cdtnv} format="number"/>}</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>비고</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}>{sdmApplyDetail?.rmrk}</TableCell> + </TableRow> + <TableRow> + <TableCell colSpan={6} className={TABLE_STYLES.fullWidth}>공급처 후보지​</TableCell> + </TableRow> + <TableRow> + <TableCell colSpan={1} className={TABLE_STYLES.header}>후보지1</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}></TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>후보지2</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}></TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>후보지3</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.cell}></TableCell> + </TableRow> + </TableBody> + </Table> + </div> + </ListDiv> + <ListDiv> + <div className="card-header"> + <h3 className="text-2xl font-medium text-label">수요신청 진행상태</h3> + </div> + <div className={`card !m-[1.2rem] !p-0r`}> + <Table variant="secondary" style={{ tableLayout: 'fixed', width: '100%' }}> + <TableBody> + <TableRow> + <TableCell colSpan={1} className={TABLE_STYLES.header}>진행상태</TableCell> + <TableCell colSpan={2} className={TABLE_STYLES.cell}>{sdmApplyDetail?.status?.toString()}</TableCell> + <TableCell colSpan={1} className={TABLE_STYLES.header}>공급처매칭결과</TableCell> + <TableCell colSpan={2} className={TABLE_STYLES.cell}></TableCell> + </TableRow> + </TableBody> + </Table> + </div> + </ListDiv> + </ContentsDiv> + {!isLogin? + <UiModal + body={<SdmLogin/>} + bodyClass="sm:max-w-[90vw] w-[530px] h-[430px]" + open={!isLogin} + onOpenChange={(open) => { + if(!open){ + setIsLogin(false) + router.push('/'); + } + }} + noHeaderYn="Y" + noFooterYn="Y" + /> + :null} + <Toaster position="top-center" /> + </> + ); +} diff --git a/apps/dm/app/(base-layout)/pages/sdmMng/sdmInfoInsert/modalSimilar/page.tsx b/apps/dm/app/(base-layout)/pages/sdmMng/sdmInfoInsert/modalSimilar/page.tsx new file mode 100644 index 000000000..22418c7b7 --- /dev/null +++ b/apps/dm/app/(base-layout)/pages/sdmMng/sdmInfoInsert/modalSimilar/page.tsx @@ -0,0 +1,166 @@ +"use client" + +import { useDmStore } from '@/app/store'; +import type { MoneyDetailType, SimilarCaseType } from '@/app/store/sdmMng'; +import { SelectBox, Title, UiTable } from '@common/business_components/ui'; +import { HeadMakeColSpan } from '@common/business_components/ui/table/HeadMakeColSpan'; +import { Table, TableBody, TableCell, TableHeader, TableRow } from '@common/components/ui/table'; +import { useEffect, useMemo, useState } from 'react'; + +type modalType = { + sply: number | 0; + usageCd: string | ''; + money: number | 0; +}; + +const searchUseTypeList = [ + { text: '공급량', val: 'S' }, + { text: '사용용도', val: 'U' }, +]; + +export default function ModalSimilarPage({ sply = 0, usageCd = '', money = 0 }: modalType) { + const { Sdm, getSimilarCaseList } = useDmStore((state) => state); + const [searchUseType, setSearchUseType] = useState<{text: string, value: string}>({text: '공급량', value: 'S'}); + const [moneyDetailList, setMoneyDetailList] = useState<MoneyDetailType[]>([]); + const { + upHeadSimilarCaseList, + similarCaseList, + upHeadMoneyDetailList, + } = Sdm; + + const rowSpans = useMemo(() => { + const spans: { [key: number]: number } = {}; + let currentValue = ''; + let currentSpan = 0; + let startIndex = 0; + + similarCaseList.forEach((item: SimilarCaseType, index: number) => { + if (item.ruseBizNm === currentValue + || item.bizBgngDate === currentValue + || item.bizEndDate === currentValue + || item.reqBizReuseFcltCst === currentValue) { + currentSpan++; + spans[startIndex] = currentSpan; + } else { + currentValue = item.ruseBizNm; + currentSpan = 1; + startIndex = index; + spans[index] = 1; + } + }); + + return spans; + }, [similarCaseList]); + + const handleCellClick = (index: number) => { + setMoneyDetailList([{ + reqBizGrssCst: similarCaseList[index]?.reqBizGrssCst ?? '', + reqBizReuseFcltCst: similarCaseList[index]?.reqBizReuseFcltCst ?? '', + reqBizPplCst: similarCaseList[index]?.reqBizPplCst ?? '', + reqBizArchCst: similarCaseList[index]?.reqBizArchCst ?? '', + reqBizMchnCst: similarCaseList[index]?.reqBizMchnCst ?? '', + reqBizMtrngCst: similarCaseList[index]?.reqBizMtrngCst ?? '', + reqBizGisCst: similarCaseList[index]?.reqBizGisCst ?? '', + reqBizCctvCst: similarCaseList[index]?.reqBizCctvCst ?? '', + reqBizCmcnCst: similarCaseList[index]?.reqBizCmcnCst ?? '', + reqBizWstCst: similarCaseList[index]?.reqBizWstCst ?? '', + reqBizDesignCst: similarCaseList[index]?.reqBizDesignCst ?? '', + reqBizSpvsCst: similarCaseList[index]?.reqBizSpvsCst ?? '', + reqBizSbsdCst: similarCaseList[index]?.reqBizSbsdCst ?? '', + reqBizEtc: similarCaseList[index]?.reqBizEtc ?? '', + prcmPlanNlep: similarCaseList[index]?.prcmPlanNlep ?? '', + prcmPlanLcep: similarCaseList[index]?.prcmPlanLcep ?? '', + prcmPlanBrdnCst: similarCaseList[index]?.prcmPlanBrdnCst ?? '', + prcmPlanEcqrRt: similarCaseList[index]?.prcmPlanEcqrRt ?? '', + prcmPlanGrssCst: similarCaseList[index]?.prcmPlanGrssCst ?? '', + clclnNlep: similarCaseList[index]?.clclnNlep ?? '', + clclnLcep: similarCaseList[index]?.clclnLcep ?? '', + clclnBrdnCst: similarCaseList[index]?.clclnBrdnCst ?? '', + clclnEcqrRt: similarCaseList[index]?.clclnEcqrRt ?? '', + clclnGrssCst: similarCaseList[index]?.clclnGrssCst ?? '' + }]); + } + + useEffect(() => { + const type = searchUseType.value; + getSimilarCaseList(type, type === 'S' ? sply : type === 'U' ? usageCd : money); + }, [getSimilarCaseList, money, searchUseType, sply, usageCd]); + + return ( + <div className="align-top w-[1360px] overflow-x-auto"> + <Title title="유사사례 조회" /> + <div className="overflow-x-auto h-[450px] w-full mt-8" tabIndex={0} aria-label="유사사례 테이블 영역"> + <div className="w-[150%]"> + <div className="card-header justify-self-start"> + <h3 className="text-2xl font-medium text-label">유사사례</h3> + <SelectBox + tabIndex={0} + aria-label="검색 기준 선택" + selectArray={searchUseTypeList} + value={searchUseType.value} + onSelectValue={(val) => { + setSearchUseType({text: searchUseTypeList.find(item => item.val === val)?.text ?? '', value: val.toString()}); + }} + className="w-[140px] ml-20" + label="" + selectClass="w-[140px]" + /> + </div> + <div className={`card !m-[1.2rem] !p-0r`}> + <Table variant="secondary"> + <TableHeader className="font-bold"> + <HeadMakeColSpan upHeadList={upHeadSimilarCaseList} /> + </TableHeader> + <TableBody className='text-center'> + {similarCaseList.length > 0 ? similarCaseList?.map((el: SimilarCaseType, index) => ( + <TableRow key={`similar-case-row-${index}-${el.ruseBizNm}-${el.bizBgngDate}`}> + {upHeadSimilarCaseList.map((head) => ( + head.id && head.id.length > 1 && ( + <TableCell + tabIndex={head.id === 'someClickableId' ? 0 : undefined} + role="button" + aria-label={`유사사례 ${head.id} 클릭 시 상세 정보 표시`} + className={`${head.align === 'right' ? 'text-right' : head.align === 'center' ? 'text-center' : typeof el[head.id as keyof SimilarCaseType] === 'number' ? 'text-right' : 'text-center'} cursor-pointer`} + key={`similar-case-cell-${head.id}-${index}-${el.ruseBizNm}`} + rowSpan={(head.id === 'businessName' || head.id ==='businessDuration' || head.id === 'money') ? rowSpans[index] : undefined} + style={(head.id === 'businessName' || head.id === 'businessDuration' || head.id === 'money') && index > 0 && similarCaseList[index - 1]?.ruseBizNm === el.ruseBizNm ? { display: 'none' } : undefined} + onClick={() => handleCellClick(index)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + handleCellClick(index); + } + }} + > + {typeof el[head.id as keyof SimilarCaseType] !== 'number' + ? String(el[head.id as keyof SimilarCaseType] ?? '') + : String(el[head.id as keyof SimilarCaseType]).includes('.') + ? String(el[head.id as keyof SimilarCaseType]).split('.').map((part, index) => + index === 0 ? part.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : part + ).join('.') + : String(el[head.id as keyof SimilarCaseType] ?? '').replace(/\B(?=(\d{3})+(?!\d))/g, ",")} + </TableCell> + ) + ))} + </TableRow> + )) : <></>} + </TableBody> + </Table> + </div> + </div> + </div> + <div className="overflow-x-auto h-[270px] w-full mt-8"> + <div className="w-[130%]"> + <UiTable + headName={"사업비 상세정보"} + tableData={moneyDetailList} + headlist={upHeadMoneyDetailList} + pageSize={0} + total={0} + cellClick={undefined} + onPageChange={() => {}} + /> + </div> + </div> + </div> + ) +} \ No newline at end of file diff --git a/apps/dm/app/(base-layout)/pages/sdmMng/sdmInfoInsert/page.tsx b/apps/dm/app/(base-layout)/pages/sdmMng/sdmInfoInsert/page.tsx new file mode 100644 index 000000000..40ff3948f --- /dev/null +++ b/apps/dm/app/(base-layout)/pages/sdmMng/sdmInfoInsert/page.tsx @@ -0,0 +1,275 @@ +"use client"; + +import { useDmStore } from "@/app/store"; +import type { DemandCompType } from "@/app/store/sdmMng"; +import { SdmInfoRegForm } from "@/components/ui/sdmInfoInsertForm/component"; +import { UiModal } from "@common/business_components/ui/dialog_ui"; +import { Button } from "@common/components/ui"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Suspense, useEffect, useState } from "react"; +import { FormProvider, useForm, type SubmitHandler } from "react-hook-form"; +import { Toaster } from "react-hot-toast"; +import * as z from "zod"; +import SdmLogin from "../sdmLogin/page"; + +const sdmInfoFormSchema = z.object({ + sdmNm: z.string() + .min(1, "수요처를 선택해주세요"), + demandYmd: z.date().optional(), + demandQnty: z.string() + .min(1, "요구수량을 입력해주세요") + .regex(/^[0-9]+$/, "요구수량은 숫자만 입력 가능합니다"), + usePurpose: z.string().optional(), + money: z.string() + .regex(/^[0-9]+$/, "희망단가는 숫자만 입력 가능합니다"), + /**요구수질 */ + reqWaterQlty: z.string().optional(), + grssClfg: z.string() + .min(1, "총대장균군수를 입력해주세요") + .regex(/^[0-9]+$/, "총대장균군수는 숫자만 입력 가능합니다"), + tn: z.string() + .min(1, "총질소를 입력해주세요") + .regex(/^[0-9]+$/, "총질소는 숫자만 입력 가능합니다"), + combResdch: z.string() + .min(1, "결합잔류염소를 입력해주세요") + .regex(/^[0-9]+$/, "결합잔류염소는 숫자만 입력 가능합니다"), + tph: z.string() + .min(1, "총인산물을 입력해주세요") + .regex(/^[0-9]+$/, "총인산물은 숫자만 입력 가능합니다"), + trbt: z.string() + .min(1, "탁도를 입력해주세요") + .regex(/^[0-9]+$/, "탁도는 숫자만 입력 가능합니다"), + ph: z.string() + .min(1, "수소이온농도를 입력해주세요") + .regex(/^[0-9]+$/, "수소이온농도는 숫자만 입력 가능합니다"), + bod: z.string() + .min(1, "생물학적 산소요구량을 입력해주세요") + .regex(/^[0-9]+$/, "생물학적 산소요구량은 숫자만 입력 가능합니다"), + /**컬럼확인필요 */ + clIonQnt: z.string() + .min(1, "염화물을 입력해주세요") + .regex(/^[0-9]+$/, "염화물은 숫자만 입력 가능합니다"), + smll: z.string() + .min(1, "냄새를 입력해주세요") + .regex(/^[0-9]+$/, "냄새는 숫자만 입력 가능합니다"), + cdtnv: z.string() + .min(1, "전기전도도를 입력해주세요") + .regex(/^[0-9]+$/, "전기전도도는 숫자만 입력 가능합니다"), + crmty: z.string() + .min(1, "색도를 입력해주세요") + .regex(/^[0-9]+$/, "색도는 숫자만 입력 가능합니다"), + rmrk: z.string().optional(), + /* telno: z.string() + .min(8, "전화번호는 8자 이상 입력해주세요") + .max(12, "전화번호는 12자 이내로 입력해주세요") + .regex(/^[0-9-]+$/, "전화번호는 숫자와 하이픈(-)만 입력 가능합니다"), */ + candidateTitle: z.array(z.object({ + ruseUtztnFcltNm: z.string(), + ruseUtztnFcltCd: z.string(), + })).optional(), +}); + +export type FormData = z.infer<typeof sdmInfoFormSchema>; + +function SdmInfoInsertContent() { + const searchParams = useSearchParams(); + const {map, insertSdmInfoData, Sdm } = useDmStore((state) => state); + const { successLogin } = Sdm; + const [ismoneyDisabled, setIsmoneyDisabled] = useState(false); + const formSchema = sdmInfoFormSchema; + + const [isLogin, setIsLogin] = useState(false); + const [facilityName, setFacilityName] = useState<string>(''); + const [defaultValues, setDefaultValues] = useState<FormData>({ + sdmNm: '', + demandYmd: new Date(), + demandQnty: '', + usePurpose: '', + money: '', + /**요구수질 */ + reqWaterQlty: '', + grssClfg: '', + tn: '', + combResdch: '', + tph: '', + trbt: '', + ph: '', + bod: '', + /**컬럼확인필요 */ + clIonQnt: '', + smll: '', + cdtnv: '', + crmty: '', + rmrk: '', + /** 후보지 제목 */ + candidateTitle: [ + { ruseUtztnFcltNm: '', ruseUtztnFcltCd: '' }, + { ruseUtztnFcltNm: '', ruseUtztnFcltCd: '' }, + { ruseUtztnFcltNm: '', ruseUtztnFcltCd: '' }, + ], + }); + + + + useEffect(() => { + const user = sessionStorage.getItem('user'); + setDefaultValues(prev => ({ + ...prev, + sdmNm: searchParams.get('dmdCoNm') || '', + })); + sessionStorage.setItem('user', searchParams.get('dmdInfoId') || ''); + setIsLogin(user || searchParams.get('dmdInfoId') ? true : false); + }, [searchParams]); + + useEffect(() => { + const local = localStorage.getItem('user'); + if (local) { + setIsLogin(true); + } + }, []); + + const form = useForm<FormData>({ + resolver: zodResolver(formSchema), + defaultValues, + mode: "onChange", + }); + + useEffect(()=>{ + if(map.subsInfo.length>0){ + form.setValue('candidateTitle', map.subsInfo) + } + +},[map.subsInfo, form]) + + // 로그인 성공 시 모달 닫기 및 폼 업데이트 + useEffect(() => { + if (successLogin) { + setIsLogin(true); + const user = sessionStorage.getItem('user'); + if (user) { + try { + const userData = JSON.parse(user); + if (userData.dmdCoNm) { + setFacilityName(userData.dmdCoNm); + form.setValue('sdmNm', userData.dmdCoNm); + } + } catch (error) { + console.error('Error parsing user data:', error); + } + } + } + }, [successLogin, form]); + + // 체크박스 상태가 변경될 때마다 money 필드의 에러를 초기화 + useEffect(() => { + if (ismoneyDisabled) { + form.clearErrors('money'); + form.setValue('money', ''); + } + }, [ismoneyDisabled, form]); +const router = useRouter(); + const onSubmit: SubmitHandler<FormData> = async (data) => { + try { + // Transform form data to match API schema + const apiData: DemandCompType = { + dmdInfoId: JSON.parse(sessionStorage.getItem('user') || '{}')?.dmdInfoId?.toString() || searchParams.get('dmdInfoId') + (searchParams.get('bznmTotNo') || ''), + dmdInfoFk: JSON.parse(sessionStorage.getItem('user') || '{}')?.dmdInfoId?.toString() || searchParams.get('dmdInfoId') + (searchParams.get('bznmTotNo') || ''), + dmdCoNm: JSON.parse(sessionStorage.getItem('user') || '{}')?.dmdCoNm?.toString() || data.sdmNm, + ctpvCd: JSON.parse(sessionStorage.getItem('user') || '{}')?.ctpvCd?.toString() || searchParams.get('ctpvCd') || '', + sggCd: JSON.parse(sessionStorage.getItem('user') || '{}')?.sggCd?.toString() || searchParams.get('sggCd') || '', + daddr: JSON.parse(sessionStorage.getItem('user') || '{}')?.daddr?.toString() || searchParams.get('daddr') || '', + telno: JSON.parse(sessionStorage.getItem('user') || '{}')?.telno?.toString() || searchParams.get('telno') || '', + rgtr: JSON.parse(sessionStorage.getItem('user') || '{}')?.rgtr?.toString() || searchParams.get('rgtr') || '', + useUsgCd: data.usePurpose || '', + wntUntprc: Number(data.money) || '', + dmndTmng: data.demandYmd || new Date(), + dmndQty: Number(data.demandQnty) || '', + grssClfg: Number(data.grssClfg) || '', + tn: Number(data.tn) || '', + combResdch: Number(data.combResdch) || '', + tph: Number(data.tph) || '', + trbt: Number(data.trbt) || '', + ph: Number(data.ph) || '', + blgcOxygDmndQnt: Number(data.bod) || '', + clIonQnt: Number(data.clIonQnt) || '', + smll: Number(data.smll) || '', + cdtnv: Number(data.cdtnv) || '', + crmty: Number(data.crmty) || '', + rmrk: data.rmrk || '', + bzmnNo: "", + dmdAplyPswd: "", + index: 0, + reqDt: "", + dmdInfoSn: 0, + status: '', + splyCndtList: data.candidateTitle || [], + }; + + + + + + await insertSdmInfoData(apiData); + } catch (error) { + console.error('Error submitting form:', error); + } + }; + + return ( + <div className="min-h-screen bg-[#F2F2F2]"> + <div className="container mx-auto px-4 py-8"> + <div className="text-center mb-8"> + <h1 className="text-4xl font-bold text-gray-900 underline"> + 수요정보 등록 + </h1> + </div> + + <div className="max-w-3xl mx-auto bg-white rounded-lg shadow-md p-6"> + <FormProvider {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> + <SdmInfoRegForm + form={form} + onCheckboxChange={setIsmoneyDisabled} + facilityName={facilityName} + /> + <div className="flex justify-center pt-4"> + <Button + type="submit" + className="w-40 bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-md transition-colors duration-200" + > + 수요정보 등록 + </Button> + </div> + </form> + </FormProvider> + </div> + </div> + + {!isLogin ? + <UiModal + body={<SdmLogin />} + bodyClass="sm:max-w-[90vw] w-[530px] h-[430px]" + open={!isLogin} + onOpenChange={(open) => { + if (!open) { + router.push("/"); + } + }} + noHeaderYn="Y" + noFooterYn="Y" + /> + : null} + <Toaster position="top-center" /> + </div> + ); +} + +export default function MyForm() { + return ( + <Suspense fallback={<div>Loading...</div>}> + <SdmInfoInsertContent /> + </Suspense> + ); +} + diff --git a/apps/dm/app/(base-layout)/pages/sdmMng/sdmInsert/page.tsx b/apps/dm/app/(base-layout)/pages/sdmMng/sdmInsert/page.tsx new file mode 100644 index 000000000..926a8f902 --- /dev/null +++ b/apps/dm/app/(base-layout)/pages/sdmMng/sdmInsert/page.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { useDmStore } from "@/app/store"; +import type { DemandCompType } from "@/app/store/sdmMng"; +import SdmInsertForm from '@common/business_components/ui/sdmInsertForm'; +import { useRouter } from "next/navigation"; +import { useCallback, useEffect, useRef } from "react"; +import { Toaster } from "react-hot-toast"; + +export default function MyForm() { + const { Common, initializeCommonData, getDemandCompList, insertSdmData, Sdm } = useDmStore((state) => state); + const { selectSidoData, selectSigunData } = Common; + const { demandCompList, successNextPage } = Sdm; + const router = useRouter(); + const formDataRef = useRef<DemandCompType | null>(null); + + useEffect(() => { + initializeCommonData(); + }, [initializeCommonData]); + + useEffect(() => { + if (successNextPage && formDataRef.current) { + const data = formDataRef.current; + console.log(data); + const queryParams = new URLSearchParams({ + dmdCoNm: data.dmdCoNm || '', + ctpvCd: data.ctpvCd || '', + sggCd: data.sggCd || '', + dmdInfoId: data.dmdInfoId || '', + daddr: data.daddr || '', + telno: data.telno || '', + }); + router.push(`/pages/sdmMng/sdmInfoInsert?${queryParams.toString()}`); + } + }, [successNextPage, router]); + + const callback = useCallback((searchText: string, pageCnt: number) => { + searchText = searchText.trim(); + if (searchText.length > 0) { + getDemandCompList(searchText, pageCnt); + } + }, [getDemandCompList]); + + const handleInsertSdmData = useCallback((data: import('@common/business_components/ui/sdmInsertForm/form').DemandCompType) => { + const convertedData: DemandCompType = { + ...data, + dmdInfoSn: '', + dmdInfoFk: '', + status: '', + }; + formDataRef.current = convertedData; + insertSdmData(convertedData).catch(error => { + console.error('Error inserting SDM data:', error); + }); + }, [insertSdmData]); + + return ( + <div className="min-h-screen bg-[#F2F2F2]"> + <div className="pt-12"> + <SdmInsertForm + selectSidoData={selectSidoData} + selectSigunData={selectSigunData} + dataList={demandCompList} + rtnValue={callback} + rtnInputData={handleInsertSdmData} + /> + <Toaster position="top-center" /> + </div> + </div> + ); +} + diff --git a/apps/dm/app/(base-layout)/pages/sdmMng/sdmLogin/page.tsx b/apps/dm/app/(base-layout)/pages/sdmMng/sdmLogin/page.tsx new file mode 100644 index 000000000..2fd6fc461 --- /dev/null +++ b/apps/dm/app/(base-layout)/pages/sdmMng/sdmLogin/page.tsx @@ -0,0 +1,18 @@ +'use client' + +import { SdmLoginForm } from '@/components/ui/sdmLoginForm/component'; + +export default function SdmLogin() { + return ( + <div className="container mx-auto px-4 py-8"> + <div className="text-center mb-8"> + <h1 className="text-4xl font-bold text-gray-900 underline"> + 수요처 로그인 + </h1> + </div> + <div className="max-w-3xl mx-auto bg-white rounded-lg shadow-md p-6"> + <SdmLoginForm /> + </div> + </div> + ); +} \ No newline at end of file diff --git a/apps/dm/app/(base-layout)/pages/wrAbout/page.tsx b/apps/dm/app/(base-layout)/pages/wrAbout/page.tsx new file mode 100644 index 000000000..8d5bdd0ba --- /dev/null +++ b/apps/dm/app/(base-layout)/pages/wrAbout/page.tsx @@ -0,0 +1,337 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; +import Image from 'next/image'; + +const NAVS = [ + { id: 'card1', label: '물재이용이란' }, + { id: 'card2', label: '물재이용 필요성' }, + { id: 'card3', label: '빗물 재이용' }, + { id: 'card4', label: '중수 재이용' }, + { id: 'card5', label: '하수 재이용' }, + { id: 'card6', label: '물재이용 목표' }, +]; + +export default function WrAboutPage() { + const [activeIdx, setActiveIdx] = useState(0); + const navRefs = useRef<(HTMLLIElement | null)[]>([]); + const cardRefs = useRef<(HTMLElement | null)[]>([]); + + // 클릭 시 이동 + const handleNavClick = (idx: number) => { + setActiveIdx(idx); + const targetCard = cardRefs.current[idx]; + if (targetCard) { + targetCard.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }; + + // 키보드 이벤트 처리 + const handleKeyDown = (e: React.KeyboardEvent, idx: number) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleNavClick(idx); + } + }; + + useEffect(() => { + const observerOptions = { + root: null, + rootMargin: '-200px 0px 0px 0px', // 상단에서 200px 지점을 기준으로 + threshold: 0 + }; + + const observer = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const idx = cardRefs.current.findIndex(ref => ref === entry.target); + if (idx !== -1) { + setActiveIdx(idx); + } + } + }); + }, observerOptions); + + // 각 카드 요소를 관찰 + cardRefs.current.forEach((card) => { + if (card) { + observer.observe(card); + } + }); + + return () => { + cardRefs.current.forEach((card) => { + if (card) { + observer.unobserve(card); + } + }); + }; + }, []); + + return ( + <div className='bg-[#F2F2F2]'> + <main className="relative w-[950px] mx-auto pt-[100px] font-sans"> + {/* 사이드바 */} + <nav className="fixed left-[290px] top-[195px] w-[210px] z-50" aria-label="물재이용 정보 섹션 내비게이션"> + <ul role="list" className="relative space-y-6"> + {NAVS.map((nav, idx) => ( + <li + key={nav.id} + ref={(el) => { navRefs.current[idx] = el; }} + role="listitem" + tabIndex={0} + aria-current={idx === activeIdx ? 'true' : 'false'} + aria-label={`현재 위치: ${nav.label}`} + onClick={() => handleNavClick(idx)} + onKeyDown={(e) => handleKeyDown(e, idx)} + className={`font-NotoSansKR nav-item relative pl-6 text-2xl font-bold text-[#383838] cursor-pointer transition-all duration-300 + after:content-[''] after:absolute after:top-6 after:left-[0.5rem] after:w-[2px] after:h-[24px] after:bg-[#C5C5C5] last:after:hidden + ${idx === activeIdx ? 'text-4xl font-bold text-[#383838]' : ''}`} + > + <span className={`absolute left-[0.5rem] top-1 w-4 h-4 rounded-full -translate-x-1/2 transition-all duration-300 + ${idx === activeIdx ? 'bg-blue-500 border-blue-500' : 'bg-white border-gray-400 border'}`}></span> + {nav.label} + </li> + ))} + </ul> + </nav> + + <article ref={(el) => { cardRefs.current[0] = el; }} className="card-wrap pl-[100px] mb-24 !pt-0" id="card1" aria-labelledby="card1-title"> + <h2 id="card1-title" className="font-NotoSansKR relative text-center text-4xl font-medium text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span> + 물재이용이란 + </h2> + <section aria-labelledby="card1-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] "> + <h1 id="card1-desc" className="font-NotoSansKR text-[#269DDA] text-5xl font-bold mb-8">물 재이용이 무엇인가요?</h1> + <p className="font-NotoSansKR text-[#231815] text-2xl mb-10 leading-relaxed"> + 물 재이용은 빗물, 오수, 하수처리수, 폐수처리수 및 발전소 온배수를<br /> + 물 재이용시설을 이용하여 처리하고, 그 처리된 물을 생활, 공업, 농업, 조경,<br /> + 하천 유지 등의 용도로 이용하는 것을 말합니다. + </p> + <Image + src="/img/wrAbout/img_01_1.png" + alt="이 이미지는 물 재이용 시스템의 개요를 나타냅니다. 가정, 공공기관, 하수처리장 등에서 발생한 생활하수, 빗물, 하수처리수를 물 재이용 시설에서 처리한 후, 청소나 화장실, 농업, 살수차, 하천 유지, 공장, 공원 및 골프장 등 다양한 용도로 활용하는 과정을 도식화한 것입니다." + width={800} + height={400} + className="w-[90%] mx-auto mb-8 rounded" + /> + <p className="font-NotoSansKR text-center font-normal text-2xl"> + 환경부는 2011년에 '물의 재이용 촉진 및 지원에 관한 법률'을 제정하여<br /> + 한국환경공단과 함께 물 재이용 확대 및 촉진을 위해 노력하고 있습니다. + </p> + </section> + </article> + + {/* 카드2 */} + <article ref={(el) => { cardRefs.current[1] = el; }} className="card-wrap pl-[100px] mb-24" id="card2" aria-labelledby="card2-title"> + <h2 id="card2-title" className="font-NotoSansKR relative text-center text-4xl font-medium text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span> + 물재이용 필요성 + </h2> + <section aria-labelledby="card2-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] "> + <h1 id="card2-desc" className="font-NotoSansKR text-[#ED5534] text-5xl font-semibold mb-8">물 재이용, 왜 필요한가요?</h1> + <p className="font-NotoSansKR text-2xl font-normal mb-10 leading-relaxed"> + 빗물, 중수, 하수처리수는 지속적으로 이용 가능한 순환자원입니다.<br /> + 물 재이용을 통한 효율적인 수자원 활용은 물 스트레스국가인 우리나라에서<br />가뭄대응능력 향상 및 하천수질보전을 위해 반드시 필요한 일입니다. + </p> + <Image + src="/img/wrAbout/img_02_1.png" + alt="이 이미지는 기후변화로 인한 온도 상승 추이를 설명한다. 왼쪽에는 지구 그림과 온도계가 함께 표시되어 있으며, 지구 평균 기온은 지난 100년간 약 0.9℃ 상승했음을 나타낸다. 오른쪽에는 대한민국 지도와 함께 온도계가 있으며, 지난 100년간 약 1.8℃ 상승했음을 보여준다. 이는 지구 평균보다 2배 높은 수치이며, 자료 출처는 「기후변화 평가보고서 2020, 환경부·기상청」이다." + width={800} + height={400} + className="w-[90%] mx-auto mb-8 w-[80%]" + /> + <p className="font-NotoSansKR text-2xl mb-10 leading-relaxed"> + - 우리나라의 기후변화 진행속도는 세계 평균을 상회하고 있습니다.<br /> + - 기후변화로 인한 국지적인 가뭄은 더욱 자주, 더욱 심하게 일어나고 있으며, 우리의 물 안보를 위협하고 있습니다. + </p> + <Image + src="/img/wrAbout/img_02_2.png" + alt="세계 평균 1인당 강수량은 15,044m³이며, 대한민국은 2,546m³로 약 1/6 수준입니다. 계절별 강수량은 봄 10%, 여름 70%, 가을 15%, 겨울 5%입니다. 이는 여름철에 강수량이 집중되는 문제를 나타냅니다." + width={800} + height={400} + className="w-[90%] mx-auto mb-8" + /> + <p className="font-NotoSansKR text-2xl mb-10 leading-relaxed"> + - 우리나라의 1인당 강수량은 2,546㎥ 으로 세계 평균 15,044㎥의 약 1/6 수준입니다.<br /> + - 우리나라 강수량의 70%는 여름철에 집중되어 있어 이용 가능한 수자원확보가 어렵습니다. + </p> + <Image + src="/img/wrAbout/img_02_3.png" + alt="물 재이용은 환경을 보호하며 지속 가능한 발전이 가능하게 하고, 안정적인 수자원 확보, 깨끗하고 경제적인 공업용수 공급, 하천 수질 개선을 통한 생태계 회복에 기여합니다." + width={800} + height={400} + className="w-[93%] mx-auto" + /> + </section> + </article> + + {/* 카드3 */} + <article ref={(el) => { cardRefs.current[2] = el; }} className="card-wrap pl-[100px] mb-24" id="card3" aria-labelledby="card3-title"> + <h2 id="card3-title" className="font-NotoSansKR relative text-center text-4xl font-medium text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span> + 빗물 재이용 + </h2> + <section aria-labelledby="card3-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] "> + <h1 id="card3-desc" className="font-NotoSansKR leading-snug text-[#269DDA] text-5xl font-bold mb-6">친환경 수자원 빗물,<br />편리하게 사용할 수 있어요!</h1> + <p className="font-NotoSansKR text-2xl mb-10 leading-relaxed">가뭄을 극복하고 홍수를 예방하기 위해<br />비가 내릴 때 빗물을 모아두었다가 사용합니다.</p> + <Image + src="/img/wrAbout/img_03_1.png" + alt="주택 지붕에 내린 빗물을 저장조에 모아 조경용수, 화장실용수, 세척·살수용수로 재활용하는 과정을 보여줍니다." + width={800} + height={400} + className="w-[90%] mx-auto mb-6" + /> + <Image + src="/img/wrAbout/img_03_2.png" + alt="빗물이용" + width={200} + height={100} + className="mb-5 w-[20%]" + /> + <p className="font-NotoSansKR text-2xl leading-relaxed"> + - 의무 설치대상: 지붕면적 1,000㎡ 이상의 운동장, 체육관, 공공업무시설 등<br /> + - 설 치 기 준: 집수시설, 처리시설, 빗물 저류조(지붕 집수면적 x 0.05m 이상)<br /> + - 활 용 용 도: 골프장 등의 조경용수, 화장실용수, 세척·살수용수 등 + </p> + </section> + </article> + + {/* 카드4 */} + <article ref={(el) => { cardRefs.current[3] = el; }} className="card-wrap pl-[100px] mb-24" id="card4" aria-labelledby="card4-title"> + <h2 id="card4-title" className="font-NotoSansKR relative text-center text-4xl font-medium text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span> + 중수 재이용 + </h2> + <section aria-labelledby="card4-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] "> + <h1 id="card4-desc" className="font-NotoSansKR leading-snug text-[#9771B0] text-5xl font-bold mb-6">요모조모 쓸모 많은 중수,<br />알뜰하게 재이용할 수 있어요!</h1> + <p className="font-NotoSansKR text-2xl mb-10 leading-relaxed">한번 사용한 물을 중수도 시설을 통해 재생하여<br />개별적 또는 지역적으로 재이용할 수 있습니다.</p> + <Image + src="/img/wrAbout/img_04_1.png" + alt="욕실, 세면대, 주방, 세탁기 등에서 발생한 생활용수를 중수도 시설에서 처리하여 화장실 변기용수와 조경용수로 재이용하는 시스템 흐름도입니다." + width={800} + height={400} + className="w-[90%] mx-auto mb-6" + /> + <Image + src="/img/wrAbout/img_04_2.png" + alt="중수재이용" + width={200} + height={100} + className="mb-5 w-[20%]" + /> + <p className="font-NotoSansKR text-2xl leading-relaxed"> + - 의무 설치대상: 일정규모 이상의 대규모점포, 숙박/목욕장, 공장, 각종 개발사업 등<br /> + - 의무 재이용량: 물 사용량의 10% 이상<br /> + - 활 용 용 도: 청소·화장실용수, 세척·살수용수, 조경용수, 공업용수 등 + </p> + </section> + </article> + + {/* 카드5 */} + <article ref={(el) => { cardRefs.current[4] = el; }} className="card-wrap pl-[100px] mb-24" id="card5" aria-labelledby="card5-title"> + <h2 id="card5-title" className="font-NotoSansKR relative text-center text-4xl font-medium text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span> + 하수 재이용 + </h2> + <section aria-labelledby="card5-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] "> + <h1 id="card5-desc" className="font-NotoSansKR leading-snug text-[#0099A4] text-5xl font-bold mb-6">아직 할 일이 많은 하수처리수,<br />풍족하게 재이용할 수 있어요!</h1> + <p className="font-NotoSansKR text-2xl mb-10 leading-relaxed">사용하고 버려져 공공하수처리장으로 모여든 물은<br />용도에 맞는 처리 후 재이용 할 수 있습니다.</p> + <Image + src="/img/wrAbout/img_05_1.png" + alt="주택과 건물에서 발생한 하수가 공공하수처리장을 거쳐 처리된 후, 물 재이용 시설에서 정화되어 공업용수, 하천유지용수, 농업용수, 세척·살수용수로 재활용됩니다." + width={800} + height={400} + className="w-[90%] mx-auto mb-6" + /> + <Image + src="/img/wrAbout/img_05_2.png" + alt="하수처리 재이용" + width={200} + height={100} + className="mb-5 w-[20%]" + /> + <p className="font-NotoSansKR text-2xl leading-relaxed"> + - 의무 이용대상: 공공하수처리장(시설용량 5,000㎥/일 이상)<br /> + - 의무 재이용량: 일 하수처리량의 10% 이상<br /> + - 활 용 용 도: 공업용수, 하천유지용수, 농업용수 등 + </p> + </section> + </article> + + {/* 카드6 */} + <article ref={(el) => { cardRefs.current[5] = el; }} className="card-wrap pl-[100px] mb-24" id="card6" aria-labelledby="card6-title"> + <h2 id="card6-title" className="font-NotoSansKR relative text-center text-4xl font-medium text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span> + 물재이용 목표 + </h2> + <section aria-labelledby="card6-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] "> + <h1 id="card6-desc" className="font-NotoSansKR leading-snug text-[#269DDA] text-5xl font-bold mb-6">2030년 물 재이용 목표를 위해<br />모두의 관심과 노력이 필요합니다.</h1> + <p className="font-NotoSansKR text-2xl mb-6 leading-relaxed"> + 환경부는 2030년까지의 제2차 물 재이용 기본계획을 수립하여<br />한국환경공단과 함께 물 재이용 확대 및 촉진을 위해 노력하고 있습니다. + </p> + <Image + src="/img/wrAbout/img_06_1.png" + alt="지속가능한 물 재이용 정착으로 건전한 물순환 확산" + width={800} + height={400} + className="w-[80%] mx-auto mb-6" + /> + <div className="flex gap-3 flex-wrap justify-between bg-gray-100 p-6 rounded-lg mb-8"> + <div className="flex-1 text-center"> + <Image + src="/img/wrAbout/icon_01.png" + alt="" + width={70} + height={70} + className="mx-auto mb-4 w-[auto] h-[70px]" + /> + <h3 className="font-NotoSansKR text-center font-medium text-nowrap mb-2 text-2xl">도시 물 순환이용<br />체계로의 전환</h3> + <ul className="font-NotoSansKR text-xl text-gray-600 leading-relaxed space-y-1"> + <li className="text-nowrap">- 지속가능한 물 순환이용체계 구축</li> + <li>- 물 재이용시설 관리체계 강화</li> + </ul> + </div> + <div className="flex-1 text-center"> + <Image + src="/img/wrAbout/icon_02.png" + alt="" + width={70} + height={70} + className="mx-auto mb-4 w-[auto] h-[70px]" + /> + <h3 className="font-NotoSansKR text-center font-medium text-nowrap mb-2 text-2xl">유역기반 하수처리수 재이용<br />수요확대 및 공급체계 구축</h3> + <ul className="font-NotoSansKR text-xl text-gray-600 leading-relaxed space-y-1"> + <li>- 유역기반 통합물관리 연계 강화</li> + <li>- 하수처리수 재이용 공업용수 공급 활성화</li> + <li>- 하수처리수 재이용을 활용한 물순환 촉진</li> + </ul> + </div> + <div className="flex-1 text-center"> + <Image + src="/img/wrAbout/icon_03.png" + alt="" + width={70} + height={70} + className="mx-auto mb-4 w-[auto] h-[70px]" + /> + <h3 className="font-NotoSansKR text-center font-medium text-nowrap mb-2 text-2xl">물 재이용 인식제고<br />및 산업발전 기반 마련</h3> + <ul className="font-NotoSansKR text-xl text-gray-600 leading-relaxed space-y-1"> + <li>- 물 재이용 교육 및 대국민 홍보 강화</li> + <li>- 핵심기술 육성 및 산업발전 지원 강화</li> + </ul> + </div> + </div> + <Image + src="/img/wrAbout/img_06_2.png" + alt="물 재이용량은 2014년 13.3억톤에서 2030년 25.9억톤까지 증가할 것으로 예상됩니다. 그에 따른 기대 효과로는 연간 상수대체 17.9억톤, 물산업 시장성장 약 2조3,541억 원, 건천화된 73개 하천 해소, 약 20,550명의 고용 효과가 포함됩니다." + width={800} + height={400} + className="w-[90%] mx-auto" + /> + </section> + </article> + </main> + </div> + ); +} diff --git a/apps/dm/app/(base-layout)/pages/wrExample/page.tsx b/apps/dm/app/(base-layout)/pages/wrExample/page.tsx new file mode 100644 index 000000000..4849c02b0 --- /dev/null +++ b/apps/dm/app/(base-layout)/pages/wrExample/page.tsx @@ -0,0 +1,364 @@ +import Image from "next/image"; + +export default function WrExamplePage() { + return ( + <div className="bg-[#F2F2F2]"> + <div className="relative w-[950px] mx-auto pt-[50px] font-sans"> + <h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 rounded-full"></span> + 물 재이용사례 + </h2> + </div> + <div className="bg-[#F2F2F2] text-gray-800"> + <div className="relative w-[950px] mx-auto pt-[50px] font-sans"> + {/* 카드 1 */} + <div className="card-wrap mb-24" id="card1"> + <h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span> + 사업사례 1 + </h2> + <div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]"> + {/* 사업명 */} + <section> + <h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2> + <p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed"> + 경기권역 반도체 사업장 물 재이용 사업<br /> + : 삼성전자 반도체 사업장 하수재이용수 공급(재이용시설 설치, 공급망 구축 등) + </p> + </section> + {/* 사업 배경 및 목적 */} + <section className="mt-8"> + <h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 배경 및 목적</h2> + <p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed"> + 삼성전자는 환경경영전략 실현과 반도체 공장의 안정적 용수 공급을 위해 하수 재이용수를 공업용수로 도입하기로 결정 + </p> + </section> + {/* 사업 주요 내용 */} + <section className="mt-8"> + <h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2> + <p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed"> + 1단계 사업[화성·오산(12만㎥/일) → 삼성 화성·기흥(29년 공급)]<br /> + 2단계 사업[수원(21만㎥/일) → 삼성 평택(30년 공급)] + </p> + </section> + {/* 특이사항 */} + <section className="mt-8"> + <h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2> + <p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed"> + 본 사업은 광역 하수 재이용 공급망 구축의 첫 사업으로 초기 민관협의체를 구성·운영 후 관계기관(환경부, 경기도, 지자체, 공단, 수공, 삼성전자) 간 MOU 체결을 통해 사업을 추진하였으며, 반도체 사업장에 하수 재이용수를 공업용수로 공급하는 국내 첫 사업으로 위협부담 해소를 위해 사업을 단계별로 시행 + </p> + </section> + {/* 단계별 시각 정보 */} + <section className="bg-white px-4 mt-8"> + <Image + src="/img/wrExample/img_ex_01_1.png" + alt="사업단계 설명" + width={800} + height={400} + className="w-[100%] mx-auto mb-8" + /> + </section> + </div> + </div> + + {/* 카드2 */} + <div className="card-wrap mb-24" id="card2"> + <h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span> + 사업사례 2 + </h2> + <div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]"> + {/* 사업명 */} + <section> + <h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2> + <p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed"> + 경기도 파주시 하수재이용<br /> + : 운정·금촌하수처리시설의 하수처리수를 막공법(U/F, R/O)으로 재처리하여 파주 LCD 산업단지내 3개 업체*에 공업용수 공급<br /><br /> + *LG디스플레이(P10공장), 희성전자, 엔비텍 등 + </p> + </section> + {/* 사업 방식 */} + <section className="mt-8"> + <h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 방식</h2> + <p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed"> + 민간투자사업(시행사 : 파주그린허브(주), 운영사 : 테크로스) + </p> + </section> + {/* 사업 주요 내용 */} + <section className="mt-8"> + <h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2> + <p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed"> + 처리공정 : 전처리분리막(U/F) + 역삼투서비(R/O)<br /> + 공급량 : 4만m³/일<br /> + 관로 : 13.6km<br /> + 사업비 : 651억원 + </p> + </section> + {/* 특이사항 */} + <section className="mt-8"> + <h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2> + <p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed"> + 하수처리수 재이용시설 자체 농축폐수 처리공정 개발 및 적용 성공사례 + </p> + <div className="grid grid-cols-1 gap-0 items-start"> + {/* 좌측 이미지 */} + <div> + <Image + src="/img/wrExample/img_ex_02_1.png" + alt="적용 성공사례 지도" + width={1000} + height={1200} + className="w-[100%] mx-auto" + /> + </div> + + {/* 우측 표 */} + <div className="space-y-3"> + {/* 하수처리수 공급 */} + <div> + <table className="min-w-full h-[500px]text-center border border-black"> + <colgroup> + <col style={{ width: "20%" }} /> + <col style={{ width: "40%" }} /> + <col style={{ width: "40%" }} /> + </colgroup> + <tbody> + <tr> + <td colSpan={3} className="font-NotoSansKR bg-[#ffc000] h-[30px] text-xl text-white font-semibold text-center pt-[6px] pb-[6px] px-0">하수처리수 공급</td> + </tr> + <tr className="text-xl h-[30px]"> + <td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">위치</td> + <td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">금촌하수</td> + <td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">운정하수</td> + </tr> + <tr className="text-sm"> + <td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">시설용량</td> + <td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">44,000㎥/일</td> + <td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">60,000㎥/일</td> + </tr> + <tr className="text-sm"> + <td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">유입량</td> + <td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">27,000㎥/일<br />자연유하</td> + <td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">30,000㎥/일<br />이송펌프장</td> + </tr> + </tbody> + </table> + {/* 첫 화살표 (자연유하 / 관로이송) */} + <div className="flex h-[30px] justify-between items-center my-3 px-4"> + <div className="flex text-xl items-center gap-1 text-orange-600 font-bold pl-6"> + <span className="font-NotoSansKR">자연유하: 161m</span> + <Image + src="/img/wrExample/img_arrow_down_1.png" + alt="오른쪽 화살표" + width={24} + height={24} + className="h-6 w-6" + /> + </div> + <div className="flex text-xl items-center gap-1 text-orange-600 font-bold"> + <Image + src="/img/wrExample/img_arrow_down_1.png" + alt="왼쪽 화살표" + width={24} + height={24} + className="h-6 w-6" + /> + <span className="font-NotoSansKR">관로이송: 3.2km</span> + </div> + </div> + </div> + + {/* 물재이용 처리시설 */} + <div> + <table className="min-w-full text-center border border-black"> + <colgroup> + <col style={{ width: "50%" }} /> + <col style={{ width: "50%" }} /> + </colgroup> + <tbody> + <tr> + <td colSpan={2} className="font-NotoSansKR bg-[#00b050] text-xl h-[30px] text-white font-semibold text-center pt-[6px] pb-[6px] px-0">물재이용 처리시설</td> + </tr> + <tr className="text-sm"> + <td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">유입유량</td> + <td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">57,000㎥/일</td> + </tr> + <tr className="text-sm"> + <td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">재이용시설</td> + <td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">40,000㎥/일</td> + </tr> + <tr className="text-sm"> + <td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">농축폐수처리</td> + <td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">17,000㎥/일</td> + </tr> + </tbody> + </table> + {/* 두 번째 화살표 (공급관로) */} + <div className="flex flex-col items-center my-3"> + <div className="flex h-[30px] items-center gap-1 text-xl text-blue-600 font-semibold"> + <span className="font-NotoSansKR">공급관로 L=10.4km</span> + <Image + src="/img/wrExample/img_arrow_down_2.png" + alt="아래 화살표" + width={24} + height={24} + className="h-6 w-6" + /> + </div> + </div> + </div> + + {/* 수요처 공급 */} + <div> + <table className="min-w-full text-center border border-black"> + <colgroup> + <col style={{ width: "50%" }} /> + <col style={{ width: "50%" }} /> + </colgroup> + <tbody> + <tr> + <td colSpan={2} className="font-NotoSansKR bg-[#3399ff] h-[30px] text-xl text-white font-semibold text-center pt-[6px] pb-[6px] px-0">수요처 공급</td> + </tr> + <tr className="text-sm"> + <td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">수요처</td> + <td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">LG디스플레이 외 2개사</td> + </tr> + <tr className="text-sm"> + <td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">수요량</td> + <td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">40,000㎥/일</td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + </section> + </div> + </div> + + {/* 카드3 */} + <div className="card-wrap mb-24" id="card3"> + <h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span> + 사업사례 3 + </h2> + <div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]"> + {/* 사업명 */} + <section> + <h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2> + <p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed"> + 충남 보령 하수재이용<br /> + : 보령·신보령화력발전소에 하루 1만톤의 탈황용수 공급을 위해 보령하수처리시설의 하수처리수 재이용 사업 추진 + </p> + </section> + {/* 사업 방식 */} + <section className="mt-8"> + <h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 방식</h2> + <p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed"> + 2017년 한국환경공단과 사업대행 위 · 수탁 협약 체결 + </p> + </section> + {/* 사업 주요 내용 */} + <section className="mt-8"> + <h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2> + <p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed"> + 처리공정 : 전처리분리막(U/F) + 역삼투서비(R/O)<br /> + 공급량 : 1만m3/일<br /> + 관로 : 8.7km<br /> + 사업비 : 315억원(국비 216억원, 재원협의(안)) + </p> + </section> + {/* 특이사항 */} + <section className="mt-8"> + <h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2> + <p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed"> + 국내 유일 하수재이용 공업용수 공급 재정사업으로 2016년 충남 서부권 가뭄 시 지역사회 기여 차원에서 기관간 MOU (환경부, 충청남도, 보령시, 한국중부발전㈜)를 바탕 + 으로 추진 되어 경제성보다 공익성을 바탕으로 국가전력 기반시설의 안정적 운영 도모 + </p> + <div className="p-2"> + <Image + src="/img/wrExample/img_ex_03_1.png" + alt="국가전력 기반시설 설명" + width={700} + height={350} + className="w-[100%] mx-auto" + /> + </div> + </section> + </div> + </div> + + {/* 카드4 */} + <div className="card-wrap mb-24" id="card4"> + <h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span> + 사업사례 4 + </h2> + <div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]"> + {/* 사업명 */} + <section> + <h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2> + <p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed"> + 경기도 수원시 빗물이용과 중수도 + </p> + </section> + {/* 사업 내용 */} + <section className="mt-8"> + <h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 내용</h2> + <p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed"> + (빗물) 월드컵경기장 등 4개소에 노면 빗물분사시스템 및 빗물 공급기를 설치하여 혹서기 도시 열섬효과 및 도로 비산먼지 저감<br /> +   ※ 시설 가동중 도로 노면 온도의 7.2 ~ 26.3℃ 감소 효과 확인(21.7월, 수원시청).<br /> +   다만, 가뭄기나 강우량이 적을 경우 빗물을 원활히 공급하기 다소 곤란<br /><br /> + - (중수도) 수원시청, 장안구청, 월드컵경기장에 빗물 연계 중수도를 화장실·조경용수 등으로 사용하여 수도요금 절감(연 7백~30백만원) + </p> + <div className="grid grid-cols-1 md:grid-cols-2 gap-2 items-start p-2"> + <div> + <Image + src="/img/wrExample/img_ex_04_1.png" + alt="월드컵경기장 인근도로 노면 살수 이미지" + width={350} + height={250} + className="w-[100%] mx-auto" + /> + <span className="font-NotoSansKR text-xl block text-center font-semibold my-2"><월드컵경기장 인근도로 노면 살수></span> + </div> + <div> + <Image + src="/img/wrExample/img_ex_04_2.png" + alt="주유기 모양의 빗물 공급기 이미지" + width={350} + height={250} + className="w-[100%] mx-auto" + /> + <span className="font-NotoSansKR text-xl block text-center font-semibold my-2"><주유기 모양의 빗물 공급기></span> + </div> + <div> + <Image + src="/img/wrExample/img_ex_04_3.png" + alt="빗물주유기에서 살수용수 공급 이미지" + width={350} + height={250} + className="w-[100%] mx-auto" + /> + <span className="font-NotoSansKR text-xl block text-center font-semibold my-2"><빗물주유기에서 살수용수 공급></span> + </div> + <div> + <Image + src="/img/wrExample/img_ex_04_4.png" + alt="빗물 공급기 및 빗물분사장치 위치" + width={350} + height={250} + className="w-[100%] mx-auto" + /> + <span className="font-NotoSansKR text-xl block text-center font-semibold my-2"><빗물 공급기 및 빗물분사장치 위치></span> + </div> + </div> + </section> + </div> + </div> + </div> + </div> + </div> + ); +} + diff --git a/apps/dm/app/(base-layout)/pages/wrOverview/page.tsx b/apps/dm/app/(base-layout)/pages/wrOverview/page.tsx new file mode 100644 index 000000000..3e5825899 --- /dev/null +++ b/apps/dm/app/(base-layout)/pages/wrOverview/page.tsx @@ -0,0 +1,113 @@ +import Image from "next/image"; + +export default function WrOverviewPage() { + return ( + <div className="bg-[#F2F2F2]"> + <div className="relative w-[950px] mx-auto pt-[50px] font-sans"> + {/* 카드 1 - 물재이용 사업 추진 절차 */} + <div className="card-wrap mb-24" id="card1"> + <h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span> + 물재이용 사업 추진 절차 + </h2> + <div className="bg-white border border-gray-300 rounded-xl p-[60px] py-[50px]"> + <table className="table-fixed w-full text-center text-sm"> + <caption className="sr-only">물 재이용 추진 절차 상세 표</caption> + <colgroup> + <col style={{ width: "28%" }} /> + <col style={{ width: "2%" }} /> + <col style={{ width: "35%" }} /> + <col style={{ width: "5%" }} /> + <col style={{ width: "30%" }} /> + </colgroup> + <tbody> + {/* 1단계 */} + <tr><td></td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">타당성 조사</td><td></td><td></td></tr> + <tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr> + {/* 2단계 */} + <tr><td className="font-NotoSansKR text-2xl bg-[#F4D62D] text-black font-bold py-5">매칭시스템 신청</td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">사업계획(안) 수립</td><td></td><td></td></tr> + <tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr> + {/* 3단계 */} + <tr><td className="font-NotoSansKR text-2xl bg-[#F4D62D] text-black font-bold py-5">하수도정보시스템 신청</td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">국고지원 신청</td><td className="font-NotoSansKR text-2xl font-bold">→</td><td className="font-NotoSansKR text-2xl text-[#fff] bg-[#00B076] font-bold py-5">기술검토(한국환경공단)</td></tr> + <tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr> + {/* 4단계 */} + <tr><td></td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">국고지원 확정 통보</td><td></td><td></td></tr> + <tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr> + {/* 5단계 */} + <tr><td></td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">사업계획 수립(실시설계)</td><td></td><td></td></tr> + <tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr> + {/* 6단계 */} + <tr><td className="font-NotoSansKR text-2xl bg-[#F4D62D] text-black font-bold py-5">(행정관) 기술검토 신청</td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">재원협의(지방환경관서장)</td><td className="font-NotoSansKR text-2xl font-bold">→</td><td className="font-NotoSansKR text-2xl text-[#fff] bg-[#00B076] font-bold py-5">기술검토(한국환경공단)</td></tr> + <tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr> + {/* 7단계 */} + <tr><td className="font-NotoSansKR text-2xl bg-[#F4D62D] text-black font-bold py-5">(행정관) 기술검토 신청</td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">설치승인(시도지사)</td><td className="font-NotoSansKR text-2xl font-bold">→</td><td className="font-NotoSansKR text-2xl text-[#fff] bg-[#00B076] font-bold py-5">기술검토(한국환경공단)</td></tr> + <tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr> + {/* 8단계 */} + <tr><td></td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">공사시행</td><td></td><td></td></tr> + <tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr> + {/* 9단계 */} + <tr><td className="font-NotoSansKR text-2xl bg-[#F4D62D] text-black font-bold py-5">(행정관) 기술검토 신청</td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">준공(국고정산)</td><td className="font-NotoSansKR text-2xl font-bold">→</td><td className="font-NotoSansKR text-2xl text-[#fff] bg-[#00B076] font-bold py-5">기술검토(한국환경공단)</td></tr> + </tbody> + </table> + </div> + </div> + + {/* 카드 2 - 물재이용 매칭 시스템 */} + <div className="mt-36 card-wrap mb-24" id="card2"> + <h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6"> + <span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span> + 물재이용 매칭 시스템이란? + </h2> + <div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]"> + {/* 개요 */} + <section> + <h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">개요</h2> + <p className="font-NotoSansKR text-2xl font-bold text-gray-800 leading-relaxed"> + 기후변화 및 지자체의 개발속도 차이에 따라 물 자원의 수요-공급 불균형이 발생하여 빗물, 중수도, 하수처리 재처리수 등의 공급 여유가 있는 지자체의 재이용수를 부족한 지자체 수요처에 공급하여 불균형 해소를 통한 수자원 절약 및 물 자원 순환 + </p> + </section> + + {/* 구성 */} + <section className="mt-8"> + <h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">구성</h2> + <ul className=" font-NotoSansKR list-[square] pl-6 text-2xl font-bold text-gray-800 leading-relaxed space-y-2"> + <li>공급자: 하수처리장, 빗물이용시설, 중수도 등</li> + <li>수요자: 산업체(냉각수), 공공시설(조경, 도로청소), 농민(농업용수) 등</li> + <li>시스템: 수질, 수량, 거리 기반 매칭, 중개</li> + <li>운영주체: 한국환경공단</li> + </ul> + </section> + + {/* 절차 */} + <section className="mt-8"> + <h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2> + <p className="font-NotoSansKR text-2xl font-bold text-gray-800 leading-relaxed"> + ① 공급자 등록 → ② 수요자 조건 입력 → ③ 매칭 검토 → ④ 시스템 매칭 → ⑤ 계약·공급 이행 + </p> + </section> + + {/* 기대효과 */} + <section className="mt-8"> + <h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">기대효과</h2> + <ul className="font-NotoSansKR list-[square] pl-6 text-2xl font-bold text-gray-800 leading-relaxed space-y-2"> + <li>물 자원 수요-공급 조정을 통한 효율적 분배</li> + <li>물 재이용시설 데이터 수집으로 체계적 관리</li> + <li>지자체, 사업시행자 등 재이용 정보 획득 용이</li> + </ul> + <div className="text-center my-6"> + <Image + src="/img/wrOverview/img_ov_01_1.png" + alt="하수 재이용 시스템 흐름도. 저장탱크에서 재처리된 물이 손 씻기, 수도, 세정 등에 공급되며 재이용 활성화를 통해 최적의 물공급 방안이 마련되는 과정을 시각화함." + width={650} + height={500} + className="h-[500px] mx-auto mb-4 rounded" + /> + <p className="font-NotoSansKR text-2xl font-bold text-gray-800 mt-10">(하수 재이용 시스템 개요도)</p> + </div> + </section> + </div> + </div> + </div> + </div> + ); +} \ No newline at end of file diff --git a/apps/dm/app/(base-layout)/pages/wrStatus/page.tsx b/apps/dm/app/(base-layout)/pages/wrStatus/page.tsx new file mode 100644 index 000000000..7ae840beb --- /dev/null +++ b/apps/dm/app/(base-layout)/pages/wrStatus/page.tsx @@ -0,0 +1,549 @@ +'use client'; + +import { CHART_FORM, MainChart, type ChartConfig } from '@common/business_components'; +import React, { useEffect, useRef, useState } from 'react'; + +interface RegionData { + name: string; + x: number; + y: number; + values: number[]; +} + +interface TableData { + region: string; + sewage: string; + rainwater: string; + reclaimed: string; + thermal: string; +} + + +const WrStatusPage: React.FC = () => { + const [selectedRegion, setSelectedRegion] = useState<string | null>(null); + const [hoveredRegion, setHoveredRegion] = useState<string | null>(null); + const [tooltipPosition, setTooltipPosition] = useState<{ x: number; y: number } | null>(null); + const svgContainerRef = useRef<HTMLDivElement>(null); + const [chartData] = useState<any[]>([ + { + label : '서울', + 하수 : 4, + 빗물 : 816, + 중수도 : 371, + 온배수 : 0 + }, + { + label : '부산', + 하수 : 13, + 빗물 : 68, + 중수도 : 20, + 온배수 : 1 + }, + { + label : '대구', + 하수 : 9, + 빗물 : 294, + 중수도 : 14, + 온배수 : 2 + }, + { + label : '인천', + 하수 : 17, + 빗물 : 165, + 중수도 : 35, + 온배수 : 15 + }, + { + label : '광주', + 하수 : 3, + 빗물 : 74, + 중수도 : 15, + 온배수 : 15 + }, + { + label : '대전', + 하수 : 2, + 빗물 : 52, + 중수도 : 11, + 온배수 : 20 + }, + { + label : '울산', + 하수 : 8, + 빗물 : 111, + 중수도 : 19, + 온배수 : 30 + }, + { + label : '세종', + 하수 : 9, + 빗물 : 65, + 중수도 : 0, + 온배수 : 0 + }, + { + label : '경기', + 하수 : 176, + 빗물 : 1009, + 중수도 : 252, + 온배수 : 40 + }, + { + label : '강원', + 하수 : 69, + 빗물 : 69, + 중수도 : 22, + 온배수 : 0 + }, + { + label : '충북', + 하수 : 38, + 빗물 : 17, + 중수도 : 91, + 온배수 : 0 + }, + { + label : '충남', + 하수 : 67, + 빗물 : 54, + 중수도 : 188, + 온배수 : 0 + }, + { + label : '전북', + 하수 : 55, + 빗물 : 33, + 중수도 : 220, + 온배수 : 0 + }, + { + label : '전남', + 하수 : 83, + 빗물 : 16, + 중수도 : 75, + 온배수 : 0 + }, + { + label : '경북', + 하수 : 81, + 빗물 : 37, + 중수도 : 70, + 온배수 : 0 + }, + { + label : '경남', + 하수 : 75, + 빗물 : 27, + 중수도 : 140, + 온배수 : 0 + }, + { + label : '제주', + 하수 : 8, + 빗물 : 47, + 중수도 : 64, + 온배수 : 0 + } + ]); + + const chartConfig = chartData.reduce((config, element) => { + config[element.label] = { + label: element.label, + color : "hsl(var(--chart-1))" + }; + return config; + }, {} as Record<string, { label: string; color: string; }>) satisfies ChartConfig; + + const chartOption = { + chartForm : CHART_FORM.BAR, + chartConfig : chartConfig, + chartData : chartData, + chartTitle : '', + chartClassName : "h-[300px] w-full", + graphSize : 40, + yFieldText : '시설(개소)', + showLegend : true + } + + const colorLegend = [ + { label: '하수', color: '#FF6B6B', key: 'sewage' }, + { label: '빗물', color: '#4ECDC4', key: 'rainwater' }, + { label: '중수도', color: '#45B7D1', key: 'reclaimed' }, + { label: '온배수', color: '#FED16A', key: 'thermal' }, + ]; + + const regionIdMap: Record<string, string> = { + 서울: '서울특별시', + 부산: '부산광역시', + 대구: '대구광역시', + 인천: '인천광역시', + 광주: '광주광역시', + 대전: '대전광역시', + 울산: '울산광역시', + 세종: '세종특별자치시', + 경기: '경기도', + 강원: '강원특별자치도', + 충북: '충청북도', + 충남: '충청남도', + 전북: '전라북도', + 전남: '전라남도', + 경북: '경상북도', + 경남: '경상남도', + 제주: '제주특별자치도' + }; + + const tableData: TableData[] = [ + { region: '전국', sewage: '717(1,137,538)', rainwater: '3544(13,945)', reclaimed: '1018(222,476)', thermal: '123' }, + { region: '서울특별시', sewage: '4(114,908)', rainwater: '816(430)', reclaimed: '371(341)', thermal: '0' }, + { region: '부산광역시', sewage: '13(146,197)', rainwater: '68(335)', reclaimed: '20(512)', thermal: '1' }, + { region: '대구광역시', sewage: '9(102,380)', rainwater: '294(133)', reclaimed: '14(44)', thermal: '2' }, + { region: '인천광역시', sewage: '17(57,691)', rainwater: '165(642)', reclaimed: '35(7,859)', thermal: '15' }, + { region: '광주광역시', sewage: '3(28,738)', rainwater: '74(23)', reclaimed: '15(450)', thermal: '15' }, + { region: '대전광역시', sewage: '2(11,966)', rainwater: '52(2)', reclaimed: '11(492)', thermal: '20' }, + { region: '울산광역시', sewage: '8(36,076)', rainwater: '111(39)', reclaimed: '19(28,904)', thermal: '30' }, + { region: '세종특별자치시', sewage: '9(2,555)', rainwater: '65(16)', reclaimed: '0(0)', thermal: '0' }, + { region: '경기도', sewage: '176(313,298)', rainwater: '1009(4,735)', reclaimed: '252(77,096)', thermal: '40' }, + { region: '강원특별자치도', sewage: '69(19,677)', rainwater: '69(4)', reclaimed: '22(447)', thermal: '0' }, + { region: '충청북도', sewage: '38(15,607)', rainwater: '17(91)', reclaimed: '91(2,211)', thermal: '0' }, + { region: '충청남도', sewage: '67(41,368)', rainwater: '54(77)', reclaimed: '188(50,434)', thermal: '0' }, + { region: '전라북도', sewage: '55(42,671)', rainwater: '33(199)', reclaimed: '220(9,453)', thermal: '0' }, + { region: '전라남도', sewage: '83(23,560)', rainwater: '16(885)', reclaimed: '75(15,412)', thermal: '0' }, + { region: '경상북도', sewage: '81(129,081)', rainwater: '37(341)', reclaimed: '70(27,818)', thermal: '0' }, + { region: '경상남도', sewage: '75(50,101)', rainwater: '27(244)', reclaimed: '140(343)', thermal: '0' }, + { region: '제주특별자치도', sewage: '8(1,666)', rainwater: '47(5,746)', reclaimed: '64(660)', thermal: '0' }, + ]; + + const regionData: RegionData[] = [ + { name: '서울', x: 170, y: 140, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '인천', x: 120, y: 150, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '경기', x: 210, y: 210, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '강원', x: 330, y: 120, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '충북', x: 260, y: 280, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '충남', x: 100, y: 320, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '세종', x: 180, y: 305, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '대전', x: 195, y: 360, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '전북', x: 170, y: 450, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '전남', x: 160, y: 600, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '광주', x: 120, y: 530, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '경북', x: 400, y: 320, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '경남', x: 310, y: 530, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '대구', x: 350, y: 450, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '울산', x: 450, y: 500, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '부산', x: 420, y: 550, values: [0.3, 0.3, 0.2, 0.2] }, + { name: '제주', x: 60, y: 750, values: [0.3, 0.3, 0.2, 0.2] }, + ]; + + const handleMarkerClick = (regionId: string) => { + if (selectedRegion === regionId) { + setSelectedRegion(null); + } else { + setSelectedRegion(regionId); + } + }; + + + const createDonutMarker = (svg: SVGSVGElement, x: number, y: number, name: string, values: number[]) => { + const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FED16A']; + const radius = 26; + const innerRadius = 18; + // const regionId = regionIdMap[name]; + const regionId = regionIdMap[name]; + if (!regionId) { + console.warn(`Invalid region name: ${name}`); + return; + } + + const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + g.setAttribute('transform', `translate(${x}, ${y})`); + g.setAttribute('id', `marker-${name}`); + g.style.cursor = 'pointer'; + g.setAttribute('role', 'button'); + g.setAttribute('tabindex', '0'); + g.setAttribute('aria-label', `${name} 지역 선택`); + + const bg = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + bg.setAttribute('r', radius.toString()); + bg.setAttribute('fill', 'white'); + bg.setAttribute('pointer-events', 'all'); + g.appendChild(bg); + + let startAngle = 0; + values.forEach((value, idx) => { + const endAngle = startAngle + value * 2 * Math.PI; + const x1 = radius * Math.sin(startAngle); + const y1 = -radius * Math.cos(startAngle); + const x2 = radius * Math.sin(endAngle); + const y2 = -radius * Math.cos(endAngle); + const largeArc = endAngle - startAngle > Math.PI ? 1 : 0; + const d = `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArc} 1 ${x2} ${y2} L 0 0 Z`; + + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path.setAttribute('d', d); + path.setAttribute('fill', colors[idx % colors.length] as string); + g.appendChild(path); + + startAngle = endAngle; + }); + + const center = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + center.setAttribute('r', innerRadius.toString()); + center.setAttribute('fill', 'white'); + g.appendChild(center); + + const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + text.setAttribute('x', '0'); + text.setAttribute('y', '2'); + text.setAttribute('text-anchor', 'middle'); + text.setAttribute('font-size', '13'); + text.setAttribute('font-weight', 'bold'); + text.setAttribute('dominant-baseline', 'middle'); + text.setAttribute('fill', 'black'); + text.textContent = name || ''; + g.appendChild(text); + + g.addEventListener('mouseenter', (e) => { + setHoveredRegion(regionId); + setTooltipPosition({ x: e.clientX, y: e.clientY }); + }); + + g.addEventListener('mouseleave', () => { + setHoveredRegion(null); + setTooltipPosition(null); + }); + + g.addEventListener('click', () => { + handleMarkerClick(regionId); + }); + + g.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + + const matrix = g.getScreenCTM(); + if (!matrix) return; // 또는 console.warn('matrix is null'); return; + + const point = svg.createSVGPoint(); + point.x = 0; + point.y = 0; + const transformed = point.matrixTransform(matrix); + + setTooltipPosition({ x: transformed.x, y: transformed.y }); + setHoveredRegion(regionId); + handleMarkerClick(regionId); + } + }); + + svg.appendChild(g); + }; + + useEffect(() => { + const loadSVG = async () => { + try { + const response = await fetch('/img/wrStatus/southKoreaMap.svg'); + const svgText = await response.text(); + + if (svgContainerRef.current) { + svgContainerRef.current.innerHTML = svgText; + + const svg = svgContainerRef.current.querySelector('svg') as SVGSVGElement; + if (svg) { + svg.style.width = '58%'; + svg.style.height = 'auto'; + svg.style.objectFit = 'contain'; + + // Set default fills + const paths = svg.querySelectorAll('path'); + paths.forEach(path => { + if (!path.getAttribute('fill') || path.getAttribute('fill') === 'none') { + path.setAttribute('fill', '#ffffff'); + } + }); + + // Create markers + regionData.forEach(region => { + createDonutMarker(svg, region.x, region.y, region.name, region.values); + }); + } + } + } catch (error) { + console.error('SVG 로딩 실패:', error); + if (svgContainerRef.current) { + svgContainerRef.current.innerHTML = "<p className='text-red-500'>SVG 로딩 실패</p>"; + } + } + }; + + loadSVG(); + }, []); + + useEffect(() => { + const svg = svgContainerRef.current?.querySelector('svg'); + if (svg) { + const paths = svg.querySelectorAll('path'); + paths.forEach(path => { + const pathId = path.getAttribute('id'); + if (pathId) { + if (selectedRegion === pathId) { + path.setAttribute('fill', '#2563eb'); + } else { + const original = '#ffffff'; + path.setAttribute('fill', original); + } + } + }); + } + }, [selectedRegion]); + + return ( + <div className="flex h-screen bg-[#F2F2F2] text-gray-800"> + {/* 우측 콘텐츠 영역 */} + <main className="flex-1 p-6 overflow-auto"> + {/* 상단 제목 */} + <div className="mb-6 flex justify-between items-center"> + <h2 className="text-2xl font-bold">물재이용 현황</h2> + <div className="text-sm text-gray-500">관리자 ▼</div> + </div> + + {/* 콘텐츠 본문 */} + <div className="grid grid-cols-1 xl:grid-cols-2 gap-4"> + {/* 지도 카드 */} + <div className="bg-white rounded-xl shadow border border-[#DBE2E6] overflow-hidden max-h-[900px]"> + {/* 타이틀 영역 */} + <div className="border-b border-[#DBE2E6] px-4 py-3"> + <h3 className="text-[12px] font-semibold">재이용시설현황(2023)</h3> + <p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설현황(개소)</p> + </div> + + {/* 본문 콘텐츠 */} + <div className="p-4"> + <div className="bg-[#E2EDF3] p-4 rounded shadow relative"> + <div className="flex justify-center"> + <div + ref={svgContainerRef} + className="w-full max-w-5xl max-h-[680px] overflow-y-auto block mx-auto" + aria-label="대한민국 지역별 재이용시설 분포도" + role="region" + tabIndex={0} + /> + </div> + + {typeof window !== 'undefined' && hoveredRegion && tooltipPosition && ( + <div + className="fixed bg-white text-[12px] text-gray-700 border border-gray-300 shadow-lg rounded px-3 py-2 z-50" + style={{ + top: tooltipPosition.y + 10, + left: tooltipPosition.x + 10, + pointerEvents: 'none', + }} + > + <div className="font-semibold mb-1">{hoveredRegion}</div> + <div className="space-y-1"> + {colorLegend.map(({ label, color, key }) => ( + <div key={key} className="flex items-center space-x-2"> + <span className="text-[12px] inline-block w-3 h-3 rounded-sm" style={{ backgroundColor: color }}></span> + <span className="text-[12px]"> + {label}:{' '} + {tableData.find(d => d.region === hoveredRegion)?.[key as keyof TableData] ?? '-'} + </span> + </div> + ))} + </div> + </div> + )} + + {/* 범례 */} + <div className="absolute bottom-2 right-2 bg-white rounded-lg shadow-md p-4 text-sm space-y-2"> + <div className="flex items-center space-x-2"> + <span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#FF6B6B'}}></span> + <span className="text-[12px] text-gray-900">하수재이용시설 (총 717개소)</span> + </div> + <div className="flex items-center space-x-2"> + <span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#4ECDC4'}}></span> + <span className="text-[12px] text-gray-900">빗물이용시설 (총 3,730개소)</span> + </div> + <div className="flex items-center space-x-2"> + <span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#45B7D1'}}></span> + <span className="text-[12px] text-gray-900">중수도시설 (총 1,074개소)</span> + </div> + <div className="flex items-center space-x-2"> + <span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#FED16A'}}></span> + <span className="text-[12px] text-gray-900">온배수시설 (총 201개소)</span> + </div> + </div> + </div> + </div> + </div> + + {/* 우측 콘텐츠 (표 + 차트) */} + <div className="col-span-1 flex flex-col space-y-4"> + {/* 재이용시설현황 표 */} + <div className="bg-white rounded-xl shadow border border-[#DBE2E6] overflow-hidden"> + {/* 타이틀 + 버튼 영역 */} + <div className="flex justify-between items-center border-b border-[#DBE2E6] px-4 py-3"> + <div> + <h3 className="text-[12px] font-semibold">재이용시설현황(2023)</h3> + <p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설현황(개소)</p> + </div> + </div> + <div className="p-4"> + <div className="rounded border border-[#DBE2E6] shadow overflow-auto max-h-[292px] p-4"> + <table className="min-w-full text-sm text-center"> + <caption className="sr-only text-[12px]">행정구역별 하수, 빗물, 중수도, 온배수 재이용시설 수</caption> + <thead className="bg-gray-50 text-xs text-gray-600 uppercase border-b"> + <tr> + <th className="text-[12px] font-semibold px-4 py-2">번호</th> + <th className="text-[12px] font-semibold px-4 py-2">시도</th> + <th className="text-[12px] font-semibold px-4 py-2">하수(이용량,천m³/년)</th> + <th className="text-[12px] font-semibold px-4 py-2">빗물(이용량,천m³/년)</th> + <th className="text-[12px] font-semibold px-4 py-2">중수도(이용량,천m³/년)</th> + <th className="text-[12px] font-semibold px-4 py-2">온배수(이용량,천m³/년)</th> + </tr> + </thead> + <tbody className="divide-y"> + {tableData.map((row, index) => ( + <tr key={index} className="hover:bg-gray-50"> + <td className="text-[12px] px-4 py-2 text-center"> + {index+1} + </td> + <td className="text-[12px]">{row.region}</td> + <td className="text-[12px]">{row.sewage}</td> + <td className="text-[12px]">{row.rainwater}</td> + <td className="text-[12px]">{row.reclaimed}</td> + <td className="text-[12px]">{row.thermal}</td> + </tr> + ))} + </tbody> + </table> + </div> + </div> + </div> + + {/* 재이용통계 차트 */} + <div className="bg-white rounded-xl shadow border border-[#DBE2E6] overflow-hidden"> + <div className="border-b border-[#DBE2E6] px-4 py-3"> + <h3 className="text-[12px] font-semibold">재이용통계(2023)</h3> + <p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설 현황 개소수 그래프</p> + </div> + <div className="p-4 bg-[#f9fbfd]"> + <div className="h-[305px] text-gray-500"> + <MainChart + chartForm={chartOption.chartForm} + chartConfig={chartOption.chartConfig} + chartData={chartOption.chartData} + chartTitle={chartOption.chartTitle} + chartClassName={chartOption.chartClassName} + graphSize={chartOption.graphSize} + yFieldText={chartOption.yFieldText} + showLegend={chartOption.showLegend} + /> + </div> + </div> + </div> + </div> + </div> + </main> + </div> + ); +}; + +export default WrStatusPage; \ No newline at end of file diff --git a/apps/dm/app/(popup-layout)/layout.tsx b/apps/dm/app/(popup-layout)/layout.tsx new file mode 100644 index 000000000..742519a66 --- /dev/null +++ b/apps/dm/app/(popup-layout)/layout.tsx @@ -0,0 +1,11 @@ +"use client"; +import "../globals.css"; +import { StoreProvider } from "../store"; + +export default function PopupLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return <StoreProvider>{children}</StoreProvider>; +} diff --git a/apps/dm/app/(popup-layout)/map/popup/page.tsx b/apps/dm/app/(popup-layout)/map/popup/page.tsx new file mode 100644 index 000000000..908f2f09c --- /dev/null +++ b/apps/dm/app/(popup-layout)/map/popup/page.tsx @@ -0,0 +1,170 @@ +"use client"; + +import Gis from "@common/business_components/ui/map/Gis"; +import { useEffect, useRef, useState, useCallback } from "react"; +import { mapService } from "../service"; +import GisApp from "@common/business_components/ui/map/lib/gis.core"; +import { SearchBar } from "@/components/ui/map/searchBar/searchBar"; +import { useDmStore } from "@/app/store"; + +// export type SubsInfo={ +// ruseUtztnFcltNm: string; // 재이용시설명 +// ruseUtztnFcltCd: string; // 재이용시설 코드 +// } +const Map = () => { + + //수요처 저장하는 함수 setSubsInfo + //setSubsInfo(subsInfo:SubsInfo[]):void + const { map,setSubsInfo } = useDmStore( + (state) => state + ); + //저장된 수요처 리스트 + const { subsInfo } = map; + + const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 + const gisMaps = useRef<any | null>(null); + const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 + const [isSearchModalOpen, setIsSearchModalOpen] = useState(true); // SearchModal 열림 상태 + const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 + const [checkedLayers, setCheckedLayers] = useState<string[]>([]); + const [checkedLayersData, setCheckedLayersData] = useState<any[]>([]); + const [selectDataPk, setSelectDataPk] = useState<string>(""); + const [subs, setSubs] = useState<object[]>([]); + + //적용 버튼 클릭 이벤트 + const onApply = () => { + //setSubs([{'41110PB001R1':'수원제1,2 재이용1'},{'41110PB001R2':'수원제1,2 재이용2'}]); + console.log(subs) + }; + + const fetchData = async () => { + try { + const response = await mapService.getLyrStngs(); + setTreeData(response); // API 응답 저장 + } catch (error) { + console.error("데이터 불러오기 실패:", error); + } + }; + + // 팝업 닫기 핸들러를 useCallback으로 메모이제이션 + const handlePopupClose = useCallback(() => { + setIsOpen(false); + setFeatureData(null); + }, []); + + // 컴포넌트 마운트 시 1회 실행 + useEffect(() => { + fetchData(); + }, []); + + useEffect(() => { + console.log("Selected Data Pk: ", selectDataPk); + }, [selectDataPk]); + + // 체크된 레이어 정보를 받는 핸들러 + const handleCheckedLayersChange = ( + checkedIds: string[], + checkedNodesData: any[] + ) => { + setCheckedLayers(checkedIds); + setCheckedLayersData(checkedNodesData); + }; + + // SearchModal, GisContent 등에서 호출 + const onFeatureSelect = (data: any) => { + const map = gisMaps.current; + if (!map) { + console.warn("지도가 초기화되지 않았습니다."); + return; + } + + // 항상 같은 select 객체 사용 + const select = GisApp.Module.select as any; + if (!select) return; + + // 1. 기존 선택 초기화 + select.resetSelectedFeature(); + + // 2. data가 OpenLayers Feature 인스턴스인지 확인 + let foundFeature: any = null; + if ( + data && + typeof data.getGeometry === "function" && + typeof data.setStyle === "function" + ) { + // 이미 Feature 인스턴스라면 그대로 사용 + foundFeature = data; + } else { + // 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용 + const featureId = data.fid || data.ftrIdn || data.id; + map + .getLayers() + .getArray() + .forEach((layer: any) => { + const source = layer.getSource?.(); + if (source?.getFeatureById) { + const feature = source.getFeatureById(String(featureId).trim()); + if (feature) foundFeature = feature; + } + }); + } + + // 3. 피처 선택 + if (foundFeature) { + const geometry = foundFeature.getGeometry(); + if (geometry) { + const extent = geometry.getExtent(); + + map.getView().fit(extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 16, + constrainResolution: true, + nearest: true, + }); + } + select.selectFeature(foundFeature, true); + } else { + console.warn("OpenLayers Feature를 찾을 수 없습니다:", data); + } + + setFeatureData(data); + setIsOpen(true); + setIsSearchModalOpen(false); + }; + + return ( + <> + <SearchBar + setHasuPk={(hasuPk: string) => { + if (hasuPk !== "") { + setSelectDataPk(hasuPk); + } + }} + onApply={onApply} + /> + <div className="bg-container"> + <Gis + treeData={treeData} + isSearch={false} + gisMaps={gisMaps} + isMiniMap={false} + searchView={undefined} + detailView={null} + onFeatureSelect={onFeatureSelect} + setIsOpen={setIsOpen} + setFeatureData={setFeatureData} + featureData={featureData} + setIsSearchModalOpen={setIsSearchModalOpen} + isOpen={isOpen} + isSearchModalOpen={isSearchModalOpen} + onCheckedLayersChange={handleCheckedLayersChange} + imgVisble={false} + setData={setSubs} + /> + </div> + </> + ); +}; + +export default Map; diff --git a/apps/dm/app/(popup-layout)/map/popup/sdmInfoMap.tsx b/apps/dm/app/(popup-layout)/map/popup/sdmInfoMap.tsx new file mode 100644 index 000000000..84dc49119 --- /dev/null +++ b/apps/dm/app/(popup-layout)/map/popup/sdmInfoMap.tsx @@ -0,0 +1,147 @@ +"use client"; + +import Gis from "@common/business_components/ui/map/Gis"; +import { useEffect, useRef, useState, useCallback } from "react"; +import { mapService } from "../service"; +import GisApp from "@common/business_components/ui/map/lib/gis.core"; +import { SearchBar } from "@/components/ui/map/searchBar/searchBar"; + +const Map = () => { + const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 + const gisMaps = useRef<any | null>(null); + const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 + const [isSearchModalOpen, setIsSearchModalOpen] = useState(true); // SearchModal 열림 상태 + const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 + const [checkedLayers, setCheckedLayers] = useState<string[]>([]); + const [checkedLayersData, setCheckedLayersData] = useState<any[]>([]); + const [selectDataPk, setSelectDataPk] = useState<string>(""); + + const fetchData = async () => { + try { + const response = await mapService.getLyrStngs(); + setTreeData(response); // API 응답 저장 + } catch (error) { + console.error("데이터 불러오기 실패:", error); + } + }; + + // 팝업 닫기 핸들러를 useCallback으로 메모이제이션 + const handlePopupClose = useCallback(() => { + setIsOpen(false); + setFeatureData(null); + }, []); + + // 컴포넌트 마운트 시 1회 실행 + useEffect(() => { + fetchData(); + }, []); + + useEffect(() => { + console.log("Selected Data Pk: ", selectDataPk); + }, [selectDataPk]); + + // 체크된 레이어 정보를 받는 핸들러 + const handleCheckedLayersChange = ( + checkedIds: string[], + checkedNodesData: any[] + ) => { + setCheckedLayers(checkedIds); + setCheckedLayersData(checkedNodesData); + }; + + // SearchModal, GisContent 등에서 호출 + const onFeatureSelect = (data: any) => { + const map = gisMaps.current; + if (!map) { + console.warn("지도가 초기화되지 않았습니다."); + return; + } + + // 항상 같은 select 객체 사용 + const select = GisApp.Module.select as any; + if (!select) return; + + // 1. 기존 선택 초기화 + select.resetSelectedFeature(); + + // 2. data가 OpenLayers Feature 인스턴스인지 확인 + let foundFeature: any = null; + if ( + data && + typeof data.getGeometry === "function" && + typeof data.setStyle === "function" + ) { + // 이미 Feature 인스턴스라면 그대로 사용 + foundFeature = data; + } else { + // 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용 + const featureId = data.fid || data.ftrIdn || data.id; + map + .getLayers() + .getArray() + .forEach((layer: any) => { + const source = layer.getSource?.(); + if (source?.getFeatureById) { + const feature = source.getFeatureById(String(featureId).trim()); + if (feature) foundFeature = feature; + } + }); + } + + // 3. 피처 선택 + if (foundFeature) { + const geometry = foundFeature.getGeometry(); + if (geometry) { + const extent = geometry.getExtent(); + + map.getView().fit(extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 16, + constrainResolution: true, + nearest: true, + }); + } + select.selectFeature(foundFeature, true); + } else { + console.warn("OpenLayers Feature를 찾을 수 없습니다:", data); + } + + setFeatureData(data); + setIsOpen(true); + setIsSearchModalOpen(false); + }; + + return ( + <> + <SearchBar + setHasuPk={(hasuPk: string) => { + if (hasuPk !== "") { + setSelectDataPk(hasuPk); + } + }} + /> + <div className="bg-container"> + <Gis + treeData={treeData} + isSearch={false} + gisMaps={gisMaps} + isMiniMap={false} + searchView={undefined} + detailView={null} + onFeatureSelect={onFeatureSelect} + setIsOpen={setIsOpen} + setFeatureData={setFeatureData} + featureData={featureData} + setIsSearchModalOpen={setIsSearchModalOpen} + isOpen={isOpen} + isSearchModalOpen={isSearchModalOpen} + onCheckedLayersChange={handleCheckedLayersChange} + imgVisble={false} + /> + </div> + </> + ); +}; + +export default Map; diff --git a/apps/dm/app/(popup-layout)/map/popup/windowPotal.tsx b/apps/dm/app/(popup-layout)/map/popup/windowPotal.tsx new file mode 100644 index 000000000..38b8fc7db --- /dev/null +++ b/apps/dm/app/(popup-layout)/map/popup/windowPotal.tsx @@ -0,0 +1,94 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { createPortal } from 'react-dom'; + +// 현재 문서의 스타일(CSS)을 새 창으로 복사하는 함수 +const copyStyles = (sourceDoc: Document, targetDoc: Document) => { + Array.from(sourceDoc.styleSheets).forEach(styleSheet => { + try { + if (styleSheet.cssRules) { // <style> 태그로 주입된 CSS + const newStyleEl = sourceDoc.createElement('style'); + Array.from(styleSheet.cssRules).forEach(cssRule => { + newStyleEl.appendChild(sourceDoc.createTextNode(cssRule.cssText)); + }); + targetDoc.head.appendChild(newStyleEl); + } else if (styleSheet.href) { // <link> 태그로 연결된 CSS 파일 + const newLinkEl = sourceDoc.createElement('link'); + newLinkEl.rel = 'stylesheet'; + newLinkEl.href = styleSheet.href; + targetDoc.head.appendChild(newLinkEl); + } + } catch (e) { + console.warn('스타일시트를 복사할 수 없습니다:', e); + } + }); +}; + +interface NewWindowPortalProps { + children: React.ReactNode; + onClose: () => void; + title?: string; + width?: number; + height?: number; + resizable?: string; + scrollbars?: string; +} + +const NewWindowPortal: React.FC<NewWindowPortalProps> = ({ children, onClose, title = "" , width = 900, height = 800, resizable = "yes", scrollbars= "yes" }) => { + const [container, setContainer] = useState<HTMLElement | null>(null); + const newWindowRef = useRef<Window | null>(null); + const onCloseRef = useRef(onClose); + + // onClose 함수를 ref로 저장하여 의존성 문제 해결 + useEffect(() => { + onCloseRef.current = onClose; + }, [onClose]); + + const handleUnload = useCallback(() => { + onCloseRef.current(); + }, []); + + useEffect(() => { + // 이미 창이 열려있으면 새로 열지 않음 + if (newWindowRef.current && !newWindowRef.current.closed) return; + + // 1. 새 창을 열고 참조를 저장합니다. + const popupOptions = `width=${width},height=${height},resizable=${resizable},scrollbars=${scrollbars}`; + newWindowRef.current = window.open('', title, popupOptions); + + if (newWindowRef.current) { + newWindowRef.current.document.title = title; + + // 2. React 컴포넌트를 마운트할 컨테이너 div를 생성하여 새 창의 body에 추가합니다. + const div = newWindowRef.current.document.createElement('div'); + newWindowRef.current.document.body.appendChild(div); + setContainer(div); + + // 3. 메인 창의 스타일을 새 창으로 복사합니다. (매우 중요) + copyStyles(document, newWindowRef.current.document); + + // 4. 사용자가 창을 닫을 때 부모에게 알리기 위한 이벤트 리스너를 설정합니다. + // const handleUnload = () => { + // onClose(); + // }; + newWindowRef.current.addEventListener('unload', handleUnload); + + // 5. 컴포넌트가 사라질 때 창을 닫는 클린업 함수를 반환합니다. + return () => { + if (newWindowRef.current) { + newWindowRef.current.removeEventListener('unload', handleUnload); + if (!newWindowRef.current.closed) { + newWindowRef.current.close(); + } + } + }; + } else { + alert('팝업이 차단되었습니다. 팝업 차단을 해제하고 다시 시도해주세요.'); + onCloseRef.current(); + } + }, []); + + // `createPortal`을 사용해 children을 새 창의 container div에 렌더링합니다. + return container ? createPortal(children, container) : null; +}; + +export default NewWindowPortal; \ No newline at end of file diff --git a/apps/sn/app/service/index.ts b/apps/dm/app/(popup-layout)/map/service/index.ts similarity index 100% rename from apps/sn/app/service/index.ts rename to apps/dm/app/(popup-layout)/map/service/index.ts diff --git a/apps/sn/app/service/map.ts b/apps/dm/app/(popup-layout)/map/service/map.ts similarity index 57% rename from apps/sn/app/service/map.ts rename to apps/dm/app/(popup-layout)/map/service/map.ts index 2182e88c8..ecc35d074 100644 --- a/apps/sn/app/service/map.ts +++ b/apps/dm/app/(popup-layout)/map/service/map.ts @@ -1,10 +1,8 @@ -import axios from 'axios' - /** * 관망도 API */ -const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}/v1/map` + const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}/v1/map` export type LyrStngProps = { lyr_sn: string @@ -31,7 +29,18 @@ export type LyrStngProps = { export const mapService = { //레이어 설정 조회 - getLyrStngs: () => { - return axios.get(`${API_URL}/lyrStngs/SN_NETWORK`) + getLyrStngs: async () => { + const response = await fetch(`${API_URL}/lyrStngs/DM_NETWORK`, { + method: 'GET', + headers: { + 'Accept': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return response.json(); } } diff --git a/apps/dm/app/base.tsx b/apps/dm/app/base.tsx new file mode 100644 index 000000000..bae02acb2 --- /dev/null +++ b/apps/dm/app/base.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { BaseLayout } from "@common/components"; +import type { MenuItem } from "./store/common"; +import { useDmStore } from "./store"; +import { usePathname, useRouter } from "next/navigation"; + +export default function Base({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + const { getMenuAuth } = useDmStore((state) => state); + + const router = useRouter(); + const pathname = usePathname(); + + const handleClick = (id: MenuItem) => { + getMenuAuth(id.menuCd); + router.push(id.urlPath); + }; + + return ( + <BaseLayout currentPath={pathname ?? ""} handleMenuClick={handleClick}> + {children} + </BaseLayout> + ); +} diff --git a/apps/dm/app/common/transValueFormat.tsx b/apps/dm/app/common/transValueFormat.tsx new file mode 100644 index 000000000..4d76d84e0 --- /dev/null +++ b/apps/dm/app/common/transValueFormat.tsx @@ -0,0 +1,26 @@ +import React from 'react' + +type CommonProps = { + value: string | number | ''; + format: string; +} + +export default function TransValueFormat({value='', format=''}: CommonProps) { + return ( + format === 'yyyyMMdd' ? + <>{value.toString().substring(0,4)+'-'+value.toString().substring(4,6)+'-'+value.toString().substring(6,8)}</> + : format === 'number' ? + <>{String(value ?? '').includes('.') + ? String(value).split('.').map((part, index) => + index === 0 ? part.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : part + ).join('.') + : String(value ?? '').replace(/\B(?=(\d{3})+(?!\d))/g, ",")}</> + : format === 'float1' ? + <>{String(value ?? '').includes('.') + ? String(value).split('.').map((part, index) => + index === 0 ? part.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : part + ).join('.') + : String(value ?? '').replace(/\B(?=(\d{3})+(?!\d))/g, ",")}</> + : <>{value}</> + ) +} \ No newline at end of file diff --git a/apps/dm/app/favicon.ico b/apps/dm/app/favicon.ico new file mode 100644 index 000000000..718d6fea4 Binary files /dev/null and b/apps/dm/app/favicon.ico differ diff --git a/apps/dm/app/globals.css b/apps/dm/app/globals.css new file mode 100644 index 000000000..f30aa3031 --- /dev/null +++ b/apps/dm/app/globals.css @@ -0,0 +1,70 @@ +@import "../../../packages/common/components/src/base.css"; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem + } + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55% + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/apps/dm/app/layout.tsx b/apps/dm/app/layout.tsx new file mode 100644 index 000000000..af4dcaf1d --- /dev/null +++ b/apps/dm/app/layout.tsx @@ -0,0 +1,17 @@ +"use client" +import "./globals.css"; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + <html lang="ko"> + <body> + {children} + </body> + </html> + ); +} + diff --git a/apps/dm/app/middleware.ts b/apps/dm/app/middleware.ts new file mode 100644 index 000000000..6ddf29f9f --- /dev/null +++ b/apps/dm/app/middleware.ts @@ -0,0 +1,130 @@ +import { NextRequest, NextResponse } from "next/server"; +import path from "path"; + +const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; +const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; +const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL || ""; +const SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ""; +const GEOSERVER_URL = process.env.NEXT_PUBLIC_GEOSERVER_URL || ""; +const LX_GEOSERVER_URL = process.env.NEXT_PUBLIC_LX_GEOSERVER_URL || ""; +const DC_URL = process.env.NEXT_PUBLIC_DC_URL || ""; + +export function middleware(req: NextRequest) { + console.log("Middleware 진입:", req.nextUrl.pathname); + const pathname = req.nextUrl.pathname; + const searchParams = req.nextUrl.searchParams; + + if (pathname.startsWith("/server")) { + + const decodedParams = Array.from(searchParams.entries()) + .map(([key, value]) => `${key}=${decodeURIComponent(value)}`) + .join("&"); + + const newUrl = `${SERVER_API_URL}${pathname.replace("/server", "")}${ + decodedParams ? `?${decodedParams}` : "" + }`; + return NextResponse.rewrite(new URL(newUrl)); + } + + if (pathname.startsWith("/geoserver")) { + let queryParams = searchParams.toString(); + const typename = searchParams.get("TYPENAME"); + + let newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; + + if ( + typename == "hasudo:tb_il_swage_prcs_fclt" //하수처리시설 + || typename == "hasudo:tb_ms_swage_prcs_zone" //하수처리구역 + || typename == "hasudo:tb_ms_sbmrg_dngr_map" //침수위험지도 + || typename == "hasudo:tb_ms_flood_dngr_map" //도시침수지도 + ) { + newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; + } + else if ( + typename == "hasudo:tb_ms_swage_ppl" //관로 + || typename == "hasudo:tb_ms_swage_mhl" //맨홀 + || typename == "hasudo:tb_ms_swage_pmstn" //펌프장 + || typename == "hasudo:tb_ms_stofcbr" //우수토실 + ) { + newUrl = `${LX_GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; + } + else if ( + typename == "hasudo:om_dt_untrt_swg_rain" //중점우수토실 + || typename == "hasudo:om_wqlt_station" //관측소 + || typename == "hasudo:tb_wr_bdst_ruse_fclt" //재이용시설 + || typename == "hasudo:tb_wr_rnwtr_utztn_fclt" //빗물이용시설 + || typename == "hasudo:tb_wr_rswtr_fclt" //중수도시설 + || typename == "hasudo:tb_wr_htdfl_fclt" //온배수시설 + || typename == "hasudo:tb_wr_ruse_fclt_dmd_co" //수요처 + || typename == "hasudo:tb_wr_tmpr_dmd_co" //잠재수요처 + ){ + const url = req.nextUrl.clone(); + url.searchParams.set('TYPENAME', typename.replace("hasudo:","")); + queryParams = url.searchParams.toString(); + newUrl = `${SEWERAGE_API_URL}/v1/map/geometry${queryParams ? `?${queryParams}` : ""}`; + } + + console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`); + return NextResponse.rewrite(new URL(newUrl)); + } + + if ( + pathname.startsWith("/attach-service") || + pathname.startsWith("/cms-service") + ) { + const requestHeaders = new Headers(req.headers); + const token = req.cookies.get(CLAIM_NAME)?.value; + const userId = req.cookies.get(AUTH_USER_ID)?.value; + const newUrl = `${SERVER_API_URL}${pathname}`; + + if (token) requestHeaders.set(CLAIM_NAME, token); + if (userId) requestHeaders.set(AUTH_USER_ID, userId); + + console.log( + `[Middleware] Rewriting from '/attach-service' path to: ${newUrl}`, + ); + return NextResponse.rewrite(new URL(newUrl), { + headers: requestHeaders, + }); + } + + if (pathname.startsWith("/user-service")) { + const requestHeaders = new Headers(req.headers); + const newUrl = `${SERVER_API_URL}${pathname}`; + + console.log( + `[Middleware] Rewriting from '/user-service' path to: ${newUrl}`, + ); + return NextResponse.rewrite(new URL(newUrl), { + headers: requestHeaders, + }); + } + + if (pathname.startsWith("/dc-service")) { + const requestHeaders = new Headers(req.headers); + const decodedParams = Array.from(searchParams.entries()) + .map(([key, value]) => `${key}=${decodeURIComponent(value)}`) + .join("&"); + + const newUrl = `${DC_URL}${pathname}${decodedParams ? `?${decodedParams}` : ""}`; + console.log( + `[Middleware] Rewriting from '/dc-service' path to: ${newUrl}`, + ); + return NextResponse.rewrite(new URL(newUrl), { + headers: requestHeaders, + }); + } + return NextResponse.next(); +} + +export const config = { + matcher: [ + "/server/:path*", + "/geoserver/:path*", + "/attach-service/:path*", + "/user-service/:path*", + "/cms-service/:path*", + "/dc-service/:path*", + ], + +}; diff --git a/apps/dm/app/page.tsx b/apps/dm/app/page.tsx new file mode 100644 index 000000000..fdb323086 --- /dev/null +++ b/apps/dm/app/page.tsx @@ -0,0 +1,36 @@ +"use client" + +import Link from 'next/link'; +import { Button } from '@common/components/ui'; + +export default function Home() { + return ( + <> + <div className='flex'> + <Button className='ml-5 place-self-auto'> + <Link href="/pages/wrAbout">물재이용이란</Link> + </Button> + <Button className='ml-5 place-self-auto'> + <Link href="/pages/wrExample">물재이용 사례</Link> + </Button> + <Button className='ml-5 place-self-auto'> + <Link href="/pages/wrStatus">물재이용 현황</Link> + </Button> + <Button className='ml-5 place-self-auto'> + <Link href="/pages/wrOverview">물재이용 사업추진절차</Link> + </Button> + <Button className='ml-5 place-self-auto'> + <Link href="/pages/sdmMng/sdmInsert">수요처 등록</Link> + </Button> + <Button className='ml-5 place-self-auto'> + <Link href="/pages/sdmMng/sdmInfoInsert">수요처 정보 등록</Link> + </Button> + </div> + <div className='flex'> + <Button className='ml-5 place-self-auto'> + <Link href="/pages/sdmMng/sdmApplyList">수요처 신청 현황(개발중)</Link> + </Button> + </div> + </> + ); +} diff --git a/apps/dm/app/store/common/index.tsx b/apps/dm/app/store/common/index.tsx new file mode 100644 index 000000000..cf4ce14d8 --- /dev/null +++ b/apps/dm/app/store/common/index.tsx @@ -0,0 +1,419 @@ +/**==================================== + * 보편적으로 쓰는 store 공간 + ====================================**/ + +import { fetchType, getFetch } from '@/components/ui/api'; + +/** + * STORE - TYPE + */ + +// selectBox +export type SelectDataType = { + text: string; + val: string; +} + +export interface MenuItem { + menuCd: number; + menuKornNm: string; + menuEngNm: string; + parentId: number | null; + sortSeq: number; + iconNm?: string; + children: MenuItem[] | null; + levelNo: number; + urlPath: string; + menuLnkgTypeCd: string; + menuLnkgSn: number; + menuPath: string; + qrcd: string; + useYn: string; + otptYn: string; + newWndwYn: string; + requiredUrlPath: boolean; +} + +// grid header +export type TableUpperProps = { + id: string; + title: string; + upName?: string; + upSequnce?: number; + align? : string; + row? : number; + rowSpan?: number; + colSpan?: number; +} + +// 시도 데이터 +export type sidoDataType = { + adstDstrCd: number, + sido: string, +} + +// 시군구 데이터 +export type sigunDataType = { + adstDstrCd: number, + gugun: string +} + +// 권역 데이터 +export type regionDataType = { + rvbsnCd: string, + rvbsn: string, +} + +// 단위유역 데이터 +export type unitWaterShedDataType = { + rvbsnCd: string, + lgbsn: string, +} + +// 중권역 데이터 +export type middleRegionDataType = { + rvbsnCd: string, + mdmbsn: string, +} + +// 환경청 데이터 +export type envPartDataType = { + lclgvrnmntCd: string, + lclgvrnmntNm: string, + adstDliStrCd: string, + adstDstrCd: string, +} + +export type envPartSelectDataType = SelectDataType & { + adstDliStrCd: string, +} + +/** + * STORE - DATA + */ + +// 시도 데이터 +let selectSidoData: SelectDataType[] = [ + { text: "전체", val: '0' } +]; + +// 시군구 데이터 +let selectSigunData: SelectDataType[] = [ + { text: "전체", val: '0' } +]; + +// 권역 데이터 +const selectRegionData: SelectDataType[] = [ + { text: "전체", val: '0' } +]; + +// 단위유역 데이터 +const selectUnitWaterShedData: SelectDataType[] = [ +]; + +// 환경청 데이터 +let selectEnvPartData: envPartSelectDataType[] = [ + { text: "전체", val: '0', adstDliStrCd: '' } +]; + +// 환경청 내 시도 데이터 +const selectEnvPartSidoData: SelectDataType[] = [ + { text: "전체", val: '0' } +]; + +// 환경청 내 시군구 데이터 +const selectEnvPartSigunData: SelectDataType[] = [ + { text: "전체", val: '0' } +]; + +// 구분 데이터 +// const selectPartData:SelectDataType[] = [ +// { text: "행정별", val: '0' }, +// { text: "유역별", val: '01' }, +// { text: "환경청별", val: '02' }, +// ]; + +// 상향 하향 데이터 +const selectUpdownData:SelectDataType[] = [ + { text: "전체", val: '0' }, + { text: "500이하", val: '499' }, + { text: "500이상", val: '500' }, +]; + +// 운영 상태 데이터 +const selectOperationData:SelectDataType[] = [ + { text: "전체", val: '0' }, + { text: "가동중", val: '1' }, + { text: "재가동", val: '2' }, + { text: "가동중지", val: '3' }, + { text: "시설폐쇄", val: '4' }, +]; + +// 시설 구분 데이터 +const selectFacilityPartData:SelectDataType[] = [ + { text: "전체", val: '0' }, + { text: "공공하수처리시설", val: 'SW' }, + { text: "오수처리장", val: 'SS' }, + { text: "개인하수처리시설", val: 'PB' }, + { text: "분뇨처리장", val: 'NS' }, + { text: "폐수처리장", val: 'LW' }, + { text: "침출수처리장", val: 'LC' }, + { text: "기타기초시설", val: 'ET' }, +]; + +// 년도 데이터 +const getYearData = () => { + const year = new Date().getFullYear(); + const year1 = year - 1; + const year2 = year - 2; + const year3 = year - 3; + const year4 = year - 4; + const year5 = year - 5; + return [ + { text: year1 + "년", val: year1.toString() }, + { text: year2 + "년", val: year2.toString() }, + { text: year3 + "년", val: year3.toString() }, + { text: year4 + "년", val: year4.toString() }, + { text: year5 + "년", val: year5.toString() }, + ] +} + +const selectSearchYear:SelectDataType[] = getYearData(); + +// 유량 기간 구분 +const selectFlowTerm : SelectDataType[] = [ + { text: "연간", val: '0' }, + { text: "하절기", val: '01' }, + { text: "동절기", val: '02' }, + { text: "청천시", val: '03' }, + { text: "강우시", val: '04' }, +]; + +// 유량 기간 구분 +const selectFlowPanel : SelectDataType[] = [ + { text: "전체", val: '0'}, + { text: "3mm미만", val: '1'}, + { text: "3~10mm미만", val: '2'}, + { text: "10~20mm미만", val: '3'}, + { text: "20~30mm미만", val: '4'}, + { text: "30~40mm미만", val: '5'}, + { text: "40~50mm미만", val: '6'}, + { text: "50~60mm미만", val: '7'}, + { text: "60mm이상", val: '8'}, +]; + +// 중권역 데이터 +const selectMiddleRegionData: SelectDataType[] = [ + { text: "전체", val: '0' } +]; + +/** + * STORE - API + */ + +// 시도 API 호출 함수 추가 +const fetchSidoData = async (): Promise<SelectDataType[]> => { + try { + const res = await getFetch(fetchType.COMMON, '/addr/city'); + const data = res.data; + selectSidoData = [ + { text: "전체", val: '0'}, + ...data.map((item: sidoDataType) => ({ + text: item.sido || '', + val: String(item.adstDstrCd) || '' + })) + ]; + return selectSidoData; + } catch (error) { + console.error('시도 데이터를 가져오는데 실패했습니다:', error); + return selectSidoData; + } +}; + +// 시군구 API 호출 함수 추가 +const fetchSigunData = async (): Promise<SelectDataType[]> => { + try { + const res = await getFetch(fetchType.COMMON, '/addr/sgg'); + const data = res.data; + selectSigunData = [ + { text: "전체", val: '0' }, + ...data + .map((item: sigunDataType) => ({ + text: item.gugun || '', + val: String(item.adstDstrCd) || '' + })) + ]; + return selectSigunData; + } catch (error) { + console.error('시군구 데이터를 가져오는데 실패했습니다:', error); + return selectSigunData; + } +}; + +// 권역 API 호출 함수 추가 +const fetchRegionData = async (): Promise<SelectDataType[]> => { + try { + const res = await getFetch(fetchType.COMMON, '/addr/sarea'); + const data = res.data; + return [ + { text: "전체", val: '0'}, + ...data.map((item: regionDataType) => ({ + text: item.rvbsn || '', + val: String(item.rvbsnCd) || '' + })) + ]; + } catch (error) { + console.error('권역 데이터를 가져오는데 실패했습니다:', error); + return selectRegionData; + } +}; + +// 중권역 API 호출 함수 추가 +const fetchMiddleRegionData = async (): Promise<SelectDataType[]> => { + try { + const res = await getFetch(fetchType.COMMON, '/addr/midArea'); + const data = res.data; + return [ + { text: "전체", val: '0' }, + ...data + .map((item: middleRegionDataType) => ({ + text: item.mdmbsn || '', + val: String(item.rvbsnCd) || '' + })) + ]; + } catch (error) { + console.error('중권역 데이터를 가져오는데 실패했습니다:', error); + return selectMiddleRegionData; + } +}; + +// 단위유역 API 호출 함수 추가 +const fetchUnitWaterShedData = async (): Promise<SelectDataType[]> => { + try { + const res = await getFetch(fetchType.COMMON, '/addr/largeArea'); + const data = res.data; + return [ + { text: "전체", val: '0' }, + ...data + .map((item: unitWaterShedDataType) => ({ + text: item.lgbsn || '', + val: String(item.rvbsnCd) || '' + })) + ]; + } catch (error) { + console.error('단위유역 데이터를 가져오는데 실패했습니다:', error); + return selectUnitWaterShedData; + } +}; + +// 환경청 데이터 +const fetchEnvPartData = async (): Promise<envPartSelectDataType[]> => { + try { + const res = await getFetch(fetchType.COMMON, '/addr/govArea'); + const data = res.data; + selectEnvPartData = [ + { text: "전체", val: '0', adstDliStrCd: '' }, + ...data + .map((item: envPartDataType) => ({ + text: item.lclgvrnmntNm || '', + val: String(item.lclgvrnmntCd) || '', + adstDliStrCd: item.adstDliStrCd || '' + })) + ]; + return selectEnvPartData; + } catch (error) { + console.error('환경청 데이터를 가져오는데 실패했습니다:', error); + return selectEnvPartData; + } +}; + +/**==================================== + * Store - expaort + ====================================*/ +export type CommonAction = { + initializeCommonData: () => Promise<void>; + getMenuAuth: (menuCd:number) => Promise<void>; +} + +export type CommonType = { + Common: { + isInit: boolean; + selectSidoData: SelectDataType[]; + selectSigunData: SelectDataType[]; + selectUpdownData: SelectDataType[]; + selectOperationData: SelectDataType[]; + selectFacilityPartData: SelectDataType[]; + selectSearchYear: SelectDataType[]; + selectUnitWaterShedData: SelectDataType[]; + selectRegionData: SelectDataType[]; + selectMiddleRegionData: SelectDataType[]; + selectEnvPartSidoData: SelectDataType[]; + selectEnvPartSigunData: SelectDataType[]; + selectFlowTerm: SelectDataType[]; + selectFlowPanel: SelectDataType[]; + selectEnvPartData: envPartSelectDataType[]; + btnClass: string; + } +} + +export const CommonState:CommonType = { + Common: { + isInit: false, + selectSidoData: selectSidoData, + selectSigunData: selectSigunData, + selectUpdownData: selectUpdownData, + selectOperationData: selectOperationData, + selectFacilityPartData: selectFacilityPartData, + selectSearchYear: selectSearchYear, + selectRegionData: selectRegionData, + selectUnitWaterShedData: selectUnitWaterShedData, + selectMiddleRegionData: selectMiddleRegionData, + selectEnvPartSidoData : selectEnvPartSidoData, + selectEnvPartSigunData : selectEnvPartSigunData, + selectFlowTerm: selectFlowTerm, + selectFlowPanel: selectFlowPanel, + selectEnvPartData : selectEnvPartData, + btnClass: "p-4 m-2 text-white rounded-sm text-lg font-medium" + } +} + +export const CommonReducer:(set:any)=>CommonAction=(set: any) => { + return { + getMenuAuth : async (menuCd : number) =>{ + const response = await getFetch( + "local", + `/cms-service/api/v1/menu-authrts/pageinfo/${menuCd}` + ); + set((state: CommonType) => { + return { + ...state, + auth: { + mngAuthrtYn: response[0].mngAuthrtYn, + inqAuthrtYn: response[0].mngAuthrtYn, + regAuthrtYn: response[0].mngAuthrtYn, + mdfcnAuthrtYn: response[0].mngAuthrtYn, + delAuthrtYn: response[0].mngAuthrtYn, + }, + }; + }); + }, + // 시도 변경 이벤트 + initializeCommonData: async () => { + const promiseSet = await Promise.all([fetchSidoData(), fetchSigunData(), fetchRegionData(), fetchUnitWaterShedData(), fetchMiddleRegionData(), fetchEnvPartData()]); + const [sidoData, sggData, regionData, unitData, midData, envData] = promiseSet; + set((state: CommonType) => ({ + Common: { + ...state.Common, + selectSidoData: sidoData, + selectSigunData: sggData, + selectRegionData: regionData, + selectMiddleRegionData: midData, + selectUnitWaterShedData: unitData, + selectEnvPartData: envData + } + }) + )}, + } +} + +export type CommonStore = CommonType & CommonAction; diff --git a/apps/dm/app/store/index.tsx b/apps/dm/app/store/index.tsx new file mode 100644 index 000000000..47170b3db --- /dev/null +++ b/apps/dm/app/store/index.tsx @@ -0,0 +1,42 @@ +'use client' + +import { type ReactNode, createContext, useContext, useRef } from 'react' +import { useStore } from 'zustand' +import { type DmStore, 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) { + // Create store instance if not initialized + storeRef.current = StoreZus() as StoreApi + } + return ( + <StoreContext.Provider value={storeRef.current}> + {children} + </StoreContext.Provider> + ) +} + +export const useDmStore = <T,>( + selector: (store: DmStore) => 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/dm/app/store/map/index.tsx b/apps/dm/app/store/map/index.tsx new file mode 100644 index 000000000..d5f2137f7 --- /dev/null +++ b/apps/dm/app/store/map/index.tsx @@ -0,0 +1,108 @@ +import { getFetch } from "@/components/ui/api"; +import { DEV_URL_FO } from "@/components/ui/config"; +import type { FacilityInfo, SubsInfo } from "./type"; + +const checkWrRegList = async () => { + const wrRegList = await getFetch("wr", `/reuseFc/pubFc`); + return wrRegList; +}; + +const getRuseFclt = async ( + page: string, + sggOrCity: string, + searchTxt?: string +) => { + const checkWrRegListRes = await checkWrRegList(); + + let url = `/prcsFclt/ruseInfo/${page ? page : 1}/5/A/${sggOrCity ? sggOrCity : "0"}/0/1/SW`; + + console.log(" checkWrRegListRes ", checkWrRegListRes); + const ruseList = checkWrRegListRes.data.filter((item: string) => item !== ""); + + if (searchTxt) { + url = url + `/${searchTxt}`; + } + + const response = await fetch(DEV_URL_FO + url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(ruseList), + }); + + const hasuList = await response.json().then((res: any) => { + const data = res.data + .map((item: any) => { + return { + ...item, + fctCpc: Number(item.fctCpc) || 0, + searchNm: item.fctNm, + searchPk: item.bscFctNm, + }; + }) + .filter((hasu) => ruseList.find((ruse) => hasu.bscFctCd === ruse)); + return { data: data, count: res.count }; + }); + + return hasuList; +}; + +export type DmMapActions = { + findRuseFclt(sggOrCity: string, searchTxt: string): void; + setSubsInfo(subsInfo: SubsInfo[]): void; +}; + +export type DmMapType = { + map: { + ruseFcltList: FacilityInfo[]; + subsInfo: SubsInfo[]; + }; +}; + +export const dmMapState: DmMapType = { + map: { + ruseFcltList: [], + // subsInfo: [ + // { + // ruseUtztnFcltNm: "수원제1,2 재이용1", // 재이용시설명 + // ruseUtztnFcltCd: "41110PB001R1", + // }, + // { + // ruseUtztnFcltNm: "낙양 재이용1", // 재이용시설명 + // ruseUtztnFcltCd: "41150SW001R1", + // }, + // { + // ruseUtztnFcltNm: "서남 재이용1", // 재이용시설명 + // ruseUtztnFcltCd: "11500PB001R1", + // }, + // ], + subsInfo:[] + }, +}; +export type DmMapStore = DmMapType & DmMapActions; + +export const DmMapReducer = (set: any): DmMapActions => { + return { + findRuseFclt: async (sggOrCity: string, searchTxt: string) => { + const ruseFcltList = await getRuseFclt("1", sggOrCity, searchTxt); + + set((state: DmMapType) => ({ + ...state, + map: { + ...state.map, + ruseFcltList: ruseFcltList.data, + }, + })); + }, + setSubsInfo: async (subsInfo: SubsInfo[]) => { + set((state: DmMapType) => ({ + ...state, + map: { + ...state.map, + subsInfo: subsInfo, + }, + })); + }, + }; +}; diff --git a/apps/dm/app/store/map/type.tsx b/apps/dm/app/store/map/type.tsx new file mode 100644 index 000000000..adcaf7f37 --- /dev/null +++ b/apps/dm/app/store/map/type.tsx @@ -0,0 +1,39 @@ +export type FacilityInfo= { + bscFctCd: string; // 기본시설 코드 + bscFctNm: string; // 기본시설명 + rvbsnCd: string; // 유역 코드 + rvbsn: string; // 유역명 + lgbsn: string; // 대권역명 + mdmbsn: string; // 중권역명 + rgnCd: string; // 관할청 코드 + rgnNm: string; // 관할청 명칭 + adstDstrCd: string; // 행정구역 코드 + sido: string; // 시도 + gugun: string; // 구군 + addr: string; // 주소 + fctCpc: string; // 시설용량 + efflWqltStndrdCd: string; // 방류수 수질기준 코드 + efflWqltStndrdNm: string; // 방류수 수질기준명 + trtFrfNm: string; // 처리공법명 + trtDstr: string; // 처리구역 + rnDt: string; // 사용개시일 (예: YYYYMMDD) + cmplDt: string; // 준공일자 (예: YYYYMMDD) + trstOprEtpNm: string; // 위탁운영기관명 + bscFctClsNm: string; // 기본시설 구분명 + sitArea: string; // 부지면적 + trtDstrArea: string; // 처리구역 면적 + swrTrtPopl: string; // 하수처리 인구 + mnstr: string; // 총괄 유역 + efflWtrrgn: string; // 방류수역 + rvNm: string; // 하천명 + bscFctClsCd: string; // 기본시설 구분 코드 + rvbsnNm: string; // 유역명 + tel: string; // 전화번호 + rswtrYn: string; // 재이용수 여부 ("Y" 또는 "N") + ruse: boolean; // 재이용 여부 (boolean) + } + + export type SubsInfo={ + ruseUtztnFcltNm: string; // 재이용시설명 + ruseUtztnFcltCd: string; // 재이용시설 코드 + } \ No newline at end of file diff --git a/apps/dm/app/store/readme.md b/apps/dm/app/store/readme.md new file mode 100644 index 000000000..0710aa69b --- /dev/null +++ b/apps/dm/app/store/readme.md @@ -0,0 +1,121 @@ +# Zustand store +## Zustand 등록방법 +**-- index.tsx --** +### context 등록 +기본적으로 createContext를 통해 react에 context를 생성해서 react에서 사용할 수 있도록 해야함. +``` js +//StoreZus는 내부적으로 사용하기 위한 store파일에서 따로 생성함. +export type StoreApi = ReturnType<typeof StoreZus> +//react에서 사용하기 위한 context 생성. +export const StoreContext = createContext<StoreApi | undefined>( + undefined, +) +``` + +### component로 작성 +등록된 context를 component화 시키기 위한 작업 +```js +//외부에서 호출시 <StoreProvider> 형태로 호출. +//app이나 home에서 전체적인 사용이 되도록 작성해야함. +export const StoreProvider = ({ + children, +}: StoreProviderProps) => { + const storeRef = useRef<StoreApi>(null) + if (!storeRef.current) { + //StoreZus는 store.tsx를 봐야함. + storeRef.current = StoreZus() + } + return ( + <StoreContext.Provider value={storeRef.current}> + {children} + </StoreContext.Provider> + ) +} +``` + +### store 등록 +react에서 사용하는 store에 현재 context 등록 +```js +export const useWrStore = <T,>( + selector: (store: WrStore) => T, +): T => { + const storeContext = useContext(StoreContext) + if (!storeContext) { + throw new Error(`useStore must be used within StoreProvider`) + } + + return useStore(storeContext, selector) +} +``` + +## Zustand 사용방법 +**-- store.tsx, 폴더별 index.tsx --** +### 기본 store 생성 +index.tsx의 context에 사용되는 StoreZus를 추가하여 사용됨. + +외부에서 생성된 state, actions를 사용 가능한 구조 +```js +import { createStore } from 'zustand/vanilla'; +import { tableActionsExport, tableStateExport, type tableState, type tableType } from './publicReuseFaciltyInsert/index';//외부 파일 위치. +export type WrStore = tableType;//typescript이므로 사용되는 스크립트를 type1 & type2 형식으로 넣어야함. +//index.tsx에서 사용하는 store 생성. +export const StoreZus = (initState: tableState = tableStateExport) => { + return createStore<WrStore>()((set) => { + return { + //아래에서 사용하는 state & actions은 외부 폴더에서 가져다가 사용. + ...initState,//외부에 있는 기본 초기값 state + ...tableActionsExport(set)//외부에 있는 actions + } + }) +} +``` +### 외부 스토어 작성 방법 +외부 스토어는 export를 어떻게 할건지만 정하면됨. + +사용하는 store.tsx에서 import를 받기만 하면 사용가능. +#### 타입을 외부로 빼는 방법 +```js +export type tableState = { + isInit: boolean; + upHeadList: TableUpperProps[]; + publicReuseFacilityUper: PublicReuseFacility[]; +} +type tableActions = { + decrementList: () => void +} + +export type tableType = tableState & tableActions; +``` +#### 초기값 state를 외부로 빼는 방법 +```js +export const tableStateExport:tableState= { + isInit:false, + upHeadList: upHeadList, + publicReuseFacilityUper: publicReuseFacilityUper, +} +``` +#### actions를 외부로 빼는 방법 +```js +//tableActionsExport를 import 받아서 실행하여 return 된 값을 사용 +export const tableActionsExport:(set:any)=>tableActions= +//set을 통해 react store에 등록 +(set: any) => { + return { + //decrementList Object 형태로 전달 + decrementList: + //set 함수를 실행해서 react store에 등록 + ()=>set( + // store에 등록되는 함수 구조를 가지고 있음. + (state:tableState) => { + //actions 실질적인 로직 작성하는 곳. + return ( + { + upHeadList: state.upHeadList, + publicReuseFacilityUper: + state.publicReuseFacilityUper.slice(0, state.publicReuseFacilityUper.length - 1) + })} + ) + } +} +``` + diff --git a/apps/dm/app/store/sdmMng/index.tsx b/apps/dm/app/store/sdmMng/index.tsx new file mode 100644 index 000000000..65fadc714 --- /dev/null +++ b/apps/dm/app/store/sdmMng/index.tsx @@ -0,0 +1,533 @@ +import { fetchType, getFetch, postFetch } from "@/components/ui/api"; +import type { SelectDataType } from "@common/business_components/ui"; +import type { TableUpperProps } from "../common"; +import toast from "react-hot-toast"; + +/**==================================== + * Store - 타입 +=====================================*/ +// 수요처 정보 +export type DemandCompType = { + index : number | ''; + dmdInfoId : string | ''; + dmdInfoFk : string | ''; + bzmnNo : string | ''; + dmdCoNm : string | ''; + daddr : string | ''; + telno : string | ''; + rgtr : string | ''; + useUsgCd : string | ''; + ctpvCd : string | ''; + sggCd : string | ''; + dmdAplyPswd : string | ''; + wntUntprc : number | ''; + dmndTmng : Date | ''; + dmndQty : number | ''; + grssClfg : number | ''; + tn : number | ''; + combResdch : number | ''; + tph : number | ''; + trbt : number | ''; + ph : number | ''; + blgcOxygDmndQnt : number | ''; + clIonQnt : number | ''; + smll : number | ''; + cdtnv : number | ''; + crmty : number | ''; + rmrk : string | ''; + reqDt : string | ''; + dmdInfoSn : number | ''; + status : string | ''; + splyCndtList:SupplyCandidate[]|[], + +} + +// 공급 후보자 타입 정의 +export type SupplyCandidate = { + ruseUtztnFcltNm: string; + ruseUtztnFcltCd: string; +}; + + +export type DemandMatchings = { + splyFcltFk : string | ''; + splyFcltFlg : string | ''; + pbnstRuseFcltBizFk : number | ''; + splyBgngDay : string | ''; + splyEndDay : string | ''; + splyCpct : number | ''; + splyFmYn : string | ''; + useUsgCd : string | ''; + dmdInfoSn : number | ''; + splyCndtList : string | ''; + mtchnCmptnYmd : string | ''; + rvwCmptnYmd : string | ''; + regDt : string | ''; + rgtr : string | ''; + mdfcnDt : string | ''; + mdfr : string | ''; +} + +// 유사사례 +export type SimilarCaseType = { + ruseBizNm : string | ''; + bizBgngDate : string | ''; + bizEndDate : string | ''; + splyFcltKnd : string | ''; + splyFcltNm : string | ''; + splyFcltCpct : string | ''; + dmdFcltNm : string | ''; + dmdFcltAddr : string | ''; + ruseFcltCpct : string | ''; + ruseFcltPplLen : string | ''; + ruseFcltUsg : string | ''; + ruseFcltSplyCpct : string | ''; + ruseFcltPrcsMsrc : string | ''; + grnteWqUsg : string | ''; + grnteWqGrssClfg : string | ''; + grnteWqCombResdch : string | ''; + grnteWqTrbt : string | ''; + grnteWqBod : string | ''; + grnteWqSmll : string | ''; + grnteWqCrmty : string | ''; + grnteWqTn : string | ''; + grnteWqTp : string | ''; + grnteWqPh : string | ''; + grnteWqClIonQnt : string | ''; + grnteWqCndctStat : string | ''; + grnteWqRmrk : string | ''; + reqBizReuseFcltCst : string | ''; + reqBizPplCst : string | ''; + reqBizArchCst : string | ''; + reqBizMchnCst : string | ''; + reqBizMtrngCst : string | ''; + reqBizGisCst : string | ''; + reqBizCctvCst : string | ''; + reqBizCmcnCst : string | ''; + reqBizWstCst : string | ''; + reqBizDesignCst : string | ''; + reqBizSpvsCst : string | ''; + reqBizSbsdCst : string | ''; + reqBizEtc : string | ''; + reqBizGrssCst : string | ''; + prcmPlanNlep : string | ''; + prcmPlanLcep : string | ''; + prcmPlanBrdnCst : string | ''; + prcmPlanEcqrRt : string | ''; + prcmPlanGrssCst : string | ''; + clclnNlep : string | ''; + clclnLcep : string | ''; + clclnBrdnCst : string | ''; + clclnEcqrRt : string | ''; + clclnGrssCst : string | ''; +} + +// 사업비 상세정보 +export type MoneyDetailType = { + reqBizGrssCst : string | ''; + reqBizReuseFcltCst : string | ''; + reqBizPplCst : string | ''; + reqBizArchCst : string | ''; + reqBizMchnCst : string | ''; + reqBizMtrngCst : string | ''; + reqBizGisCst : string | ''; + reqBizCctvCst : string | ''; + reqBizCmcnCst : string | ''; + reqBizWstCst : string | ''; + reqBizDesignCst : string | ''; + reqBizSpvsCst : string | ''; + reqBizSbsdCst : string | ''; + reqBizEtc : string | ''; + + /** 재원조달계획(백만원원) */ + prcmPlanGrssCst : string | ''; + prcmPlanNlep : string | ''; + prcmPlanLcep : string | ''; + prcmPlanBrdnCst : string | ''; + prcmPlanEcqrRt : string | ''; + /** 사업비 정산(백만원) */ + clclnGrssCst : string | ''; + clclnNlep : string | ''; + clclnLcep : string | ''; + clclnBrdnCst : string | ''; + clclnEcqrRt : string | ''; +} + +/**==================================== + * Store - 데이터 + ====================================*/ +// 수요처 정보 +const demandCompList : DemandCompType[] = [ +] + +// 사용용도 +const useTypesData : SelectDataType[] = [ + { text: '청소', val: "청소" }, + { text: "화장실용수", val: "화장실용수" }, + { text: "세척", val: "세척" }, + { text: "살수", val: "살수" }, + { text: "조경", val: "조경" }, + { text: "농업", val: "농업" }, + { text: "하천", val: "하천" }, + { text: "지하수", val: "지하수" }, + { text: "공업", val: "공업" }, +]; + +// 유사사례 모달 헤더 +export const similarCaseModalHeader: TableUpperProps[] = [ + { id: "ruseBizNm", title: "사업명", row: 0, rowSpan: 2 }, + { id: "bizBgngDate", title: "사업시작일", row: 0, rowSpan: 2 }, + { id: "bizEndDate", title: "사업종료일", row: 0, rowSpan: 2 }, + { id: "A", title: "공급처", row: 0, colSpan: 3 }, + { id: "splyFcltKnd", title: "시설종류", row: 1 }, + { id: "splyFcltNm", title: "공급처명", row: 1 }, + { id: "splyFcltCpct", title: "시설용량\n(㎥/일)", row: 1 }, + { id: "B", title: "수요처", row: 0, colSpan: 2 }, + { id: "dmdFcltNm", title: "수요처명", row: 1 }, + { id: "dmdFcltAddr", title: "주소", row: 1 }, + { id: "C", title: "재이용시설", row: 0, colSpan: 5 }, + { id: "ruseFcltCpct", title: "시설용량\n(㎥/일)", row: 1 }, + { id: "ruseFcltPplLen", title: "관로\n(km)", row: 1 }, + { id: "ruseFcltUsg", title: "용도", row: 1 }, + { id: "ruseFcltSplyCpct", title: "공급량\n(㎥/일)", row: 1 }, + { id: "ruseFcltPrcsMsrc", title: "처리공법", row: 1 }, + { id: "D", title: "보증수질", row: 0, colSpan: 13 }, + { id: "grnteWqUsg", title: "용도별", row: 1 }, + { id: "grnteWqGrssClfg", title: "총대장균군수\n(개/100ml)", row: 1 }, + { id: "grnteWqCombResdch", title: "결합잔류염소\n(mg/L)", row: 1 }, + { id: "grnteWqTrbt", title: "탁도\n(NTU)", row: 1 }, + { id: "grnteWqBod", title: "생물학적산소요구량\n(BOD)\n(mg/L)", row: 1 }, + { id: "grnteWqSmll", title: "냄새\n(개/100ml)", row: 1 }, + { id: "grnteWqCrmty", title: "색도\n(도)", row: 1 }, + { id: "grnteWqTn", title: "총질소\n(T-N)\n(mg/L)", row: 1 }, + { id: "grnteWqTp", title: "총인\n(T-P)\n(mg/L)", row: 1 }, + { id: "grnteWqPh", title: "수소이온농도\n(pH)", row: 1 }, + { id: "grnteWqClIonQnt", title: "염화물\n(mgCL/L)", row: 1 }, + { id: "grnteWqCndctStat", title: "전기전도도\n(μS/cm)", row: 1 }, + { id: "grnteWqRmrk", title: "비고", row: 1 }, + { id: "reqBizGrssCst", title: "소요사업비\n(백만원)", row: 0, rowSpan: 2 }, +] + +// 유사사례 데이터 +const similarCaseList : SimilarCaseType[] = [ + +] + +// 사업비 상세정보 모달 헤더 +export const moneyDetailModalHeader: TableUpperProps[] = [ + { id: "A", title: "소요사업비(백만원)", row: 0, colSpan: 14 }, + { id: "reqBizGrssCst", title: "총", row: 1, rowSpan: 2 }, + { id: "B", title: "토목", row: 1, colSpan: 2 }, + { id: "reqBizReuseFcltCst", title: "재이용시설", row: 2 }, + { id: "reqBizPplCst", title: "관로", row: 2 }, + { id: "reqBizArchCst", title: "건축", row: 1, rowSpan: 2 }, + { id: "reqBizMchnCst", title: "기계", row: 1, rowSpan: 2 }, + { id: "reqBizMtrngCst", title: "전기,계측", row: 1, rowSpan: 2 }, + { id: "reqBizGisCst", title: "GIS/DB", row: 1, rowSpan: 2 }, + { id: "reqBizCctvCst", title: "CCTV", row: 1, rowSpan: 2 }, + { id: "reqBizCmcnCst", title: "시공 및 준공", row: 1, rowSpan: 2 }, + { id: "reqBizWstCst", title: "폐기물", row: 1, rowSpan: 2 }, + { id: "reqBizDesignCst", title: "설계비", row: 1, rowSpan: 2 }, + { id: "reqBizSpvsCst", title: "감리비", row: 1, rowSpan: 2 }, + { id: "reqBizSbsdCst", title: "시설 부대비", row: 1, rowSpan: 2 }, + { id: "reqBizEtc", title: "기타", row: 1, rowSpan: 2 }, + { id: "C", title: "재원조달계획(백만원)", row: 0, colSpan: 5 }, + { id: "prcmPlanGrssCst", title: "총", row: 1, rowSpan: 2 }, + { id: "prcmPlanNlep", title: "국비", row: 1, rowSpan: 2 }, + { id: "prcmPlanLcep", title: "지방비", row: 1, rowSpan: 2 }, + { id: "prcmPlanBrdnCst", title: "원인자부담", row: 1, rowSpan: 2 }, + { id: "prcmPlanEcqrRt", title: "국고비율", row: 1, rowSpan: 2 }, + { id: "D", title: "사업비 정산(백만원)", row: 0, colSpan: 5 }, + { id: "clclnGrssCst", title: "총", row: 1, rowSpan: 2 }, + { id: "clclnNlep", title: "국비", row: 1, rowSpan: 2 }, + { id: "clclnLcep", title: "지방비", row: 1, rowSpan: 2 }, + { id: "clclnBrdnCst", title: "원인자부담", row: 1, rowSpan: 2 }, + { id: "clclnEcqrRt", title: "국고비율", row: 1, rowSpan: 2 }, +] + +// 사업비 상세정보 +const moneyDetailList : MoneyDetailType[] = [ +] + +// 수요정보 신청현황 헤더 +export const upHeadSdmInfoStatus: TableUpperProps[] = [ + { id: "index", title: "No."}, + { id: "ctpvCd", title: "시도", row: 0, rowSpan: 2 }, + { id: "sggCd", title: "시군구", row: 0, rowSpan: 2 }, + { id: "dmdCoNm", title: "회사명", row: 0, rowSpan: 2 }, + { id: "daddr", title: "공장주소", row: 0, rowSpan: 2 }, + { id: "telno", title: "전화번호", row: 0, rowSpan: 2 }, + { id: "dmndTmng", title: "신청일자", row: 0, rowSpan: 2 }, + { id: "status", title: "진행상태", row: 0, rowSpan: 2 }, +] + +// 수요정보 신청현황 데이터 + +/**==================================== + * Store - Api +=====================================*/ +// 수요처 등록 +const postSdmData = async (data: DemandCompType) => { + try { + await postFetch("/reuseFc/request", data); + toast.success("수요처 등록에 성공하였습니다."); + return true; + } catch (error) { + console.log(error); + toast.error("수요처 등록에 실패하였습니다."); + return false; + } +} + +// 수요정보 등록 +const postSdmInfoData = async (data: DemandCompType) => { + try { + await postFetch("/reuseFc/request/demandInfo", data); + toast.success('수요 정보 등록에 성공하였습니다.'); + } catch (error) { + console.log(error); + toast.error('수요 정보 등록에 실패하였습니다.'); + } +} + +const postSdmLoginData = async (data: DemandCompType) => { + try { + const res = await postFetch("/reuseFc/request/login", data); + const jsonData = await res.json(); + + const status = jsonData.status; + const loginData = jsonData.data; + let loginSuccess = false; + if(status === 200 && loginData != null) { + alert("수요처 로그인에 성공하였습니다."); + sessionStorage.setItem("user", JSON.stringify(loginData)); + loginSuccess = true; + } else if(status === 401) { + alert("패스워드가 틀렸습니다."); + loginSuccess = false; + } else { + alert("등록된 수요처가 없습니다."); + loginSuccess = false; + } + + return loginSuccess; + } catch (error) { + console.log(error); + alert("수요처 로그인에 실패하였습니다."); + } +} + +const getDemandCompList = async (searchText: string, pageCnt: number) => { + try { + const url = '/reuseFc/request/' + (searchText ? searchText : '') + '/' + pageCnt; + const res = await getFetch(fetchType.WR, url); + return { + data: res?.data || [], + count: res?.count || 0 + } + } catch (error) { + console.error(error+' 수요처 정보를 불러오지 못했습니다.'); + return { data: [], count: 0 }; + } +} + +const getSdmInfoStatusList = async (dmdInfoId : string) => { + try { + const url = '/reuseFc/request/' + dmdInfoId; + const res = await getFetch(fetchType.WR, url); + return res?.data || []; + } catch (error) { + console.error(error+' 수요정보 신청현황을 불러오지 못했습니다.'); + return []; + } +} + +// 유사사례 조회 +const fetchGetSimilarCaseList = async (part: string, value: number|string) => { + try { + const url = '/reuseFc/pubFcRuseCase/dm/' + part + '/' + (value ? value : 0); + const res = await getFetch(fetchType.WR, url); + return res?.data || []; + } catch (error) { + console.error(error+' 유사사례 조회에 실패하였습니다.'); + return []; + } +} + +/**==================================== + * Store - expaort + ====================================*/ +export type SdmActions = { + initializeSdmData: () => void; + insertSdmData: (data: DemandCompType) => Promise<void>; + insertSdmInfoData: (data: DemandCompType) => Promise<void>; + getDemandCompList: (searchText: string, pageCnt: number) => Promise<void>; + actionLogin: (data: DemandCompType) => Promise<void>; + getSdmInfoStatusList: (dmdInfoId: string) => Promise<void>; + getSimilarCaseList: (part: string, value: number|string) => Promise<void>; +} + +export type SdmType ={ + Sdm :{ + isInit: boolean; + useTypesData: SelectDataType[]; + demandCompList: DemandCompType[]; + upHeadSimilarCaseList: TableUpperProps[]; + similarCaseList: SimilarCaseType[]; + upHeadMoneyDetailList : TableUpperProps[]; + moneyDetailList: MoneyDetailType[]; + + // 수요정보 신청현황 + sdmInfoStatusList: DemandCompType[]; + upHeadSdmInfoStatus: TableUpperProps[]; + + // 로그인관련 + successNextPage : boolean; + successLogin: boolean; + } +} + +export const SdmState: SdmType = { + Sdm:{ + isInit:false, + useTypesData: useTypesData, + demandCompList: demandCompList, + upHeadSimilarCaseList: similarCaseModalHeader, + similarCaseList: similarCaseList, + upHeadMoneyDetailList : moneyDetailModalHeader, + moneyDetailList: moneyDetailList, + // 수요정보 신청현황 + sdmInfoStatusList: [], + upHeadSdmInfoStatus: upHeadSdmInfoStatus, + // 로그인관련 + successNextPage : false, + successLogin: false, + } +} + +export const SdmReducer = (set: any): SdmActions => { + return { + initializeSdmData: async () => { + }, + + insertSdmData: async (data: DemandCompType) => { + const preRes = await getSdmInfoStatusList(data.dmdInfoId); + if(preRes?.dmdInfoId) { + toast.error('이미 등록된 수요처입니다.'); + return; + } + + const res = await postSdmData(data); + if(res) { + set((state: SdmType) => ({ + Sdm: { + ...state.Sdm, + successNextPage: true, + } + })) + } + }, + + insertSdmInfoData: async (data: DemandCompType) => { + postSdmInfoData(data); + }, + + // 수요처 정보 + getDemandCompList: async (searchText: string, pageCnt: number) => { + const res = await getDemandCompList(searchText, pageCnt); + if(res.data.length === 0) { + set((state: SdmType) => ({ + Sdm: { + ...state.Sdm, + demandCompList: [], + } + })) + return; + } else { + set((state: SdmType) => ({ + Sdm: { + ...state.Sdm, + demandCompList: res.data, + } + })) + } + }, + + // 수요정보 신청현황 + getSdmInfoStatusList: async (dmdInfoId: string) => { + const res = await getSdmInfoStatusList(dmdInfoId); + if(res.length === 0) { + set((state: SdmType) => ({ + Sdm: { + ...state.Sdm, + sdmInfoStatusList: [], + } + })) + return; + } + const sdmInfoStatusList : DemandCompType[] = []; + + res.demandInfo.map((el: DemandCompType, index: number) => { + const matchingInfo = res.demandMatchings.find((matching: DemandMatchings) => matching.dmdInfoSn === el.dmdInfoSn); + const status = matchingInfo?.splyFcltFlg?.trim() === 'Sub' ? '검토' : matchingInfo?.splyFcltFlg?.trim() === 'matched' ? '매칭완료' : '신청'; + + const tmpData = { + ...el, + index: ++index, + bzmnNo: res.bzmnNo, + dmdCoNm: res.dmdCoNm, + dmdInfoId: res.dmdInfoId, + daddr: res.daddr, + telno: res.telno.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3'), + rgtr: res.rgtr, + ctpvCd: res.ctpvCd, + sggCd: res.sggCd, + useUsgCd: el.useUsgCd, + dmndTmng: el.dmndTmng, + dmdInfoSn : el.dmdInfoSn, + status : status + } + sdmInfoStatusList.push(tmpData); + }) + set((state: SdmType) => ({ + Sdm: { + ...state.Sdm, + sdmInfoStatusList: sdmInfoStatusList, + } + })) + }, + + actionLogin: async (data: DemandCompType) => { + const loginSuccess = await postSdmLoginData(data); + set((state: SdmType) => ({ + Sdm: { + ...state.Sdm, + successLogin: loginSuccess, + } + })) + }, + + // 유사사례 조회 + getSimilarCaseList: async (part: string, value: number|string) => { + const res = await fetchGetSimilarCaseList(part, value); + if(res && res.length !== 0) { + set((state: SdmType) => ({ + Sdm: { + ...state.Sdm, + similarCaseList: res, + } + })) + return; + }else{ + set((state: SdmType) => ({ + Sdm: { + ...state.Sdm, + similarCaseList: [], + } + })) + return; + } + } + } +} +export type SdmStore = SdmType & SdmActions; diff --git a/apps/dm/app/store/store.tsx b/apps/dm/app/store/store.tsx new file mode 100644 index 000000000..d8f108319 --- /dev/null +++ b/apps/dm/app/store/store.tsx @@ -0,0 +1,27 @@ +import { createStore } from 'zustand/vanilla'; +import { CommonReducer, CommonState, type CommonStore, type CommonType } from './common'; +import { SdmReducer, SdmState, type SdmStore, type SdmType } from './sdmMng'; +import { dmMapState, type DmMapType, type DmMapStore, DmMapReducer } from './map'; +import { tokenManager } from '@/src/entities/auth/lib/token-manager'; + +export type DmStore = CommonStore & SdmStore &DmMapStore ; +export type DmState = CommonType & SdmType &DmMapType; + +export const StoreZus = (initState: DmState= { + ...CommonState, + ...SdmState, + ...dmMapState, +}) => { + return createStore<DmStore>()((set) => { + tokenManager.setToken("site-id-key", 'DM'); + tokenManager.setToken("site-theme-key", 'green'); + + + return { + ...initState, + ...CommonReducer(set), + ...SdmReducer(set), + ...DmMapReducer(set) + } + }) +} \ No newline at end of file diff --git a/apps/dm/app/style/PublicReuseFaciltyInsert.module.css b/apps/dm/app/style/PublicReuseFaciltyInsert.module.css new file mode 100644 index 000000000..6fc6f75ee --- /dev/null +++ b/apps/dm/app/style/PublicReuseFaciltyInsert.module.css @@ -0,0 +1,9 @@ +.formInsideTitle{ + background-color: gray; + text-align: center; +} + + + +.smallTitle{ +} \ No newline at end of file diff --git a/apps/dm/components.json b/apps/dm/components.json new file mode 100644 index 000000000..d9ef0ae53 --- /dev/null +++ b/apps/dm/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/dm/components/toastFromUi.tsx b/apps/dm/components/toastFromUi.tsx new file mode 100644 index 000000000..24b0b985f --- /dev/null +++ b/apps/dm/components/toastFromUi.tsx @@ -0,0 +1,53 @@ + +import { Button } from "@common/components/ui"; +import Link from "next/link"; +import toast from "react-hot-toast"; + +export function ToastFormUi(body:string,title:string,url:string,buttonName:string){ + + //const router = useRouter(); + + return( + + toast.custom((t) => ( + + <div + className={`${ + t.visible ? 'animate-enter' : 'animate-leave' + } w-4/12 h-40 bg-white shadow-lg rounded-lg pointer-events-auto flex ring-1 ring-black ring-opacity-5`} + > + <div className="w-full flex-1 py-10"> + <div className="flex items-start"> + <div className="flex-shrink-0 pt-0.5"> + + </div> + <div className="ml-3 flex-1"> + <p className="text-xl font-medium underline"> + {title} + </p> + <p className="mt-1 text-2xl"> + {body} + </p> + </div> + </div> + </div> + <div className="flex py-20"> + <Button + size='sm' + onClick={() => toast.dismiss(t.id)} + > + <Link href={url}> {buttonName}</Link> + </Button> + </div> + <div className="flex pl-4 pr-4 py-20"> + <Button + onClick={() => toast.dismiss(t.id)} + size='sm' + > + 닫기 + </Button> + </div> + </div> + )) + ) +} \ No newline at end of file diff --git a/apps/dm/components/ui/api/index.ts b/apps/dm/components/ui/api/index.ts new file mode 100644 index 000000000..51d06e700 --- /dev/null +++ b/apps/dm/components/ui/api/index.ts @@ -0,0 +1,72 @@ +//8880 +import { DEV_URL_FO, DEV_URL_WR } from "../config"; + +export const fetchType = { + COMMON: "common", + WR: "wr" +}; + +interface RequestParams { + [key: string]: unknown; +} + +export const getFetch = async (type: string, url: string) => { + const baseUrl = type === fetchType.COMMON ? DEV_URL_FO : DEV_URL_WR; + const response = await fetch(baseUrl + url, { + method: "GET", + headers: { + accept: "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`오류 발생: ${response.status}`); + } + const res = await response.json(); + return res; +}; + +export const postFetch = async (url: string, params: RequestParams) => { + const response = await fetch(DEV_URL_WR + url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(params), + }); + + if (!response.ok) { + throw new Error(`오류 발생: ${response.status}`); + } + return response; +}; + +export const putFetch = async (url: string, params: RequestParams) => { + const response = await fetch(DEV_URL_WR + url, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(params), + }); + + if (!response.ok) { + throw new Error(`오류 발생: ${response.status}`); + } + return response; +}; + +export const deleteFetch = async (url: string, params: RequestParams) => { + const response = await fetch(DEV_URL_WR + url, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(params), + }); + + if (!response.ok) { + throw new Error(`오류 발생: ${response.status}`); + } + return response; +}; \ No newline at end of file diff --git a/apps/dm/components/ui/common/searhDemandInput/component.tsx b/apps/dm/components/ui/common/searhDemandInput/component.tsx new file mode 100644 index 000000000..5598129a7 --- /dev/null +++ b/apps/dm/components/ui/common/searhDemandInput/component.tsx @@ -0,0 +1,103 @@ +import { useDmStore } from '@/app/store'; +import type { DemandCompType } from '@/app/store/sdmMng'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; + +type thisProps = { + label: string | undefined; + initValue: string | undefined; + setValue : (dmdCoNm?: string) => void; + setRtnData : (rtnData: DemandCompType[]) => void; +} + +export default function SearchInputText({label=undefined, initValue=undefined, setValue=()=>{}, setRtnData=()=>{}} :thisProps) { + + const { getDemandCompList, Sdm } = useDmStore((state) => state); + + const { + demandCompList + } = Sdm; + + const [isOpen, setIsOpen] = useState(false); + const [textValue, setTextValue] = useState(initValue || ''); + const wrapperRef = useRef<HTMLDivElement>(null); + const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { + const value = e.target.value; + setTextValue(value); + setValue(value); + if (value) { + setIsOpen(true); + getDemandCompList(value, 20); + } else { + setIsOpen(false); + } + }, [getDemandCompList, setValue]); + + const handleSuggestionClick = useCallback((dmdCoNm: string) => { + setTextValue(dmdCoNm); + setValue(dmdCoNm); + setIsOpen(false); + setRtnData(demandCompList); + }, [demandCompList, setRtnData, setValue]); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + return ( + <div ref={wrapperRef}> + <input + type="text" + value={textValue} + onChange={handleInputChange} + /* onFocus={() => setIsOpen(true)} */ + className="w-full h-[3.2rem] !text-xl pl-3 text-gray-600 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + placeholder={`${label}을(를) 입력하세요`} + /> + {isOpen && ( + <div className=" z-1 w-full mt-2 bg-white border border-gray-300 rounded-md shadow-lg h-90 overflow-auto"> + <div className="max-h-72"> + {demandCompList && demandCompList.length > 0 ? demandCompList.map((item : DemandCompType, index : number) => ( + <div + key={index} + className="!text-xl w-full text-left px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto" + onClick={() => handleSuggestionClick(item.dmdCoNm)} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + handleSuggestionClick(item.dmdCoNm); + } + }} + > + {item.dmdInfoId.substring(10,14) !== '0000' ? ( + <> + <span className="text-gray-500">사업자번호 : </span> {item.dmdInfoId.substring(0,10)}  + <span className="text-gray-500">종사업자번호 : </span> {item.dmdInfoId.substring(10,14)}  + </> + ) : ( + <> + <span className="text-gray-500">사업자번호 : </span> {item.dmdInfoId.substring(0,10)}  + </> + )} + <span className="float-right">{item.dmdCoNm}</span> + </div> + )) : ( + <div className="!text-xl w-full px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto"> + 검색 결과가 없습니다. + </div> + )} + </div> + </div> + )} + </div> + ) +} \ No newline at end of file diff --git a/apps/dm/components/ui/config/index.ts b/apps/dm/components/ui/config/index.ts new file mode 100644 index 000000000..2ed4ef03d --- /dev/null +++ b/apps/dm/components/ui/config/index.ts @@ -0,0 +1,5 @@ +import { PROXY_HOST, SERVER_API_FO_URL, SERVER_API_WR_URL } from "@/src/shared/config/env"; + +export const LOCAL_URL = PROXY_HOST; +export const DEV_URL_FO = SERVER_API_FO_URL; +export const DEV_URL_WR = SERVER_API_WR_URL; diff --git a/apps/dm/components/ui/map/SearchInputText/SearchInputText.tsx b/apps/dm/components/ui/map/SearchInputText/SearchInputText.tsx new file mode 100644 index 000000000..3660891f1 --- /dev/null +++ b/apps/dm/components/ui/map/SearchInputText/SearchInputText.tsx @@ -0,0 +1,108 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; + +type SearchType = { + searchNm: string; + searchPk: string; +}; +type thisProps = { + label: string | undefined; + initValue: string | undefined; + setValue: (searchNm?: string) => void; + setRtnData: (rtnData: any) => void; + searchList?: any[]; +}; +export default function SearchInputText({ + label = undefined, + initValue = undefined, + setValue = () => {}, + setRtnData = () => {}, + searchList = undefined, +}: thisProps) { + + const [isOpen, setIsOpen] = useState(false); + const [textValue, setTextValue] = useState(initValue || ""); + + const wrapperRef = useRef<HTMLDivElement>(null); + const handleInputChange = useCallback( + (e: React.ChangeEvent<HTMLInputElement>) => { + const value = e.target.value; + + setTextValue(value); + setValue(value); + if (value && value!=='') { + setIsOpen(true); + } else { + setIsOpen(false); + } + }, + [setValue] + ); + + const handleSuggestionClick = useCallback( + (searchNm: string) => { + // Do not update textValue to keep input unchanged on suggestion click + setValue(searchNm); + setIsOpen(false); + setRtnData(searchList?.find((item) => item.searchNm === searchNm) || {}); + }, + [searchList, setRtnData, setValue] + ); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + wrapperRef.current && + !wrapperRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + + return ( + <div ref={wrapperRef} className="relative"> + <input + type="text" + value={textValue ||''} + onChange={handleInputChange} + /* onFocus={() => setIsOpen(true)} */ + className="w-full h-[3.2rem] !text-xl pl-3 text-gray-600 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + placeholder={`${label}을(를) 입력하세요`} + /> + {isOpen && ( + <div className="absolute top-full left-0 z-50 mb-2 max-h-72 w-full overflow-auto rounded-md border border-gray-300 bg-white shadow-lg"> + <div className="max-h-72"> + {searchList && searchList.length > 0 ? ( + searchList.map((item: SearchType, index: number) => ( + <div + key={index} + className="w-full cursor-pointer overflow-auto px-5 py-3 text-left !text-xl hover:bg-gray-100" + onClick={() => handleSuggestionClick(item.searchNm)} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + handleSuggestionClick(item.searchNm); + } + }} + > + {item.searchPk?.substring(0, 10)}  + <span className="float-right">{item.searchNm}</span> + </div> + )) + ) : ( + <div className="!text-xl w-full px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto"> + 검색 결과가 없습니다. + </div> + )} + </div> + </div> + )} + </div> + ); +} diff --git a/apps/dm/components/ui/map/searchBar/searchBar.tsx b/apps/dm/components/ui/map/searchBar/searchBar.tsx new file mode 100644 index 000000000..544bc7984 --- /dev/null +++ b/apps/dm/components/ui/map/searchBar/searchBar.tsx @@ -0,0 +1,145 @@ +"use client"; +import { useDmStore } from "@/app/store"; +import { + SearchDiv, + SearchFormLeft, + SearchFormRight, + SelectBox, + type SelectDataType, +} from "@common/business_components/ui"; +import { Button, Label } from "@common/components/ui"; +import { useCallback, useEffect, useState } from "react"; +import SearchInputText from "../SearchInputText/SearchInputText"; + +export function SearchBar({ + setHasuPk, + onApply +}: { + setHasuPk: (hasuPk: string) => void; + onApply: () => void; +}) { + const { map, Common, initializeCommonData, findRuseFclt } = useDmStore( + (state) => state + ); + const [sidoValue, setSidoValue] = useState<string>(""); + const [sigunValue, setSigunValue] = useState<string>(""); + const [filteredSigunData, setFilteredSigunData] = useState<SelectDataType[]>([ + { text: "전체", val: "0" }, + ]); + useEffect(() => { + initializeCommonData(); + }, [initializeCommonData]); + + const initializeSigunData = useCallback( + (sidoCd: string) => { + if (sidoCd === "0") { + setSigunValue("0"); + setFilteredSigunData([{ text: "전체", val: "0" }]); + } else { + const newFilteredData = [ + { text: "전체", val: "0" }, + ...Common.selectSigunData.filter( + (item: SelectDataType) => item.val.substring(0, 2) === sidoCd + ), + ]; + setSigunValue("0"); + setFilteredSigunData(newFilteredData); + } + }, + [Common.selectSigunData] + ); + const onSelectValue = useCallback( + (val: string, type: string) => { + switch (type) { + case "ctpvCd": + initializeSigunData(val); + setSidoValue(val); + break; + case "sggCd": + setSigunValue(val); + } + }, + [initializeSigunData] + ); + + useEffect(() => { + console.log("map.ruseFcltList ", map.ruseFcltList); + }, [map.ruseFcltList]); + + return ( + <> + <div className="grid grid-cols-4 gap-4 "> + <div className="col-span-4"> + <SearchDiv> + <SearchFormLeft> + <div className="col-span-10 flex flex-row"> + <SelectBox + className="col-span-2 w-1/6 h-full text-2xl" + label="시도" + selectClass={""} + selectArray={Common.selectSidoData || []} + onSelectValue={(val) => + onSelectValue(val.toString(), "ctpvCd") + } + value={sidoValue || Common.selectSidoData[0]?.val || ""} + /> + <SelectBox + className="col-span-2 w-1/6 h-full text-2xl ml-10" + label="시군구" + selectClass={""} + selectArray={filteredSigunData || []} + onSelectValue={(val) => + onSelectValue(val.toString(), "sggCd") + } + value={sigunValue} + /> + <div className="col-span-6 flex flex-row items-center min-w-[27rem] ml-10"> + <Label className="text-xl justify-self-center mt-2 mx-3"> + 하수재이용시설 + </Label> + <SearchInputText + label={"시설명"} + initValue={undefined} + setValue={(searchNm?: string) => { + let sggOrCity = ""; + if (searchNm) { + if (sigunValue === "0" || sigunValue === "") { + sggOrCity = sidoValue; + } else { + sggOrCity = sigunValue; + } + findRuseFclt(sggOrCity, searchNm); + } + }} + setRtnData={(rtnData: any) => { + setHasuPk(rtnData.bscFctCd || ""); + }} + searchList={map.ruseFcltList || []} + /> + </div> + <div className="flex flex-row space-x-2"> + <Button + color="red" + className="mx-2 mt-2" + size={"sm"} + onClick={() => {}} + > + 후보지선택 + </Button> + <Button + color="sky" + className="mt-2" + size={"sm"} + onClick={() => {onApply();}} + > + 적용 + </Button> + </div> + </div> + </SearchFormLeft> + </SearchDiv> + </div> + </div> + </> + ); +} diff --git a/apps/dm/components/ui/sdmInfoInsertForm/AccessibleFormField.tsx b/apps/dm/components/ui/sdmInfoInsertForm/AccessibleFormField.tsx new file mode 100644 index 000000000..d2dfc00cb --- /dev/null +++ b/apps/dm/components/ui/sdmInfoInsertForm/AccessibleFormField.tsx @@ -0,0 +1,34 @@ +import { FormUi } from "@common/business_components/ui"; +import type { ReactNode } from "react"; +import type { ControllerRenderProps, FieldError, FieldValues, Path } from "react-hook-form"; + + +interface AccessibleFormFieldProps<T extends FieldValues> { + field: ControllerRenderProps<T, Path<T>>; + label: string; + required?: boolean; + error?: FieldError; + inputComponent: ReactNode; +} + +export function AccessibleFormField<T extends FieldValues>({ + label, + required = false, + error, + inputComponent, +}: AccessibleFormFieldProps<T>) { + const labelText = required ? `${label} *` : label; + + return ( + <FormUi label={labelText}> + <div> + {inputComponent} + {error?.message && Array.from(new Set(error.message.split('\n'))).map((msg, idx) => ( + <p key={idx} className="text-red-500 text-sm mt-1" role="alert"> + {msg} + </p> + ))} + </div> + </FormUi> + ); +} diff --git a/apps/dm/components/ui/sdmInfoInsertForm/component.tsx b/apps/dm/components/ui/sdmInfoInsertForm/component.tsx new file mode 100644 index 000000000..08309ff0f --- /dev/null +++ b/apps/dm/components/ui/sdmInfoInsertForm/component.tsx @@ -0,0 +1,606 @@ +import ModalSimilarPage from "@/app/(base-layout)/pages/sdmMng/sdmInfoInsert/modalSimilar/page"; +import type { FormData } from "@/app/(base-layout)/pages/sdmMng/sdmInfoInsert/page"; +import { useDmStore } from "@/app/store"; +import { + CANDIDATE, + DatePicker, + FormField, + FormLabel, + FormUi, + SelectBox, + UiModal, +} from "@common/business_components"; +import { Input } from "@common/components"; +import { Button } from "@common/components/ui"; +import { useSearchParams } from "next/navigation"; +import { useCallback, useEffect, useState } from "react"; +import { AccessibleFormField } from "./AccessibleFormField"; +import type { ControllerRenderProps, UseFormReturn } from "react-hook-form"; +import SearchInputText from "../common/searhDemandInput/component"; +import type { DemandCompType } from "@/app/store/sdmMng"; +import NewWindowPortal from "@/app/(popup-layout)/map/popup/windowPotal"; +import Map from "@/app/(popup-layout)/map/popup/sdmInfoMap"; + +export interface SdmInfoRegFormProps { + form: UseFormReturn<FormData>; + onCheckboxChange?: (checked: boolean) => void; + facilityName?: string; +} + +type FieldComponent = ( + field: ControllerRenderProps<FormData, keyof FormData> +) => JSX.Element; + +export function SdmInfoRegForm({ + form, + onCheckboxChange, + facilityName, +}: SdmInfoRegFormProps) { + const { initializeCommonData, Sdm } = useDmStore((state) => state); + const { useTypesData } = Sdm; + const searchParams = useSearchParams(); + + const [sdmNmValue, setSdmNmValue] = useState<string>( + searchParams.get("dmdCoNm") || "" + ); + const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(false); + const [isSimilarCase, setIsSimilarCase] = useState(false); + const [key, setKey] = useState(0); + // 후보지 팝업 오픈 상태 + const [isCandidatePopup, setIsCandidatePopup] = useState(false); + + //모달 조회에 필요한 데이터 + const [sply, setSply] = useState<number>(0); + const [usageCd, setUsageCd] = useState<string>("청소"); + const [money, setMoney] = useState<number>(0); + + useEffect(() => { + initializeCommonData(); + }, [initializeCommonData]); + + + + useEffect(() =>{ + console.log('candidateTitle ', form.getValues("candidateTitle")) + },[form.getValues("candidateTitle") ]) + useEffect(() => { + if (useTypesData && useTypesData.length > 0) { + setTimeout(() => { + form.setValue("usePurpose", useTypesData[0]?.val?.toString() || ""); + }, 500); + } + }, [useTypesData, form]); + + useEffect(() => { + if (facilityName) { + setSdmNmValue(facilityName); + form.setValue("sdmNm", facilityName); + setKey((prev) => prev + 1); + } + }, [facilityName, form]); + + useEffect(() => { + if (searchParams.get("dmdCoNm")) { + form.setValue("sdmNm", searchParams.get("dmdCoNm") || ""); + setSdmNmValue(searchParams.get("dmdCoNm") || ""); + setKey((prev) => prev + 1); + } + }, [searchParams, form]); + + const handleCandidateClose = useCallback( + () => setIsCandidatePopup(false), + [] + ); + + const searchCandidate = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + //setIsCandidatePopup(true); + const popupOptions = `width=1920,height=1080,resizable='y',scrollbars='y'`; + window.open("/map/popup", "candidatePopup", popupOptions); + }; + + const searchSimilarCase = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsSimilarCase(true); + }; + + const handleCheckboxChange = (checked: boolean) => { + setIsBznmTotNoDisabled(checked); + onCheckboxChange?.(checked); + }; + + // 시설 선택시 이벤트 + const selectComp = useCallback( + (rtnData: DemandCompType[]) => { + const rtnCompData = rtnData?.[0]; + form.setValue("sdmNm", rtnCompData?.dmdCoNm || ""); + }, + [form] + ); + + const formFields: Array<{ + name: keyof FormData; + label: string; + component: FieldComponent | undefined; + formIndex: number; + required?: boolean; + }> = [ + { + name: "sdmNm", + label: "수요처선택", + formIndex: 0, + required: true, + component: (field) => ( + <div key={key}> + <SearchInputText + label="수요처명" + setValue={(val) => { + setSdmNmValue(val || ""); + field.onChange(val || ""); + }} + setRtnData={selectComp} + initValue={facilityName || sdmNmValue} + {...field} + /> + </div> + ), + }, + { + name: "demandYmd", + label: "요구시기", + formIndex: 0, + required: true, + component: (field) => ( + <div className="w-full"> + <DatePicker + mode="single" + selected={field.value as Date} + onSelect={(date) => { + field.onChange(date as Date); + }} + className="w-full" + defaultMonth={field.value as Date} + field={{ + value: field.value as Date, + onChange: (date: Date | undefined) => { + field.onChange(date as Date); + }, + }} + selectClass={undefined} + /> + </div> + ), + }, + { + name: "demandQnty", + label: "요구수량(㎥/d)", + formIndex: 0, + required: true, + component: (field) => ( + <Input + placeholder="요구수량(㎥/d)을 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={(e) => { + field.onChange(e); + setSply(Number(e.target.value)); + }} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ), + }, + { + name: "usePurpose", + label: "사용용도", + formIndex: 0, + required: true, + component: (field) => ( + <SelectBox + selectClass={undefined} + label="" + selectArray={useTypesData} + onSelectValue={(val) => { + field.onChange(val.toString()); + setUsageCd(val.toString()); + }} + className="" + value={field.value as string} + /> + ), + }, + { + name: "money", + label: "희망단가(원/m3)", + formIndex: 0, + required: true, + component: (field) => ( + <AccessibleFormField + field={field} + label="희망단가(원/㎥)" + required={true} + error={form.formState.errors.money} + inputComponent={ + <FormUi + onCheckboxChange={handleCheckboxChange} + isDisabled={isBznmTotNoDisabled} + > + <Input + placeholder="희망단가(원/㎥)을 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={(e) => { + field.onChange(e); + setMoney(Number(e.target.value)); + }} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + </FormUi> + } + /> + ), + }, + { + name: "reqWaterQlty", + label: "요구수질", + formIndex: 0, + required: true, + component: undefined, + }, + { + name: "grssClfg", + label: "총대장균군수(개/100ml)", + formIndex: 1, + required: true, + component: (field) => ( + <Input + placeholder="총대장균군수를 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ), + }, + { + name: "tn", + label: "총질소(T-N)(mg/L)", + formIndex: 1, + required: true, + component: (field) => ( + <Input + placeholder="총질소를 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ), + }, + { + name: "combResdch", + label: "결합잔류염소(mg/L)", + formIndex: 1, + required: true, + component: (field) => ( + <Input + placeholder="결합잔류염소를 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ), + }, + { + name: "tph", + label: "총인(T-P)(mg/L)", + formIndex: 1, + required: true, + component: (field) => ( + <Input + placeholder="총인을 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ), + }, + { + name: "trbt", + label: "탁도(NTU)", + formIndex: 1, + required: true, + component: (field) => ( + <Input + placeholder="탁도를 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ), + }, + { + name: "ph", + label: "수소이온농도(pH)", + formIndex: 1, + required: true, + component: (field) => ( + <Input + placeholder="수소이온농도를 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ), + }, + { + name: "bod", + label: "생물학적 산소요구량(BOD)(mg/L)", + formIndex: 1, + required: true, + component: (field) => ( + <Input + placeholder="생물학적 산소요구량을 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ), + }, + { + name: "clIonQnt", + label: "염화물(mgCL/L)", + formIndex: 1, + required: true, + component: (field) => ( + <Input + placeholder="염화물을 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ), + }, + { + name: "smll", + label: "냄새", + formIndex: 1, + required: true, + component: (field) => ( + <Input + placeholder="냄새를 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ), + }, + { + name: "cdtnv", + label: "전기전도도(㎲/cm)", + formIndex: 1, + required: true, + component: (field) => ( + <Input + placeholder="전기전도도를 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ), + }, + { + name: "crmty", + label: "색도(도)", + formIndex: 1, + required: true, + component: (field) => ( + <Input + placeholder="색도를 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ), + }, + { + name: "rmrk", + label: "비고", + formIndex: 1, + component: (field) => ( + <Input + placeholder="비고를 입력하세요" + type="text" + className="!text-xl text-gray-600" + value={field.value as string} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + ), + }, + { + name: "candidateTitle", + label: CANDIDATE || "공급처 후보지 선택(필수아님)", + formIndex: 2, + component: () => ( + <div> + <div className="-mt-14 float-right flex gap-10"> + <Button className={"bg-gray-200"} onClick={searchCandidate}> + 후보지 조회 + </Button> + <Button className={"bg-gray-200"} onClick={searchSimilarCase}> + 유사사례 + </Button> + </div> + <div className="flex justify-center gap-10 mt-5"> + {(form.getValues("candidateTitle") || []).map((candidate: any, index: number) => ( + <Input + key={index} + placeholder={`후보지${index + 1}`} + type="text" + className="!text-xl w-[130px] text-gray-600" + value={candidate.ruseUtztnFcltNm} + readOnly + /> + ))} + </div> + </div> + ), + }, + ]; + + const normalFields = formFields.filter( + (f) => f.formIndex === 0 && f.component + ); + const groupFields = formFields.filter( + (f) => f.formIndex === 1 && f.component + ); + const candidateFields = formFields.filter( + (f) => f.formIndex === 2 && f.component + ); + + return ( + <> + {normalFields.map(({ name, label, component, required }) => + component ? ( + <FormField + key={name} + control={form.control} + name={name} + render={({ field }) => + name === "money" ? ( + component(field) + ) : ( + // <FormUi label={label}> + // {component(field)} + // </FormUi> + <AccessibleFormField + field={field} + label={label} + required={required} + error={form.formState.errors[name]} + inputComponent={component(field)} + /> + ) + } + /> + ) : null + )} + + {groupFields.length > 0 && ( + <div className="mt-10"> + <FormLabel className="text-xl">요구수질</FormLabel> + <div className="flex flex-wrap justify-start gap-10 mt-3 mb-3"> + {groupFields.map(({ name, label, component, required }) => ( + <div key={name} className="min-w-[180px]"> + <FormField + control={form.control} + name={name} + render={({ field }) => ( + // <FormUi label={label}> + // {component?.(field)} + // </FormUi> + <AccessibleFormField + field={field} + label={label} + required={required} + error={form.formState.errors[name]} + inputComponent={component?.(field)} + /> + )} + /> + </div> + ))} + </div> + </div> + )} + + {candidateFields.map(({ name, label, component, required }, index) => + component ? ( + <FormField + key={name + index} + control={form.control} + name={name} + render={({ field }) => ( + // <FormUi label={label}> + // {component(field)} + // </FormUi> + <AccessibleFormField + field={field} + label={label} + required={required} + error={form.formState.errors[name]} + inputComponent={component?.(field)} + /> + )} + /> + ) : null + )} + + {isSimilarCase && ( + <UiModal + body={ + <ModalSimilarPage sply={sply} usageCd={usageCd} money={money} /> + } + bodyClass="sm:max-w-[100vw] w-[1400px] h-[900px]" + open={isSimilarCase} + onOpenChange={(open) => !open && setIsSimilarCase(false)} + noHeaderYn="Y" + noFooterYn="Y" + /> + )} + + </> + ); +} diff --git a/apps/dm/components/ui/sdmLoginForm/component.tsx b/apps/dm/components/ui/sdmLoginForm/component.tsx new file mode 100644 index 000000000..204b12308 --- /dev/null +++ b/apps/dm/components/ui/sdmLoginForm/component.tsx @@ -0,0 +1,173 @@ +import { useDmStore } from "@/app/store"; +import type { DemandCompType } from "@/app/store/sdmMng"; +import { BZNM_TOT_NO, FormField, FormUi } from "@common/business_components"; +import { Button, Input } from "@common/components"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { sha512 } from 'js-sha512'; +import { useEffect, useState, useMemo } from "react"; +import { FormProvider, useForm, type ControllerRenderProps } from "react-hook-form"; +import * as z from "zod"; + +export function SdmLoginForm() { + const { actionLogin, initializeCommonData } = useDmStore((state) => state); + const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(true); + + const formatBusinessNumber = (value: string) => { + // 숫자만 추출 + const digitsOnly = value.replace(/\D/g, ""); + + // 10자리까지만 처리 + const limitedDigits = digitsOnly.slice(0, 10); + + // 3-2-5 형식으로 포맷팅 + if (limitedDigits.length <= 3) { + return limitedDigits; + } else if (limitedDigits.length <= 5) { + return `${limitedDigits.slice(0, 3)}-${limitedDigits.slice(3)}`; + } else { + return `${limitedDigits.slice(0, 3)}-${limitedDigits.slice(3, 5)}-${limitedDigits.slice(5)}`; + } + }; + + // 스키마를 useMemo로 최적화 + const sdmLoginFormSchema = useMemo(() => z.object({ + dmdInfoId: z.string() + .min(1, "사업자등록번호를 입력해주세요") + .regex(/^\d{10}$/, "사업자등록번호는 10자리 숫자만 입력 가능합니다"), + bznmTotNo: isBznmTotNoDisabled + ? z.string().optional() + : z.string().length(4, "종사업자등록번호는 4자리 숫자만 입력 가능합니다"), + password: z.string() + .min(1, "비밀번호를 입력해주세요") + .min(8, "비밀번호는 8자 이상 입력해주세요") + .max(16, "비밀번호는 16자 이내로 입력해주세요"), + rgtr: z.string().optional(), + dmdAplyPswd: z.string().optional(), + }), [isBznmTotNoDisabled]); + + type FormData = z.infer<typeof sdmLoginFormSchema>; + type FieldComponent = (field: ControllerRenderProps<FormData, keyof FormData>) => JSX.Element; + + const formSchema = sdmLoginFormSchema; + + useEffect(() => { + initializeCommonData(); + }, [initializeCommonData]); + + const defaultValues: FormData = { + dmdInfoId: '', + bznmTotNo: '', + password: '', + rgtr: '', + dmdAplyPswd: '', + }; + + const form = useForm<FormData>({ + resolver: zodResolver(formSchema), + defaultValues, + mode: "onChange", + }); + + const handleCheckboxChange = (checked: boolean) => { + setIsBznmTotNoDisabled(checked); + }; + + const handleLogin = () => { + const data = form.getValues(); + const passwordStr = sha512.hex(data.password) + data.dmdAplyPswd = passwordStr; + const param = { + dmdInfoId: data.dmdInfoId + (isBznmTotNoDisabled ? '0000' : data.bznmTotNo), + dmdAplyPswd: data.dmdAplyPswd, + } + actionLogin(param as unknown as DemandCompType); + }; + + const formFields: Array<{ + name: keyof FormData; + label: string; + component: FieldComponent | undefined; + }> = [ + { + name: "dmdInfoId", + label: "사업자등록번호", + component: (field) => ( + <Input + placeholder="사업자등록번호를 입력하세요" + type="text" + className="!mt-5 !mb-5 !text-xl text-gray-600" + {...field} + value={formatBusinessNumber(field.value as string)} + onChange={(e) => { + const dmdId = e.target.value.replace(/\D/g, "").slice(0, 10); + field.onChange(dmdId); + }} + /> + ) + }, + { + name: "bznmTotNo", + label: BZNM_TOT_NO || "종사업자등록번호", + component: (field) => ( + <FormUi + label="종사업자등록번호" + onCheckboxChange={handleCheckboxChange} + isDisabled={isBznmTotNoDisabled} + > + <Input + placeholder="여러사업장이 있을 경우 입력해주세요" + type="text" + className="!mt-5 !text-xl text-gray-600" + disabled={isBznmTotNoDisabled} + {...field} + /> + </FormUi> + ) + }, + { + name: "password", + label: "비밀번호", + component: (field) => ( + <Input + placeholder="비밀번호를 입력하세요" + type="password" + className="!mt-5 !mb-5 !text-xl text-gray-600" + {...field} + /> + ) + } + ]; + + return ( + <> + <div> + <FormProvider {...form}> + {formFields.map(({ name, label, component }) => ( + <FormField + key={name} + control={form.control} + name={name} + render={({ field }) => ( + label==='종사업자등록번호' ? + <> + {component && component(field)} + </> + : <FormUi label={label}> + {component && component(field)} + </FormUi> + )} + /> + ))} + </FormProvider> + <div className="flex justify-center pt-4"> + <Button + onClick={handleLogin} + className="w-32 bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-md transition-colors duration-200" + > + 로그인 + </Button> + </div> + </div> + </> + ); +} \ No newline at end of file diff --git a/apps/dm/eslint.config.mjs b/apps/dm/eslint.config.mjs new file mode 100644 index 000000000..29e63f653 --- /dev/null +++ b/apps/dm/eslint.config.mjs @@ -0,0 +1,20 @@ +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"), + {rules:{ + // other rules.. + "@typescript-eslint/no-explicit-any": "off", + }}, +]; + +export default eslintConfig; diff --git a/apps/dm/middleware.ts b/apps/dm/middleware.ts new file mode 100644 index 000000000..8fcfd4167 --- /dev/null +++ b/apps/dm/middleware.ts @@ -0,0 +1,132 @@ +import { NextRequest, NextResponse } from "next/server"; +import { CLAIM_NAME, AUTH_USER_ID } from "./src/shared/config"; + +// const CLAIM_NAME = "Authorization"; +// const AUTH_USER_ID = "token-id"; +const serverApiUrl = process.env.NEXT_PUBLIC_CS_SERVER_API_URL; +const GEOSERVER_URL = process.env.NEXT_PUBLIC_GEOSERVER_URL || ""; +const LX_GEOSERVER_URL = process.env.NEXT_PUBLIC_LX_GEOSERVER_URL || ""; +const SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ""; + +export function middleware(req: NextRequest) { + const pathname = req.nextUrl.pathname; + console.log(`[Middleware] Request URL: ${req.url}`); + const searchParams = req.nextUrl.searchParams; + + // if (pathname.startsWith("/server")) { + // const queryParams = searchParams.toString(); + // const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${ + // queryParams ? `?${queryParams}` : "" + // }`; + // console.log(`[Middleware] Rewriting from '/server' path to: ${newUrl}`); + // return NextResponse.rewrite(new URL(newUrl)); + // } + + if (pathname.startsWith("/server")) { + const decodedParams = Array.from(searchParams.entries()) + .map(([key, value]) => `${key}=${decodeURIComponent(value)}`) + .join("&"); + + const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${ + decodedParams ? `?${decodedParams}` : "" + }`; + return NextResponse.rewrite(new URL(newUrl)); + } + + if (pathname === "/") { + console.log(" maim 화면이동 "); + return NextResponse.redirect(new URL("/pages/wrStatus", req.url)); + } + + + if (pathname.startsWith("/attach-service")) { + const requestHeaders = new Headers(req.headers); + // const token = + // "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhMGVjN2EwNy03NTJhLTRmN2YtODlhNi04NTRlNjNkMmUzYTAiLCJhdXRob3JpdGllcyI6IlJPTEVfQURNSU4sUk9MRV9BTk9OWU1PVVMiLCJtbmdyWW4iOiJZIiwiZXhwIjoxNzUyMTM1OTk3fQ.itGY6M6mC2iwKvEpXw2KS6354T_B_OFz4hD26ResIgwJhpwiDJCMuSXD-rZ3AarmAIKd0udpCuJR7UlvFMN2ww"; + // const userId = "a0ec7a07-752a-4f7f-89a6-854e63d2e3a0"; + const token = req.cookies.get(CLAIM_NAME)?.value; + const userId = req.cookies.get(AUTH_USER_ID)?.value; + const newUrl = `${serverApiUrl}${pathname}`; + + //wr:dev: pathname /attach-service/api/v1/tus/upload/5e60b70a-956e-40c3-be5a-cdd3100bf8ad + //http://dev.onasys.co.kr:8000/attach-service/api/v1/tus/upload/5e60b70a-956e-40c3-be5a-cdd3100bf8ad + if (token) requestHeaders.set(CLAIM_NAME, token); + if (userId) requestHeaders.set(AUTH_USER_ID, userId); + + console.log( + `[Middleware] Rewriting from '/attach-service' path to: ${newUrl}` + ); + return NextResponse.rewrite(new URL(newUrl), { + headers: requestHeaders, + }); + } + + if (pathname.startsWith("/user-service")) { + const requestHeaders = new Headers(req.headers); + const newUrl = `${serverApiUrl}${pathname}`; + + console.log( + `[Middleware] Rewriting from '/user-service' path to: ${newUrl}` + ); + return NextResponse.rewrite(new URL(newUrl), { + headers: requestHeaders + }); + } + + if (pathname.startsWith("/geoserver")) { + let queryParams = searchParams.toString(); + const typename = searchParams.get("TYPENAME"); + console.log("typename ", typename); + + let newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; + + if ( + typename == "hasudo:tb_il_swage_prcs_fclt" //하수처리시설 + || typename == "hasudo:tb_ms_swage_prcs_zone" //하수처리구역 + || typename == "hasudo:tb_ms_sbmrg_dngr_map" //침수위험지도 + || typename == "hasudo:tb_ms_flood_dngr_map" //도시침수지도 + ) { + newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; + } + else if ( + typename == "hasudo:tb_ms_swage_ppl" //관로 + || typename == "hasudo:tb_ms_swage_mhl" //맨홀 + || typename == "hasudo:tb_ms_swage_pmstn" //펌프장 + || typename == "hasudo:tb_ms_stofcbr" //우수토실 + ) { + newUrl = `${LX_GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; + } + else if ( + typename == "hasudo:om_dt_untrt_swg_rain" //중점우수토실 + || typename == "hasudo:om_wqlt_station" //관측소 + || typename == "hasudo:tb_wr_bdst_ruse_fclt" //재이용시설 + || typename == "hasudo:tb_wr_rnwtr_utztn_fclt" //빗물이용시설 + || typename == "hasudo:tb_wr_rswtr_fclt" //중수도시설 + || typename == "hasudo:tb_wr_htdfl_fclt" //온배수시설 + || typename == "hasudo:tb_wr_ruse_fclt_dmd_co" //수요처 + || typename == "hasudo:tb_wr_tmpr_dmd_co" //잠재수요처 + ){ + const url = req.nextUrl.clone(); + url.searchParams.set('TYPENAME', typename.replace("hasudo:","")); + queryParams = url.searchParams.toString(); + newUrl = `${SEWERAGE_API_URL}/v1/map/geometry${queryParams ? `?${queryParams}` : ""}`; + } + + console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`); + return NextResponse.rewrite(new URL(newUrl)); + } + + if (pathname.startsWith("/sewerage-service")) { + const queryParams = searchParams.toString(); + const newUrl = `${SEWERAGE_API_URL}${pathname.replace("/sewerage-service/api", "")}${queryParams ? `?${queryParams}` : "" + }`; + console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`); + return NextResponse.rewrite(new URL(newUrl)); + } + + return NextResponse.next(); +} + +export const config = { + matcher: ["/server/:path*", "/user-service/:path*", "/attach-service/:path*", "/geoserver/:path*", "/sewerage-service/:path*", "/"], +}; diff --git a/apps/dm/next.config.ts b/apps/dm/next.config.ts new file mode 100644 index 000000000..48fc8dacb --- /dev/null +++ b/apps/dm/next.config.ts @@ -0,0 +1,34 @@ +import type { NextConfig } from "next"; + +const subDomain = + process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:3000"; + +const nextConfig: NextConfig = { + output: "standalone", + async rewrites() { + return { + beforeFiles: [ + { + source: "/_next/static/:path*", + destination: `${subDomain}/_next/static/:path*`, + has: [ + { + type: "header", + key: "referer", + value: "(.*)/pst(.*)", + }, + ], + }, + ], + afterFiles: [ + { + source: "/pst/:path*", + destination: `${subDomain}/pst/:path*`, + }, + ], + fallback: [], + }; + }, +}; + +export default nextConfig; diff --git a/apps/dm/package.json b/apps/dm/package.json new file mode 100644 index 000000000..2fdff5dbc --- /dev/null +++ b/apps/dm/package.json @@ -0,0 +1,37 @@ +{ + "name": "dm", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@common/assets": "workspace:*", + "@common/business_components": "workspace:*", + "@common/components": "workspace:*", + "@hookform/resolvers": "^4.1.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "js-cookie": "^3.0.5", + "js-sha512": "^0.9.0", + "lucide-react": "^0.474.0", + "next": "15.1.7", + "react-hook-form": "^7.54.2", + "react-hot-toast": "^2.5.2", + "tailwind-merge": "^3.0.1", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.24.2" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@types/webpack": "^5.28.5", + "eslint": "^9", + "eslint-config-next": "15.1.7", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } +} diff --git a/apps/dm/postcss.config.mjs b/apps/dm/postcss.config.mjs new file mode 100644 index 000000000..1a69fd2a4 --- /dev/null +++ b/apps/dm/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/apps/dm/public/file.svg b/apps/dm/public/file.svg new file mode 100644 index 000000000..004145cdd --- /dev/null +++ b/apps/dm/public/file.svg @@ -0,0 +1 @@ +<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg> \ No newline at end of file diff --git a/apps/dm/public/fonts/NotoSansKR-Black.otf b/apps/dm/public/fonts/NotoSansKR-Black.otf new file mode 100644 index 000000000..2b54d195e Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Black.otf differ diff --git a/apps/dm/public/fonts/NotoSansKR-Black.woff b/apps/dm/public/fonts/NotoSansKR-Black.woff new file mode 100644 index 000000000..90ef00381 Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Black.woff differ diff --git a/apps/dm/public/fonts/NotoSansKR-Black.woff2 b/apps/dm/public/fonts/NotoSansKR-Black.woff2 new file mode 100644 index 000000000..8953c8fa7 Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Black.woff2 differ diff --git a/apps/dm/public/fonts/NotoSansKR-Bold.otf b/apps/dm/public/fonts/NotoSansKR-Bold.otf new file mode 100644 index 000000000..2b78dd845 Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Bold.otf differ diff --git a/apps/dm/public/fonts/NotoSansKR-Bold.woff b/apps/dm/public/fonts/NotoSansKR-Bold.woff new file mode 100644 index 000000000..74b20d49c Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Bold.woff differ diff --git a/apps/dm/public/fonts/NotoSansKR-Bold.woff2 b/apps/dm/public/fonts/NotoSansKR-Bold.woff2 new file mode 100644 index 000000000..216f94dad Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Bold.woff2 differ diff --git a/apps/dm/public/fonts/NotoSansKR-Light.otf b/apps/dm/public/fonts/NotoSansKR-Light.otf new file mode 100644 index 000000000..ecab31abc Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Light.otf differ diff --git a/apps/dm/public/fonts/NotoSansKR-Light.woff b/apps/dm/public/fonts/NotoSansKR-Light.woff new file mode 100644 index 000000000..40aabd2a4 Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Light.woff differ diff --git a/apps/dm/public/fonts/NotoSansKR-Light.woff2 b/apps/dm/public/fonts/NotoSansKR-Light.woff2 new file mode 100644 index 000000000..6b7cd1307 Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Light.woff2 differ diff --git a/apps/dm/public/fonts/NotoSansKR-Medium.otf b/apps/dm/public/fonts/NotoSansKR-Medium.otf new file mode 100644 index 000000000..371959d51 Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Medium.otf differ diff --git a/apps/dm/public/fonts/NotoSansKR-Medium.woff b/apps/dm/public/fonts/NotoSansKR-Medium.woff new file mode 100644 index 000000000..e15aae09e Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Medium.woff differ diff --git a/apps/dm/public/fonts/NotoSansKR-Medium.woff2 b/apps/dm/public/fonts/NotoSansKR-Medium.woff2 new file mode 100644 index 000000000..b97668034 Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Medium.woff2 differ diff --git a/apps/dm/public/fonts/NotoSansKR-Regular.otf b/apps/dm/public/fonts/NotoSansKR-Regular.otf new file mode 100644 index 000000000..7ba76b49c Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Regular.otf differ diff --git a/apps/dm/public/fonts/NotoSansKR-Regular.woff b/apps/dm/public/fonts/NotoSansKR-Regular.woff new file mode 100644 index 000000000..40fab257c Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Regular.woff differ diff --git a/apps/dm/public/fonts/NotoSansKR-Regular.woff2 b/apps/dm/public/fonts/NotoSansKR-Regular.woff2 new file mode 100644 index 000000000..61d064695 Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Regular.woff2 differ diff --git a/apps/dm/public/fonts/NotoSansKR-Thin.otf b/apps/dm/public/fonts/NotoSansKR-Thin.otf new file mode 100644 index 000000000..06c566278 Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Thin.otf differ diff --git a/apps/dm/public/fonts/NotoSansKR-Thin.woff b/apps/dm/public/fonts/NotoSansKR-Thin.woff new file mode 100644 index 000000000..40723731f Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Thin.woff differ diff --git a/apps/dm/public/fonts/NotoSansKR-Thin.woff2 b/apps/dm/public/fonts/NotoSansKR-Thin.woff2 new file mode 100644 index 000000000..7835c23ed Binary files /dev/null and b/apps/dm/public/fonts/NotoSansKR-Thin.woff2 differ diff --git a/apps/dm/public/globe.svg b/apps/dm/public/globe.svg new file mode 100644 index 000000000..567f17b0d --- /dev/null +++ b/apps/dm/public/globe.svg @@ -0,0 +1 @@ +<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg> \ No newline at end of file diff --git a/apps/dm/public/images/map/layericon/BML_CBAS_PS.png b/apps/dm/public/images/map/layericon/BML_CBAS_PS.png new file mode 100644 index 000000000..5ff4add3e Binary files /dev/null and b/apps/dm/public/images/map/layericon/BML_CBAS_PS.png differ diff --git a/apps/dm/public/images/map/layericon/BML_ELEV_PS.png b/apps/dm/public/images/map/layericon/BML_ELEV_PS.png new file mode 100644 index 000000000..83970a331 Binary files /dev/null and b/apps/dm/public/images/map/layericon/BML_ELEV_PS.png differ diff --git a/apps/dm/public/images/map/layericon/BML_ETCP_PS.png b/apps/dm/public/images/map/layericon/BML_ETCP_PS.png new file mode 100644 index 000000000..df9e4957b Binary files /dev/null and b/apps/dm/public/images/map/layericon/BML_ETCP_PS.png differ diff --git a/apps/dm/public/images/map/layericon/BML_STMS_PS.png b/apps/dm/public/images/map/layericon/BML_STMS_PS.png new file mode 100644 index 000000000..423f2e8fc Binary files /dev/null and b/apps/dm/public/images/map/layericon/BML_STMS_PS.png differ diff --git a/apps/dm/public/images/map/layericon/BML_SYMB_PS.png b/apps/dm/public/images/map/layericon/BML_SYMB_PS.png new file mode 100644 index 000000000..497589bea Binary files /dev/null and b/apps/dm/public/images/map/layericon/BML_SYMB_PS.png differ diff --git a/apps/dm/public/images/map/layericon/RDL_BYST_PS.png b/apps/dm/public/images/map/layericon/RDL_BYST_PS.png new file mode 100644 index 000000000..1e78cba68 Binary files /dev/null and b/apps/dm/public/images/map/layericon/RDL_BYST_PS.png differ diff --git a/apps/dm/public/images/map/layericon/RDL_CROS_PS.png b/apps/dm/public/images/map/layericon/RDL_CROS_PS.png new file mode 100644 index 000000000..57b5f2ca5 Binary files /dev/null and b/apps/dm/public/images/map/layericon/RDL_CROS_PS.png differ diff --git a/apps/dm/public/images/map/layericon/RDL_ETCT_PS.png b/apps/dm/public/images/map/layericon/RDL_ETCT_PS.png new file mode 100644 index 000000000..29c0b77b4 Binary files /dev/null and b/apps/dm/public/images/map/layericon/RDL_ETCT_PS.png differ diff --git a/apps/dm/public/images/map/layericon/RDL_OCUP_PS.png b/apps/dm/public/images/map/layericon/RDL_OCUP_PS.png new file mode 100644 index 000000000..8a1ac0074 Binary files /dev/null and b/apps/dm/public/images/map/layericon/RDL_OCUP_PS.png differ diff --git a/apps/dm/public/images/map/layericon/RDL_RDSN_PS.png b/apps/dm/public/images/map/layericon/RDL_RDSN_PS.png new file mode 100644 index 000000000..5629de9fb Binary files /dev/null and b/apps/dm/public/images/map/layericon/RDL_RDSN_PS.png differ diff --git a/apps/dm/public/images/map/layericon/RDL_SCLT_PS.png b/apps/dm/public/images/map/layericon/RDL_SCLT_PS.png new file mode 100644 index 000000000..7281dc61d Binary files /dev/null and b/apps/dm/public/images/map/layericon/RDL_SCLT_PS.png differ diff --git a/apps/dm/public/images/map/layericon/RDL_SEPC_PS.png b/apps/dm/public/images/map/layericon/RDL_SEPC_PS.png new file mode 100644 index 000000000..087b4e941 Binary files /dev/null and b/apps/dm/public/images/map/layericon/RDL_SEPC_PS.png differ diff --git a/apps/dm/public/images/map/layericon/RDL_STAT_PS.png b/apps/dm/public/images/map/layericon/RDL_STAT_PS.png new file mode 100644 index 000000000..13d8ea689 Binary files /dev/null and b/apps/dm/public/images/map/layericon/RDL_STAT_PS.png differ diff --git a/apps/dm/public/images/map/layericon/RDL_STLT_PS.png b/apps/dm/public/images/map/layericon/RDL_STLT_PS.png new file mode 100644 index 000000000..489524b5c Binary files /dev/null and b/apps/dm/public/images/map/layericon/RDL_STLT_PS.png differ diff --git a/apps/dm/public/images/map/layericon/RDL_TFSN_PS.png b/apps/dm/public/images/map/layericon/RDL_TFSN_PS.png new file mode 100644 index 000000000..6dfff6696 Binary files /dev/null and b/apps/dm/public/images/map/layericon/RDL_TFSN_PS.png differ diff --git a/apps/dm/public/images/map/layericon/RDL_TRCT_PS.png b/apps/dm/public/images/map/layericon/RDL_TRCT_PS.png new file mode 100644 index 000000000..ed1a72d8a Binary files /dev/null and b/apps/dm/public/images/map/layericon/RDL_TRCT_PS.png differ diff --git a/apps/dm/public/images/map/layericon/RDL_TREE_PS.png b/apps/dm/public/images/map/layericon/RDL_TREE_PS.png new file mode 100644 index 000000000..0ee84dedc Binary files /dev/null and b/apps/dm/public/images/map/layericon/RDL_TREE_PS.png differ diff --git a/apps/dm/public/images/map/layericon/RDL_TRSN_PS.png b/apps/dm/public/images/map/layericon/RDL_TRSN_PS.png new file mode 100644 index 000000000..15675cb31 Binary files /dev/null and b/apps/dm/public/images/map/layericon/RDL_TRSN_PS.png differ diff --git a/apps/dm/public/images/map/layericon/RDL_TSCT_PS.png b/apps/dm/public/images/map/layericon/RDL_TSCT_PS.png new file mode 100644 index 000000000..ffc225159 Binary files /dev/null and b/apps/dm/public/images/map/layericon/RDL_TSCT_PS.png differ diff --git a/apps/dm/public/images/map/layericon/SWL_CLAY_PS.png b/apps/dm/public/images/map/layericon/SWL_CLAY_PS.png new file mode 100644 index 000000000..e603ffa9c Binary files /dev/null and b/apps/dm/public/images/map/layericon/SWL_CLAY_PS.png differ diff --git a/apps/dm/public/images/map/layericon/SWL_CLAY_PS_RED.png b/apps/dm/public/images/map/layericon/SWL_CLAY_PS_RED.png new file mode 100644 index 000000000..0e5450688 Binary files /dev/null and b/apps/dm/public/images/map/layericon/SWL_CLAY_PS_RED.png differ diff --git a/apps/dm/public/images/map/layericon/SWL_DEPT_PS.png b/apps/dm/public/images/map/layericon/SWL_DEPT_PS.png new file mode 100644 index 000000000..fede6c304 Binary files /dev/null and b/apps/dm/public/images/map/layericon/SWL_DEPT_PS.png differ diff --git a/apps/dm/public/images/map/layericon/SWL_MANH_PS.png b/apps/dm/public/images/map/layericon/SWL_MANH_PS.png new file mode 100644 index 000000000..75217f494 Binary files /dev/null and b/apps/dm/public/images/map/layericon/SWL_MANH_PS.png differ diff --git a/apps/dm/public/images/map/layericon/SWL_MANH_PS_BLACK.png b/apps/dm/public/images/map/layericon/SWL_MANH_PS_BLACK.png new file mode 100644 index 000000000..5d2e8a70a Binary files /dev/null and b/apps/dm/public/images/map/layericon/SWL_MANH_PS_BLACK.png differ diff --git a/apps/dm/public/images/map/layericon/SWL_MANH_PS_BLUE.png b/apps/dm/public/images/map/layericon/SWL_MANH_PS_BLUE.png new file mode 100644 index 000000000..e521cb84a Binary files /dev/null and b/apps/dm/public/images/map/layericon/SWL_MANH_PS_BLUE.png differ diff --git a/apps/dm/public/images/map/layericon/SWL_MANH_PS_GRAY.png b/apps/dm/public/images/map/layericon/SWL_MANH_PS_GRAY.png new file mode 100644 index 000000000..e19f3d8b3 Binary files /dev/null and b/apps/dm/public/images/map/layericon/SWL_MANH_PS_GRAY.png differ diff --git a/apps/dm/public/images/map/layericon/SWL_MANH_PS_GREEN.png b/apps/dm/public/images/map/layericon/SWL_MANH_PS_GREEN.png new file mode 100644 index 000000000..4503c13c6 Binary files /dev/null and b/apps/dm/public/images/map/layericon/SWL_MANH_PS_GREEN.png differ diff --git a/apps/dm/public/images/map/layericon/SWL_MANH_PS_ORANGE.png b/apps/dm/public/images/map/layericon/SWL_MANH_PS_ORANGE.png new file mode 100644 index 000000000..288d1afa8 Binary files /dev/null and b/apps/dm/public/images/map/layericon/SWL_MANH_PS_ORANGE.png differ diff --git a/apps/dm/public/images/map/layericon/SWL_MANH_PS_RED.png b/apps/dm/public/images/map/layericon/SWL_MANH_PS_RED.png new file mode 100644 index 000000000..b8fa45f9c Binary files /dev/null and b/apps/dm/public/images/map/layericon/SWL_MANH_PS_RED.png differ diff --git a/apps/dm/public/images/map/layericon/SWL_MANH_PS_YELLOW.png b/apps/dm/public/images/map/layericon/SWL_MANH_PS_YELLOW.png new file mode 100644 index 000000000..b7de1fbfd Binary files /dev/null and b/apps/dm/public/images/map/layericon/SWL_MANH_PS_YELLOW.png differ diff --git a/apps/dm/public/images/map/layericon/SWL_SPEW_PS.png b/apps/dm/public/images/map/layericon/SWL_SPEW_PS.png new file mode 100644 index 000000000..d260ea3a6 Binary files /dev/null and b/apps/dm/public/images/map/layericon/SWL_SPEW_PS.png differ diff --git a/apps/dm/public/images/map/layericon/SWL_SPOT_PS.png b/apps/dm/public/images/map/layericon/SWL_SPOT_PS.png new file mode 100644 index 000000000..84c7992e4 Binary files /dev/null and b/apps/dm/public/images/map/layericon/SWL_SPOT_PS.png differ diff --git a/apps/dm/public/images/map/layericon/SWL_SPOT_PS_BLACK.png b/apps/dm/public/images/map/layericon/SWL_SPOT_PS_BLACK.png new file mode 100644 index 000000000..fe67f8f98 Binary files /dev/null and b/apps/dm/public/images/map/layericon/SWL_SPOT_PS_BLACK.png differ diff --git a/apps/dm/public/images/map/layericon/SWL_SPOT_PS_BLUE.png b/apps/dm/public/images/map/layericon/SWL_SPOT_PS_BLUE.png new file mode 100644 index 000000000..691f2f45e Binary files /dev/null and b/apps/dm/public/images/map/layericon/SWL_SPOT_PS_BLUE.png differ diff --git a/apps/dm/public/images/map/layericon/SWL_SPOT_PS_GREEN.png b/apps/dm/public/images/map/layericon/SWL_SPOT_PS_GREEN.png new file mode 100644 index 000000000..d09c4bd97 Binary files /dev/null and b/apps/dm/public/images/map/layericon/SWL_SPOT_PS_GREEN.png differ diff --git a/apps/dm/public/images/map/layericon/SWL_SPOT_PS_GREY.png b/apps/dm/public/images/map/layericon/SWL_SPOT_PS_GREY.png new file mode 100644 index 000000000..32d0bf519 Binary files /dev/null and b/apps/dm/public/images/map/layericon/SWL_SPOT_PS_GREY.png differ diff --git a/apps/dm/public/images/map/layericon/SWL_SPOT_PS_RED.png b/apps/dm/public/images/map/layericon/SWL_SPOT_PS_RED.png new file mode 100644 index 000000000..a2da36f9b Binary files /dev/null and b/apps/dm/public/images/map/layericon/SWL_SPOT_PS_RED.png differ diff --git a/apps/dm/public/images/map/layericon/SWL_VENT_PS.png b/apps/dm/public/images/map/layericon/SWL_VENT_PS.png new file mode 100644 index 000000000..834eeec21 Binary files /dev/null and b/apps/dm/public/images/map/layericon/SWL_VENT_PS.png differ diff --git a/apps/dm/public/images/map/layericon/TL_SPBD_ENTRC.png b/apps/dm/public/images/map/layericon/TL_SPBD_ENTRC.png new file mode 100644 index 000000000..8e7de8593 Binary files /dev/null and b/apps/dm/public/images/map/layericon/TL_SPBD_ENTRC.png differ diff --git a/apps/dm/public/images/map/layericon/TL_SPGF_LOC.png b/apps/dm/public/images/map/layericon/TL_SPGF_LOC.png new file mode 100644 index 000000000..74b0adac3 Binary files /dev/null and b/apps/dm/public/images/map/layericon/TL_SPGF_LOC.png differ diff --git a/apps/dm/public/images/map/layericon/TL_SPOT_NOIN.png b/apps/dm/public/images/map/layericon/TL_SPOT_NOIN.png new file mode 100644 index 000000000..4800beaeb Binary files /dev/null and b/apps/dm/public/images/map/layericon/TL_SPOT_NOIN.png differ diff --git a/apps/dm/public/images/map/layericon/TL_SPRD_BANO.png b/apps/dm/public/images/map/layericon/TL_SPRD_BANO.png new file mode 100644 index 000000000..8d08dca29 Binary files /dev/null and b/apps/dm/public/images/map/layericon/TL_SPRD_BANO.png differ diff --git a/apps/dm/public/images/map/layericon/TL_SPRD_CRSRD.png b/apps/dm/public/images/map/layericon/TL_SPRD_CRSRD.png new file mode 100644 index 000000000..57b5f2ca5 Binary files /dev/null and b/apps/dm/public/images/map/layericon/TL_SPRD_CRSRD.png differ diff --git a/apps/dm/public/images/map/layericon/TL_SPRD_NONM.png b/apps/dm/public/images/map/layericon/TL_SPRD_NONM.png new file mode 100644 index 000000000..e68c05ab6 Binary files /dev/null and b/apps/dm/public/images/map/layericon/TL_SPRD_NONM.png differ diff --git a/apps/dm/public/images/map/layericon/TL_SPRD_RENM.png b/apps/dm/public/images/map/layericon/TL_SPRD_RENM.png new file mode 100644 index 000000000..4800beaeb Binary files /dev/null and b/apps/dm/public/images/map/layericon/TL_SPRD_RENM.png differ diff --git a/apps/dm/public/images/map/layericon/TL_SPRD_RWNM.png b/apps/dm/public/images/map/layericon/TL_SPRD_RWNM.png new file mode 100644 index 000000000..e68c05ab6 Binary files /dev/null and b/apps/dm/public/images/map/layericon/TL_SPRD_RWNM.png differ diff --git a/apps/dm/public/images/map/layericon/UFL_BMAN_PS.png b/apps/dm/public/images/map/layericon/UFL_BMAN_PS.png new file mode 100644 index 000000000..f2f64a641 Binary files /dev/null and b/apps/dm/public/images/map/layericon/UFL_BMAN_PS.png differ diff --git a/apps/dm/public/images/map/layericon/UFL_BOPC_PS.png b/apps/dm/public/images/map/layericon/UFL_BOPC_PS.png new file mode 100644 index 000000000..0f8e99f45 Binary files /dev/null and b/apps/dm/public/images/map/layericon/UFL_BOPC_PS.png differ diff --git a/apps/dm/public/images/map/layericon/UFL_BPIP_PS.png b/apps/dm/public/images/map/layericon/UFL_BPIP_PS.png new file mode 100644 index 000000000..dcc3622a7 Binary files /dev/null and b/apps/dm/public/images/map/layericon/UFL_BPIP_PS.png differ diff --git a/apps/dm/public/images/map/layericon/UFL_BPOL_PS.png b/apps/dm/public/images/map/layericon/UFL_BPOL_PS.png new file mode 100644 index 000000000..4f1102989 Binary files /dev/null and b/apps/dm/public/images/map/layericon/UFL_BPOL_PS.png differ diff --git a/apps/dm/public/images/map/layericon/UFL_BTRS_PS.png b/apps/dm/public/images/map/layericon/UFL_BTRS_PS.png new file mode 100644 index 000000000..557c17089 Binary files /dev/null and b/apps/dm/public/images/map/layericon/UFL_BTRS_PS.png differ diff --git a/apps/dm/public/images/map/layericon/UFL_GMNT_PS.png b/apps/dm/public/images/map/layericon/UFL_GMNT_PS.png new file mode 100644 index 000000000..6081d8b00 Binary files /dev/null and b/apps/dm/public/images/map/layericon/UFL_GMNT_PS.png differ diff --git a/apps/dm/public/images/map/layericon/UFL_GPIP_PS.png b/apps/dm/public/images/map/layericon/UFL_GPIP_PS.png new file mode 100644 index 000000000..5d7e86fd2 Binary files /dev/null and b/apps/dm/public/images/map/layericon/UFL_GPIP_PS.png differ diff --git a/apps/dm/public/images/map/layericon/UFL_GPRS_PS.png b/apps/dm/public/images/map/layericon/UFL_GPRS_PS.png new file mode 100644 index 000000000..79c120f20 Binary files /dev/null and b/apps/dm/public/images/map/layericon/UFL_GPRS_PS.png differ diff --git a/apps/dm/public/images/map/layericon/UFL_GVAL_PS.png b/apps/dm/public/images/map/layericon/UFL_GVAL_PS.png new file mode 100644 index 000000000..e8f7cde2e Binary files /dev/null and b/apps/dm/public/images/map/layericon/UFL_GVAL_PS.png differ diff --git a/apps/dm/public/images/map/layericon/UFL_KMAN_PS.png b/apps/dm/public/images/map/layericon/UFL_KMAN_PS.png new file mode 100644 index 000000000..764ce8ba5 Binary files /dev/null and b/apps/dm/public/images/map/layericon/UFL_KMAN_PS.png differ diff --git a/apps/dm/public/images/map/layericon/UFL_KPOL_PS.png b/apps/dm/public/images/map/layericon/UFL_KPOL_PS.png new file mode 100644 index 000000000..d9fa712cf Binary files /dev/null and b/apps/dm/public/images/map/layericon/UFL_KPOL_PS.png differ diff --git a/apps/dm/public/images/map/layericon/UFL_VALV_PS.png b/apps/dm/public/images/map/layericon/UFL_VALV_PS.png new file mode 100644 index 000000000..1b5d70178 Binary files /dev/null and b/apps/dm/public/images/map/layericon/UFL_VALV_PS.png differ diff --git a/apps/dm/public/images/map/layericon/WTL_FIRE_PS.png b/apps/dm/public/images/map/layericon/WTL_FIRE_PS.png new file mode 100644 index 000000000..4137a3f74 Binary files /dev/null and b/apps/dm/public/images/map/layericon/WTL_FIRE_PS.png differ diff --git a/apps/dm/public/images/map/layericon/WTL_FLOW_PS.png b/apps/dm/public/images/map/layericon/WTL_FLOW_PS.png new file mode 100644 index 000000000..285f7edfa Binary files /dev/null and b/apps/dm/public/images/map/layericon/WTL_FLOW_PS.png differ diff --git a/apps/dm/public/images/map/layericon/WTL_MANH_PS.png b/apps/dm/public/images/map/layericon/WTL_MANH_PS.png new file mode 100644 index 000000000..5f2a57cfe Binary files /dev/null and b/apps/dm/public/images/map/layericon/WTL_MANH_PS.png differ diff --git a/apps/dm/public/images/map/layericon/WTL_META_PS.png b/apps/dm/public/images/map/layericon/WTL_META_PS.png new file mode 100644 index 000000000..22c7af31b Binary files /dev/null and b/apps/dm/public/images/map/layericon/WTL_META_PS.png differ diff --git a/apps/dm/public/images/map/layericon/WTL_PIPE_PS.png b/apps/dm/public/images/map/layericon/WTL_PIPE_PS.png new file mode 100644 index 000000000..c9c6863f6 Binary files /dev/null and b/apps/dm/public/images/map/layericon/WTL_PIPE_PS.png differ diff --git a/apps/dm/public/images/map/layericon/WTL_RSRV_PS.png b/apps/dm/public/images/map/layericon/WTL_RSRV_PS.png new file mode 100644 index 000000000..d7b307a61 Binary files /dev/null and b/apps/dm/public/images/map/layericon/WTL_RSRV_PS.png differ diff --git a/apps/dm/public/images/map/layericon/WTL_RSRV_PS_BLACK.png b/apps/dm/public/images/map/layericon/WTL_RSRV_PS_BLACK.png new file mode 100644 index 000000000..395abeb78 Binary files /dev/null and b/apps/dm/public/images/map/layericon/WTL_RSRV_PS_BLACK.png differ diff --git a/apps/dm/public/images/map/layericon/WTL_RSRV_PS_GRAY.png b/apps/dm/public/images/map/layericon/WTL_RSRV_PS_GRAY.png new file mode 100644 index 000000000..33a667a1e Binary files /dev/null and b/apps/dm/public/images/map/layericon/WTL_RSRV_PS_GRAY.png differ diff --git a/apps/dm/public/images/map/layericon/WTL_RSRV_PS_GREEN.png b/apps/dm/public/images/map/layericon/WTL_RSRV_PS_GREEN.png new file mode 100644 index 000000000..33933b9e0 Binary files /dev/null and b/apps/dm/public/images/map/layericon/WTL_RSRV_PS_GREEN.png differ diff --git a/apps/dm/public/images/map/layericon/WTL_RSRV_PS_ORANGE.png b/apps/dm/public/images/map/layericon/WTL_RSRV_PS_ORANGE.png new file mode 100644 index 000000000..7a78c3466 Binary files /dev/null and b/apps/dm/public/images/map/layericon/WTL_RSRV_PS_ORANGE.png differ diff --git a/apps/dm/public/images/map/layericon/WTL_RSRV_PS_PURPLE.png b/apps/dm/public/images/map/layericon/WTL_RSRV_PS_PURPLE.png new file mode 100644 index 000000000..ef25607cf Binary files /dev/null and b/apps/dm/public/images/map/layericon/WTL_RSRV_PS_PURPLE.png differ diff --git a/apps/dm/public/images/map/layericon/WTL_RSRV_PS_RED.png b/apps/dm/public/images/map/layericon/WTL_RSRV_PS_RED.png new file mode 100644 index 000000000..26fd5f68e Binary files /dev/null and b/apps/dm/public/images/map/layericon/WTL_RSRV_PS_RED.png differ diff --git a/apps/dm/public/images/map/layericon/WTL_RSRV_PS_YELLOW.png b/apps/dm/public/images/map/layericon/WTL_RSRV_PS_YELLOW.png new file mode 100644 index 000000000..76718e865 Binary files /dev/null and b/apps/dm/public/images/map/layericon/WTL_RSRV_PS_YELLOW.png differ diff --git a/apps/dm/public/images/map/layericon/WTL_VALV_PS.png b/apps/dm/public/images/map/layericon/WTL_VALV_PS.png new file mode 100644 index 000000000..ca2b4788b Binary files /dev/null and b/apps/dm/public/images/map/layericon/WTL_VALV_PS.png differ diff --git a/apps/dm/public/images/map/layericon/default.png b/apps/dm/public/images/map/layericon/default.png new file mode 100644 index 000000000..92bf1f587 Binary files /dev/null and b/apps/dm/public/images/map/layericon/default.png differ diff --git a/apps/dm/public/images/map/layericon/reset.png b/apps/dm/public/images/map/layericon/reset.png new file mode 100644 index 000000000..45f889206 Binary files /dev/null and b/apps/dm/public/images/map/layericon/reset.png differ diff --git a/apps/dm/public/images/map/toolbar/arrow_down.png b/apps/dm/public/images/map/toolbar/arrow_down.png new file mode 100644 index 000000000..88590ecaf Binary files /dev/null and b/apps/dm/public/images/map/toolbar/arrow_down.png differ diff --git a/apps/dm/public/images/map/toolbar/arrow_right.png b/apps/dm/public/images/map/toolbar/arrow_right.png new file mode 100644 index 000000000..7090c37dd Binary files /dev/null and b/apps/dm/public/images/map/toolbar/arrow_right.png differ diff --git a/apps/dm/public/images/map/toolbar/backward_icon.png b/apps/dm/public/images/map/toolbar/backward_icon.png new file mode 100644 index 000000000..3a853475e Binary files /dev/null and b/apps/dm/public/images/map/toolbar/backward_icon.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_cont_area.png b/apps/dm/public/images/map/toolbar/btn_cont_area.png new file mode 100644 index 000000000..044562b55 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_cont_area.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_cont_area_on.png b/apps/dm/public/images/map/toolbar/btn_cont_area_on.png new file mode 100644 index 000000000..0129ff1bd Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_cont_area_on.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_cont_next.png b/apps/dm/public/images/map/toolbar/btn_cont_next.png new file mode 100644 index 000000000..075111dcf Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_cont_next.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_cont_next_on.png b/apps/dm/public/images/map/toolbar/btn_cont_next_on.png new file mode 100644 index 000000000..4fdbe3a00 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_cont_next_on.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_cont_prev.png b/apps/dm/public/images/map/toolbar/btn_cont_prev.png new file mode 100644 index 000000000..7e3fae7d2 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_cont_prev.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_cont_prev_on.png b/apps/dm/public/images/map/toolbar/btn_cont_prev_on.png new file mode 100644 index 000000000..fb2d55cfd Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_cont_prev_on.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_cont_print.png b/apps/dm/public/images/map/toolbar/btn_cont_print.png new file mode 100644 index 000000000..82596b83d Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_cont_print.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_cont_print_on.png b/apps/dm/public/images/map/toolbar/btn_cont_print_on.png new file mode 100644 index 000000000..bb7f27334 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_cont_print_on.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_cont_ruler.png b/apps/dm/public/images/map/toolbar/btn_cont_ruler.png new file mode 100644 index 000000000..501e460a4 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_cont_ruler.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_cont_ruler_on.png b/apps/dm/public/images/map/toolbar/btn_cont_ruler_on.png new file mode 100644 index 000000000..676a81bcc Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_cont_ruler_on.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_cont_save.png b/apps/dm/public/images/map/toolbar/btn_cont_save.png new file mode 100644 index 000000000..c057b9377 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_cont_save.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_cont_save_on.png b/apps/dm/public/images/map/toolbar/btn_cont_save_on.png new file mode 100644 index 000000000..bf8e6d623 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_cont_save_on.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_cont_sketch.png b/apps/dm/public/images/map/toolbar/btn_cont_sketch.png new file mode 100644 index 000000000..38a5f507f Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_cont_sketch.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_cont_sketch_on.png b/apps/dm/public/images/map/toolbar/btn_cont_sketch_on.png new file mode 100644 index 000000000..79fb54e03 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_cont_sketch_on.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_cont_zoomin.png b/apps/dm/public/images/map/toolbar/btn_cont_zoomin.png new file mode 100644 index 000000000..ac32fbab0 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_cont_zoomin.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_cont_zoomin_on.png b/apps/dm/public/images/map/toolbar/btn_cont_zoomin_on.png new file mode 100644 index 000000000..f258a965f Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_cont_zoomin_on.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_cont_zoomout.png b/apps/dm/public/images/map/toolbar/btn_cont_zoomout.png new file mode 100644 index 000000000..2ee0b5161 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_cont_zoomout.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_cont_zoomout_on.png b/apps/dm/public/images/map/toolbar/btn_cont_zoomout_on.png new file mode 100644 index 000000000..58dcbfdae Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_cont_zoomout_on.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_edit_add.png b/apps/dm/public/images/map/toolbar/btn_edit_add.png new file mode 100644 index 000000000..fe4f0993f Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_edit_add.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_edit_add_on.png b/apps/dm/public/images/map/toolbar/btn_edit_add_on.png new file mode 100644 index 000000000..37a836298 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_edit_add_on.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_edit_del.png b/apps/dm/public/images/map/toolbar/btn_edit_del.png new file mode 100644 index 000000000..3bb372ab9 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_edit_del.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_edit_del_on.png b/apps/dm/public/images/map/toolbar/btn_edit_del_on.png new file mode 100644 index 000000000..89e558739 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_edit_del_on.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_edit_next.png b/apps/dm/public/images/map/toolbar/btn_edit_next.png new file mode 100644 index 000000000..df4913c90 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_edit_next.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_edit_next_on.png b/apps/dm/public/images/map/toolbar/btn_edit_next_on.png new file mode 100644 index 000000000..4e5540b55 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_edit_next_on.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_edit_prev.png b/apps/dm/public/images/map/toolbar/btn_edit_prev.png new file mode 100644 index 000000000..e94bf6d42 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_edit_prev.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_edit_prev_on.png b/apps/dm/public/images/map/toolbar/btn_edit_prev_on.png new file mode 100644 index 000000000..8645dedfa Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_edit_prev_on.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_edit_save.png b/apps/dm/public/images/map/toolbar/btn_edit_save.png new file mode 100644 index 000000000..5f01dd55c Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_edit_save.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_edit_save_on.png b/apps/dm/public/images/map/toolbar/btn_edit_save_on.png new file mode 100644 index 000000000..b035687d6 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_edit_save_on.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_edit_slt.png b/apps/dm/public/images/map/toolbar/btn_edit_slt.png new file mode 100644 index 000000000..aaf2bbd08 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_edit_slt.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_edit_slt_on.png b/apps/dm/public/images/map/toolbar/btn_edit_slt_on.png new file mode 100644 index 000000000..02d035a74 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_edit_slt_on.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_edit_snap.png b/apps/dm/public/images/map/toolbar/btn_edit_snap.png new file mode 100644 index 000000000..fc91d4304 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_edit_snap.png differ diff --git a/apps/dm/public/images/map/toolbar/btn_edit_snap_on.png b/apps/dm/public/images/map/toolbar/btn_edit_snap_on.png new file mode 100644 index 000000000..f856d8fbe Binary files /dev/null and b/apps/dm/public/images/map/toolbar/btn_edit_snap_on.png differ diff --git a/apps/dm/public/images/map/toolbar/close_icon.png b/apps/dm/public/images/map/toolbar/close_icon.png new file mode 100644 index 000000000..e8e76d5d7 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/close_icon.png differ diff --git a/apps/dm/public/images/map/toolbar/ico_gis_sprite.png b/apps/dm/public/images/map/toolbar/ico_gis_sprite.png new file mode 100644 index 000000000..0ee4b5985 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/ico_gis_sprite.png differ diff --git a/apps/dm/public/images/map/toolbar/icon-viewclose.png b/apps/dm/public/images/map/toolbar/icon-viewclose.png new file mode 100644 index 000000000..d93ce9c05 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/icon-viewclose.png differ diff --git a/apps/dm/public/images/map/toolbar/img_base_map.png b/apps/dm/public/images/map/toolbar/img_base_map.png new file mode 100644 index 000000000..7f96fcf5e Binary files /dev/null and b/apps/dm/public/images/map/toolbar/img_base_map.png differ diff --git a/apps/dm/public/images/map/toolbar/img_network_map.png b/apps/dm/public/images/map/toolbar/img_network_map.png new file mode 100644 index 000000000..df80af6dc Binary files /dev/null and b/apps/dm/public/images/map/toolbar/img_network_map.png differ diff --git a/apps/dm/public/images/map/toolbar/img_road_map.png b/apps/dm/public/images/map/toolbar/img_road_map.png new file mode 100644 index 000000000..02d015d12 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/img_road_map.png differ diff --git a/apps/dm/public/images/map/toolbar/img_settle_map.png b/apps/dm/public/images/map/toolbar/img_settle_map.png new file mode 100644 index 000000000..1a93f50e8 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/img_settle_map.png differ diff --git a/apps/dm/public/images/map/toolbar/loading.gif b/apps/dm/public/images/map/toolbar/loading.gif new file mode 100644 index 000000000..7c3683bb4 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/loading.gif differ diff --git a/apps/dm/public/images/map/toolbar/map_pin.png b/apps/dm/public/images/map/toolbar/map_pin.png new file mode 100644 index 000000000..b6cc2f6aa Binary files /dev/null and b/apps/dm/public/images/map/toolbar/map_pin.png differ diff --git a/apps/dm/public/images/map/toolbar/toggle_off.png b/apps/dm/public/images/map/toolbar/toggle_off.png new file mode 100644 index 000000000..d5dea9f67 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/toggle_off.png differ diff --git a/apps/dm/public/images/map/toolbar/toggle_on.png b/apps/dm/public/images/map/toolbar/toggle_on.png new file mode 100644 index 000000000..9cb573875 Binary files /dev/null and b/apps/dm/public/images/map/toolbar/toggle_on.png differ diff --git a/apps/dm/public/img/wrAbout/icon_01.png b/apps/dm/public/img/wrAbout/icon_01.png new file mode 100644 index 000000000..30eff5745 Binary files /dev/null and b/apps/dm/public/img/wrAbout/icon_01.png differ diff --git a/apps/dm/public/img/wrAbout/icon_02.png b/apps/dm/public/img/wrAbout/icon_02.png new file mode 100644 index 000000000..36401729f Binary files /dev/null and b/apps/dm/public/img/wrAbout/icon_02.png differ diff --git a/apps/dm/public/img/wrAbout/icon_03.png b/apps/dm/public/img/wrAbout/icon_03.png new file mode 100644 index 000000000..87f48c444 Binary files /dev/null and b/apps/dm/public/img/wrAbout/icon_03.png differ diff --git a/apps/dm/public/img/wrAbout/img_01_1.png b/apps/dm/public/img/wrAbout/img_01_1.png new file mode 100644 index 000000000..9aab4e824 Binary files /dev/null and b/apps/dm/public/img/wrAbout/img_01_1.png differ diff --git a/apps/dm/public/img/wrAbout/img_02_1.png b/apps/dm/public/img/wrAbout/img_02_1.png new file mode 100644 index 000000000..3d61c6e5d Binary files /dev/null and b/apps/dm/public/img/wrAbout/img_02_1.png differ diff --git a/apps/dm/public/img/wrAbout/img_02_2.png b/apps/dm/public/img/wrAbout/img_02_2.png new file mode 100644 index 000000000..c8283053e Binary files /dev/null and b/apps/dm/public/img/wrAbout/img_02_2.png differ diff --git a/apps/dm/public/img/wrAbout/img_02_3.png b/apps/dm/public/img/wrAbout/img_02_3.png new file mode 100644 index 000000000..f5ba9d81e Binary files /dev/null and b/apps/dm/public/img/wrAbout/img_02_3.png differ diff --git a/apps/dm/public/img/wrAbout/img_03_1.png b/apps/dm/public/img/wrAbout/img_03_1.png new file mode 100644 index 000000000..a7534ae68 Binary files /dev/null and b/apps/dm/public/img/wrAbout/img_03_1.png differ diff --git a/apps/dm/public/img/wrAbout/img_03_2.png b/apps/dm/public/img/wrAbout/img_03_2.png new file mode 100644 index 000000000..6b4faca7d Binary files /dev/null and b/apps/dm/public/img/wrAbout/img_03_2.png differ diff --git a/apps/dm/public/img/wrAbout/img_04_1.png b/apps/dm/public/img/wrAbout/img_04_1.png new file mode 100644 index 000000000..a160072bd Binary files /dev/null and b/apps/dm/public/img/wrAbout/img_04_1.png differ diff --git a/apps/dm/public/img/wrAbout/img_04_2.png b/apps/dm/public/img/wrAbout/img_04_2.png new file mode 100644 index 000000000..374fc55bf Binary files /dev/null and b/apps/dm/public/img/wrAbout/img_04_2.png differ diff --git a/apps/dm/public/img/wrAbout/img_05_1.png b/apps/dm/public/img/wrAbout/img_05_1.png new file mode 100644 index 000000000..1418fa92d Binary files /dev/null and b/apps/dm/public/img/wrAbout/img_05_1.png differ diff --git a/apps/dm/public/img/wrAbout/img_05_2.png b/apps/dm/public/img/wrAbout/img_05_2.png new file mode 100644 index 000000000..0c2f92396 Binary files /dev/null and b/apps/dm/public/img/wrAbout/img_05_2.png differ diff --git a/apps/dm/public/img/wrAbout/img_06_1.png b/apps/dm/public/img/wrAbout/img_06_1.png new file mode 100644 index 000000000..08acf4971 Binary files /dev/null and b/apps/dm/public/img/wrAbout/img_06_1.png differ diff --git a/apps/dm/public/img/wrAbout/img_06_2.png b/apps/dm/public/img/wrAbout/img_06_2.png new file mode 100644 index 000000000..19b150b7c Binary files /dev/null and b/apps/dm/public/img/wrAbout/img_06_2.png differ diff --git a/apps/dm/public/img/wrExample/icon_01.png b/apps/dm/public/img/wrExample/icon_01.png new file mode 100644 index 000000000..30eff5745 Binary files /dev/null and b/apps/dm/public/img/wrExample/icon_01.png differ diff --git a/apps/dm/public/img/wrExample/icon_02.png b/apps/dm/public/img/wrExample/icon_02.png new file mode 100644 index 000000000..36401729f Binary files /dev/null and b/apps/dm/public/img/wrExample/icon_02.png differ diff --git a/apps/dm/public/img/wrExample/icon_03.png b/apps/dm/public/img/wrExample/icon_03.png new file mode 100644 index 000000000..87f48c444 Binary files /dev/null and b/apps/dm/public/img/wrExample/icon_03.png differ diff --git a/apps/dm/public/img/wrExample/img_01_1.png b/apps/dm/public/img/wrExample/img_01_1.png new file mode 100644 index 000000000..9aab4e824 Binary files /dev/null and b/apps/dm/public/img/wrExample/img_01_1.png differ diff --git a/apps/dm/public/img/wrExample/img_02_1.png b/apps/dm/public/img/wrExample/img_02_1.png new file mode 100644 index 000000000..3d61c6e5d Binary files /dev/null and b/apps/dm/public/img/wrExample/img_02_1.png differ diff --git a/apps/dm/public/img/wrExample/img_02_2.png b/apps/dm/public/img/wrExample/img_02_2.png new file mode 100644 index 000000000..c8283053e Binary files /dev/null and b/apps/dm/public/img/wrExample/img_02_2.png differ diff --git a/apps/dm/public/img/wrExample/img_02_3.png b/apps/dm/public/img/wrExample/img_02_3.png new file mode 100644 index 000000000..f5ba9d81e Binary files /dev/null and b/apps/dm/public/img/wrExample/img_02_3.png differ diff --git a/apps/dm/public/img/wrExample/img_03_1.png b/apps/dm/public/img/wrExample/img_03_1.png new file mode 100644 index 000000000..a7534ae68 Binary files /dev/null and b/apps/dm/public/img/wrExample/img_03_1.png differ diff --git a/apps/dm/public/img/wrExample/img_03_2.png b/apps/dm/public/img/wrExample/img_03_2.png new file mode 100644 index 000000000..6b4faca7d Binary files /dev/null and b/apps/dm/public/img/wrExample/img_03_2.png differ diff --git a/apps/dm/public/img/wrExample/img_04_1.png b/apps/dm/public/img/wrExample/img_04_1.png new file mode 100644 index 000000000..a160072bd Binary files /dev/null and b/apps/dm/public/img/wrExample/img_04_1.png differ diff --git a/apps/dm/public/img/wrExample/img_04_2.png b/apps/dm/public/img/wrExample/img_04_2.png new file mode 100644 index 000000000..374fc55bf Binary files /dev/null and b/apps/dm/public/img/wrExample/img_04_2.png differ diff --git a/apps/dm/public/img/wrExample/img_05_1.png b/apps/dm/public/img/wrExample/img_05_1.png new file mode 100644 index 000000000..1418fa92d Binary files /dev/null and b/apps/dm/public/img/wrExample/img_05_1.png differ diff --git a/apps/dm/public/img/wrExample/img_05_2.png b/apps/dm/public/img/wrExample/img_05_2.png new file mode 100644 index 000000000..0c2f92396 Binary files /dev/null and b/apps/dm/public/img/wrExample/img_05_2.png differ diff --git a/apps/dm/public/img/wrExample/img_06_1.png b/apps/dm/public/img/wrExample/img_06_1.png new file mode 100644 index 000000000..08acf4971 Binary files /dev/null and b/apps/dm/public/img/wrExample/img_06_1.png differ diff --git a/apps/dm/public/img/wrExample/img_06_2.png b/apps/dm/public/img/wrExample/img_06_2.png new file mode 100644 index 000000000..19b150b7c Binary files /dev/null and b/apps/dm/public/img/wrExample/img_06_2.png differ diff --git a/apps/dm/public/img/wrExample/img_arrow_down_1.png b/apps/dm/public/img/wrExample/img_arrow_down_1.png new file mode 100644 index 000000000..9e8fb76c6 Binary files /dev/null and b/apps/dm/public/img/wrExample/img_arrow_down_1.png differ diff --git a/apps/dm/public/img/wrExample/img_arrow_down_2.png b/apps/dm/public/img/wrExample/img_arrow_down_2.png new file mode 100644 index 000000000..2002bb607 Binary files /dev/null and b/apps/dm/public/img/wrExample/img_arrow_down_2.png differ diff --git a/apps/dm/public/img/wrExample/img_ex_01_1.png b/apps/dm/public/img/wrExample/img_ex_01_1.png new file mode 100644 index 000000000..9d6bfb002 Binary files /dev/null and b/apps/dm/public/img/wrExample/img_ex_01_1.png differ diff --git a/apps/dm/public/img/wrExample/img_ex_02_1.png b/apps/dm/public/img/wrExample/img_ex_02_1.png new file mode 100644 index 000000000..70c1f8ed2 Binary files /dev/null and b/apps/dm/public/img/wrExample/img_ex_02_1.png differ diff --git a/apps/dm/public/img/wrExample/img_ex_03_1.png b/apps/dm/public/img/wrExample/img_ex_03_1.png new file mode 100644 index 000000000..c981fa9fb Binary files /dev/null and b/apps/dm/public/img/wrExample/img_ex_03_1.png differ diff --git a/apps/dm/public/img/wrExample/img_ex_04_1.png b/apps/dm/public/img/wrExample/img_ex_04_1.png new file mode 100644 index 000000000..f5547820e Binary files /dev/null and b/apps/dm/public/img/wrExample/img_ex_04_1.png differ diff --git a/apps/dm/public/img/wrExample/img_ex_04_2.png b/apps/dm/public/img/wrExample/img_ex_04_2.png new file mode 100644 index 000000000..b102942ee Binary files /dev/null and b/apps/dm/public/img/wrExample/img_ex_04_2.png differ diff --git a/apps/dm/public/img/wrExample/img_ex_04_3.png b/apps/dm/public/img/wrExample/img_ex_04_3.png new file mode 100644 index 000000000..50ca8045f Binary files /dev/null and b/apps/dm/public/img/wrExample/img_ex_04_3.png differ diff --git a/apps/dm/public/img/wrExample/img_ex_04_4.png b/apps/dm/public/img/wrExample/img_ex_04_4.png new file mode 100644 index 000000000..6e6d0c294 Binary files /dev/null and b/apps/dm/public/img/wrExample/img_ex_04_4.png differ diff --git a/apps/dm/public/img/wrOverview/img_ov_01_1.png b/apps/dm/public/img/wrOverview/img_ov_01_1.png new file mode 100644 index 000000000..e59a10192 Binary files /dev/null and b/apps/dm/public/img/wrOverview/img_ov_01_1.png differ diff --git a/apps/dm/public/img/wrStatus/icon_favorit_off.png b/apps/dm/public/img/wrStatus/icon_favorit_off.png new file mode 100644 index 000000000..4de0b3ebc Binary files /dev/null and b/apps/dm/public/img/wrStatus/icon_favorit_off.png differ diff --git a/apps/dm/public/img/wrStatus/icon_favorit_on.png b/apps/dm/public/img/wrStatus/icon_favorit_on.png new file mode 100644 index 000000000..f2edfa4e9 Binary files /dev/null and b/apps/dm/public/img/wrStatus/icon_favorit_on.png differ diff --git a/apps/dm/public/img/wrStatus/icon_right.png b/apps/dm/public/img/wrStatus/icon_right.png new file mode 100644 index 000000000..fef56226c Binary files /dev/null and b/apps/dm/public/img/wrStatus/icon_right.png differ diff --git a/apps/dm/public/img/wrStatus/southKoreaMap.svg b/apps/dm/public/img/wrStatus/southKoreaMap.svg new file mode 100644 index 000000000..7b3c170e3 --- /dev/null +++ b/apps/dm/public/img/wrStatus/southKoreaMap.svg @@ -0,0 +1,19 @@ +<svg width="506" height="785" viewBox="0 0 506 785" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path id="전라북도" d="M273.567 425.516L274.188 429.893L276.776 436.366L272.497 444.123L262.388 453.507L253.164 454.538L241.456 466.408L234.164 484.969L234.199 489.884L228.425 500.23L234.578 510.92L233.336 516.076L236.637 519.971L228.736 529.825L229.207 534.786L222.445 540.171L212.531 530.741L203.135 532.139L199.017 538.372L191.864 536.768L186.861 538.819L178.752 536.126L178.43 533.686L170.644 532.093L159.695 536.699L152.358 532.517L155.785 526.261L152.174 521.77L153.301 515.491L150.678 509.923L146.09 512.799L143.398 518.848L135.29 516.167L133.266 509.43L123.122 503.816L114.117 508.754L114.416 513.612L109.16 518.184L110.057 521.77L99.1308 527.9L94.1739 524.096L89.24 525.711L90.4706 530.077L82.9604 529.126L82.5924 520.2L79.1881 518.573L78.9811 511.447L73.0925 508.709L79.9126 497.056L93.1848 491.912L98.1302 491.935L102.064 487.913L106.906 492.153L107.423 488.944L103.754 484.98L84.6165 484.969L78.9811 480.489L80.7752 474.141L98.1762 464.196L104.306 451.112L124.123 456.68L124.364 451.33L120.201 449.978L113.634 443.859L122.673 440.548L127.63 441.453L130.552 436.286L130.149 434.155L121.098 437.409L112.541 435.621L109.608 438.749L102.627 436.057L103.168 425.779L97.0031 422.823L98.6938 419.707L117.038 419.237L140.604 407.436L141.846 399.336L147.608 396.54L155.164 396.987L161.248 399.45L167.044 409.808L173.347 412.283L175.969 408.822L181.513 407.757L188.08 409.132L193.911 406.187L199.305 404.835L211.634 423.27L214.555 422.606L217.936 428.438L226.436 427.853L227.85 421.93L235.429 423.018L236.499 426.776L240.11 422.606L239.455 417.221L243.837 416.854L245.033 420.097L246.447 420.452L247.678 417.897L250.461 417.771L253.969 423.362L256.798 423.408L260.49 425.837L270.254 423.305L273.567 425.516Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="서울특별시" d="M200.823 161.685L192.036 164.378L194.072 156.747L192.485 153.378L193.393 150.686L190.909 149.792L191.588 144.407L187.309 142.391L180.777 141.039L177.177 150.021L175.153 150.239L172.22 146.195L167.24 149.403L165.687 156.747L152.829 155.315L150.828 157.641L147.895 163.473L153.531 165.054L154.209 170.427L152.852 171.55L153.531 174.919L158.706 174.243L162.985 182.32L167.263 181.873L171.093 183.225L181.007 179.41L181.455 182.549L182.352 184.566L189.333 181.209H191.818L194.072 179.628L196.993 178.287L195.187 173.349L200.823 167.07V161.685Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="인천광역시" d="M126.722 162.453L125.376 165.054L126.285 167.517L129.068 166.875L130.782 169.98L126.423 172.1L128.079 180.303L131.702 186.914L140.259 190.214L146.768 178.951L143.398 173.578L143.168 168.411H145.422L147.895 163.472L150.828 157.641L146.55 157.423L134.381 146.871L130.103 148.899V150.468L123.582 153.608L126.722 162.453Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="경기도" d="M286.172 172.455L282.618 168.537L274.475 166.394L258.259 155.177H251.036L253.521 145.977L246.769 140.363L252.393 134.313L252.163 123.715L263.653 116.359L262.756 104.466L256.235 100.88L254.613 97.1337L244.285 90.3394L245.194 80.463L243.837 75.5249L228.299 78.2174L224.469 65.8777L210.725 60.275L199.029 45.0137L185.504 55.3368L181.904 64.984L169.517 74.4021L165.687 81.3567L155.336 89.6634L146.55 93.9256L142.72 111.421L133.933 122.42L120.201 123.543L118.165 152.255L123.582 153.607L130.103 150.468V148.898L134.381 146.87L146.55 157.423L150.828 157.64L152.829 155.315L165.687 156.747L167.24 149.403L172.22 146.194L175.153 150.239L177.177 150.021L180.777 141.039L187.309 142.391L191.588 144.407L190.909 149.792L193.393 150.686L192.485 153.378L194.072 156.747L192.036 164.377L200.823 161.685V167.07L195.187 173.349L196.993 178.287L194.072 179.627L191.818 181.208H189.333L182.352 184.566L181.455 182.549L181.007 179.41L171.093 183.225L167.263 181.873L162.985 182.32L158.706 174.242L153.531 174.918L152.852 171.55L154.209 170.427L153.531 165.053L147.895 163.472L145.422 168.41H143.168L143.398 173.578L146.768 178.951L140.258 190.214L135.405 195.748L153.082 208.569L146.09 211.937L138.211 210.815L129.206 212.167L130.552 216.876L127.182 223.83L130.782 226.305L129.885 230.785L138.441 228.539L142.49 224.953L147.447 228.986L141.593 231.232L142.49 242.678L145.422 249.403L144.065 254.124L149.252 260.62L154.209 273.418L163.859 268.698L177.844 266.91L186.642 262.648L195.866 266.234L206.228 275.435L209.161 270.496L216.993 268.847L235.061 253.677L244.745 255.464L256.177 245.198L259.375 238.416L263.078 241.417L265.873 236.25L271.543 231.908L272.21 222.261L275.142 219.121L275.372 204.765L281.595 187.659L281.226 179.857L284.378 177.164L286.172 172.455Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-miterlimit="10"/> +<path id="강원특별자치도" d="M210.047 41.6452L215.912 42.3212L220.858 38.2882L231.669 45.2429L235.05 40.7515L253.291 44.3377L274.234 40.0755L292.256 42.3212L300.583 47.7062L308.472 45.4606L320.629 47.0302L337.064 46.5834L349.462 40.3047L358.916 29.7638L363.873 7.99466L369.612 1.81909L372.545 7.88009L377.502 18.4324L378.399 28.9733L386.622 42.7681L396.754 72.1679L401.481 87.6469L412.971 107.617L425.357 133.419L450.579 165.054L449.452 169.98L458.009 180.533L456.882 187.029L483.23 235.952L486.83 254.101L472.419 262.648L463 270.726L451.258 261.972L442.022 264.665L429.406 259.956L421.079 267.586L409.141 259.727L404.863 267.357L392.027 262.946L386.622 260.254L380.538 259.807L370.635 251.649H359.813L353.062 244.775L341.572 244.694L348.105 233.707L323.102 230.785L312.072 236.399L307.564 226.076L296.534 225.893L292.923 238.267L279.639 239.092L274.464 240.432L271.543 231.908L272.21 222.261L275.143 219.121L275.373 204.765L281.595 187.659L281.227 179.857L284.378 177.164L286.172 172.455L282.618 168.537L274.475 166.394L258.248 155.177H251.036L253.521 145.977L246.758 140.363L252.394 134.313L252.164 123.715L263.653 116.359L262.756 104.466L256.235 100.88L254.613 97.1337L244.285 90.3394L245.194 80.4631L243.837 75.5249L228.299 78.2174L224.458 65.8777L210.725 60.275L199.029 45.0137L210.047 41.6452Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="경상북도" d="M481.264 504.297L493.903 466.82L494.57 458.742L501.551 447.972L504.346 437.535L500.206 434.052L496.594 439.219L484.012 447.205L476.813 439.322L483.759 430.019L480.378 392.77L488.268 377.074L489.843 341.395L497.721 321.196L492.995 303.482V271.619L486.83 254.101L472.419 262.648L463 270.726L451.257 261.972L442.022 264.665L429.405 259.956L421.079 267.586L409.141 259.726L404.862 267.357L392.027 262.946L382.194 265.111L374.902 272.742L364.77 277.99L358.479 289.206L361.17 295.588L350.163 302.841L333.464 290.467L327 298.556L319.295 295.623L314.763 298.544L308.725 298.922L304.424 307.378L308.898 313.599L304.286 315.616L299.685 311.422L282.871 316.945L289.243 323.155L288.012 328.471L281.893 323.522L270.254 330.304L279.053 336.205L283.446 343.733L279.421 348.671L277.845 365.605L279.869 370.176L273.567 377.612L273.498 381.783L278.075 384.762L280.41 380.912L287.598 388.382L297.995 386.87L299.801 398.155L290.519 397.491L290.002 406.92L282.791 421.804L273.636 425.527L274.188 429.893L276.775 436.366L272.497 444.123L279.041 453.357L291.209 459.602L300.479 459.682L303.492 466.809L311.612 474.829L312.693 483.995L306.586 488.841L312.82 492.84L330.393 492.164L339.56 499.164L335.5 483.903L343.067 485.702L344.689 483.181L339.111 476.272L343.389 468.424L350.14 467.53L352.004 464.471L340.077 458.96L347.886 446.437L354.097 443.894L352.165 452.269L355.546 454.286L358.479 452.04L362.079 440.823L369.957 439.7L375.592 437.237L382.803 436.332L388.301 439.104L389.474 443.951L389.784 450.883L391.13 454.068L388.784 462.535L384.598 463.257L383.24 466.408L382.93 470.681L379.871 474.027L380.101 477.178L378.513 484.35L369.06 485.702V482.104L361.618 482.631L355.316 490.697L358.099 499.313L387.3 511.252L404.264 502.006L418.675 505.81L422.033 502.716L430.372 501.376L435.674 491.568L446.358 490.617L456.939 494.421L455.593 500.929L462.126 502.728L466.853 500.482L473.592 500.253L481.264 504.297Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="충청북도" d="M274.464 240.432L279.639 239.092L292.923 238.267L296.534 225.893L307.564 226.042L312.072 236.399L323.102 230.785L348.105 233.707L341.572 244.694L353.062 244.774L359.813 251.649H370.635L380.538 259.807L386.622 260.253L392.027 262.946L382.194 265.203L374.902 272.742L364.77 277.99L358.479 289.206L361.17 295.588L350.164 302.841L333.464 290.467L327 298.556L319.295 295.623L314.763 298.544L308.725 298.922L304.424 307.378L308.898 313.599L304.286 315.616L299.686 311.423L282.871 316.945L289.243 323.155L288.012 328.471L281.893 323.522L270.254 330.304L279.053 336.205L283.446 343.733L279.421 348.671L277.845 365.605L279.869 370.176L273.567 377.612L273.498 381.783L278.075 384.762L280.41 380.912L287.598 388.382L297.995 386.87L299.801 398.155L290.462 397.594L290.002 406.92L282.791 421.804L273.567 425.608L270.254 423.305L260.49 425.837L256.798 423.408L253.969 423.362L250.461 417.771L247.678 417.897L246.447 420.452L245.033 420.096L243.837 416.854L240.122 412.225L238.144 402.67L240.053 401.467L237.994 394.191L233.014 385.896L226.505 385.415L224.584 382.172L231.554 362.42L234.946 359.75L234.268 357.046L228.954 356.084V350.527L225.93 346.047L221.18 349.988L217.821 349.037L216.016 343.32L212.013 342.713V335.586L209.863 332.607L210.058 330.866L207.091 327.818L203.71 320.852L201.007 318.618L204.561 309.234L213.819 300.779L219.581 301.626L222.042 298.315L210.323 278.517L206.228 275.435L209.161 270.496L216.993 268.847L235.061 253.677L244.745 255.464L256.177 245.198L259.375 238.416L263.078 241.417L265.873 236.25L271.543 231.908L274.464 240.432Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="충청남도" d="M226.505 385.415L219.397 392.942L215.567 391.04L214.762 388.038L212.197 386.434V380.603L210.392 380.144L209.092 385.713L204.308 392.266L198.81 386.331L198.672 379.927L195.164 377.521L195.682 371.322L198.672 367.587L197.994 365.502L200.052 356.118L194.244 346.207L197.407 341.727L194.681 339.997L192.899 329.835L185.239 318.835L187.493 311.434L185.239 304.479L188.206 302.841L195.153 306.496L204.561 309.234L213.819 300.778L219.581 301.626L222.042 298.315L210.323 278.517L206.228 275.435L195.866 266.234L186.642 262.648L177.844 266.91L163.859 268.698L154.209 273.418L145.917 273.132L137.13 255.418L131.955 258.317L129.609 252.898L118.66 250.251L107.4 241.498L103.685 241.624L100.465 246.103L101.443 252.004L103.478 257.126L102.374 257.882L98.1419 250.664L91.6668 247.604L85.6403 247.364L80.6259 250.48L86.2844 253.746L82.857 255.877L87.9865 260.047L90.3672 260.849L90.7928 263.748L84.5707 269.568L76.198 280.281L72.3681 278.425L78.5787 265.501L77.0375 263.702L78.1301 256.163L76.1405 253.299L71.5861 267.128L68.6878 268.812L65.0765 263.324L60.8211 265.936L61.2696 273.968L59.8435 275.606L56.6922 274.827L52.6554 277.898L47.7214 291.097L48.1585 292.506L51.8618 292.323L52.0228 286.147L57.3593 288.565L60.108 293.606L52.9659 295.863L52.8969 298.762L58.6474 299.931L62.8338 298.498L64.858 295.313L68.7108 294.992L67.3997 300.767L71.3675 304.342L69.0558 313.29L73.1732 319.145L74.1853 344.958L85.3643 346.952L79.0272 321.413L81.2354 317.483L75.2089 312.843L76.888 311.044L77.7966 304.433L79.7173 302.99L81.799 311.56L86.0313 314.653L88.9871 312.797L91.9544 306.965L93.668 308.421L91.8739 316.681L95.7037 343.606L92.9664 349.782L100.879 357.287V359.292L94.2776 362.053L97.0493 382.86L89.7692 391.052L90.9883 393.618L94.105 391.04L101.949 393.756L107.964 401.1L106.779 403.77L111.702 416.281L117.038 419.237L140.604 407.436L141.846 399.336L147.608 396.54L155.164 396.987L161.248 399.45L167.102 409.773L173.405 412.237L175.969 408.822L181.513 407.757L188.08 409.132L193.911 406.187L199.305 404.835L211.634 423.27L214.555 422.606L217.937 428.437L226.436 427.853L227.85 421.93L235.43 423.018L236.499 426.776L240.11 422.606L239.455 417.221L243.837 416.854L240.122 412.225L238.144 402.67L240.053 401.467L237.994 394.191L233.014 385.896L226.505 385.415Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="세종특별자치시" d="M204.561 309.234L201.007 318.618L203.71 320.852L207.091 327.818L210.058 330.866L209.863 332.607L212.013 335.586V342.713L210.84 349.633L209.253 350.928L207.919 354.8L200.052 356.118L194.244 346.207L197.407 341.727L194.681 339.997L192.899 329.835L185.239 318.835L187.493 311.434L185.239 304.479L188.206 302.841L195.153 306.496L204.561 309.234Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="대전광역시" d="M200.052 356.118L207.919 354.8L209.253 350.928L210.84 349.633L212.013 342.713L216.016 343.32L217.821 349.037L221.18 349.988L225.93 346.047L228.954 350.527V356.084L234.268 357.046L234.946 359.75L231.554 362.42L224.584 382.172L226.505 385.415L219.397 392.942L215.567 391.04L214.762 388.039L212.197 386.435V380.603L210.392 380.144L209.092 385.713L204.308 392.266L198.81 386.331L198.672 379.927L195.164 377.521L195.682 371.322L198.672 367.587L197.994 365.502L200.052 356.118Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="대구광역시" d="M361.619 482.631L369.06 482.104V485.702L378.514 484.35L380.101 477.178L379.871 474.027L382.93 470.681L383.24 466.408L384.598 463.257L388.784 462.535L391.13 454.068L389.785 450.883L389.474 443.951L388.301 439.105L382.803 436.332L375.592 437.237L369.957 439.7L362.079 440.823L358.479 452.04L355.546 454.286L352.165 452.269L354.097 443.894L347.886 446.437L340.077 458.96L352.004 464.471L350.141 467.53L343.389 468.424L339.111 476.273L344.689 483.181L343.067 485.702L335.5 483.903L339.56 499.164L350.819 496.472L355.316 490.697L361.619 482.631Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="울산광역시" d="M459.136 556.349L456.663 554.332L454.19 547.377L446.496 545.327L444.161 538.326L439.204 536.699L432.361 528.06L420.515 521.724L416.858 516.465L422.769 511.183L422.033 502.716L430.372 501.376L435.674 491.568L446.358 490.617L456.939 494.421L455.593 500.929L462.126 502.728L466.853 500.482L473.592 500.253L481.264 504.297L481.206 506.761L483.012 511.252L481.666 515.743L481.885 522.469L478.963 528.977L474.455 532.116L473.109 526.731L471.683 524.13L471.752 531.669L465.898 535.702L467.704 538.624L467.474 544.685L465.001 548.718L466.347 550.517L462.597 552.797L459.136 556.349Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="부산광역시" d="M446.496 545.327L454.19 547.378L456.663 554.332L459.136 556.349L453.857 558.216L449.233 567.336L449.452 570.258L438.871 583.503L432.338 581.922L429.187 585.737L430.993 588.876L430.935 591.569L426.484 591.351L423.103 587.983L415.673 597.412L412.522 595.384L411.395 600.322L408.244 601.216L406.887 596.954L408.692 585.29L402.93 591.5L399.434 589.151L395.409 592.245L391.004 591.122L391.13 586.722L386.909 580.959L403.908 570.682L411.959 570.854L416.203 568.253L418.077 564.197L433.132 556.039L435.754 549.554L443.046 549.99L446.496 545.327Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="경상남도" d="M272.497 444.123L262.388 453.506L253.164 454.538L241.456 466.408L234.164 484.969L234.199 489.884L228.425 500.23L234.578 510.92L233.336 516.076L236.637 519.971L228.736 529.825L229.207 534.786L222.445 540.171L225.907 549.085L227.712 563.888L233.589 569.742L239.639 582.506L245.251 586.676L246.769 593.001L242.652 598.684L255.2 603.026L258.892 602.797L264.838 597.286L265.321 595.075L267.793 596.198L268.242 598.89L272.095 597.676L274.326 598.89L277.132 597.263L274.498 590.824L276.799 589.919L278.846 590.492L280.145 586.39L285.091 584.946L282.434 589.243L282.216 599.784L280.881 606.509L289.795 611.218L291.968 615.297L293.245 615.939L297.098 614.243L299.778 615.492L301.319 615.263L303.849 609.076L309.692 609.557L312.613 614.369L316.742 612.376L320.042 607.185L322.297 613.693L317.041 616.088L318.248 619.525L323.067 621.003L324.999 619.525L327.024 621.278L326.794 628.725L321.492 627.373L320.042 628.954L324.585 632.793L327.208 639.369L329.738 640.538L332.061 638.636L333.165 632.735L331.854 628.806L334.787 627.488L338.628 620.155L336.259 617.222L333.947 621.324L331.808 620.419L334.545 606.532L334.235 603.828L341.216 602.476L344.252 598.936L338.962 593.952L330.635 595.522L328.887 598.214L325.448 598.661L328.312 593.345L340.296 589.403L339.18 585.794L351.578 584.981L354.12 586.699L355.857 594.181L361.998 591.706L355.857 572.194L359.422 567.565L361.032 568.379L359.917 574.841L362.159 579.813L366.645 580.478L369.807 584.076L371.21 580.26L373.189 581.841L372.2 587.318L377.697 589.69L385.196 588.384L386.035 590.583L391.004 591.122L391.13 586.722L386.909 580.959L403.908 570.682L411.924 570.842L416.202 568.253L418.077 564.197L433.097 556.039L435.754 549.554L443.011 549.978L446.496 545.327L444.161 538.326L439.204 536.699L432.361 528.06L420.515 521.724L416.858 516.465L422.77 511.183L422.033 502.716L418.675 505.81L404.264 502.006L387.3 511.252L358.099 499.313L355.316 490.697L350.647 496.598L339.56 499.164L330.393 492.164L312.843 492.771L306.586 488.841L312.693 483.995L311.612 474.829L303.492 466.809L300.479 459.682L291.209 459.602L279.041 453.358L272.497 444.123Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="전라남도" d="M238.224 616.271L235.165 615.859L225.619 619.64L224.043 615.824L217.81 608.721L222.019 601.239L224.055 605.272L229.127 606.991L239.593 598.1L242.675 598.695L246.781 593.012L245.263 586.677L239.65 582.517L233.601 569.754L227.724 563.899L225.918 549.096L222.456 540.182L212.554 530.764L203.146 532.162L199.04 538.395L191.875 536.791L186.872 538.842L178.764 536.149L178.442 533.709L170.656 532.105L159.707 536.711L152.369 532.529L155.796 526.273L152.197 521.781L153.324 515.503L150.69 509.934L146.113 512.81L143.41 518.871L135.302 516.179L133.277 509.442L123.145 503.828L114.14 508.766L114.439 513.796L109.183 518.195L110.08 521.781L99.1539 527.911L94.197 524.096L89.2515 525.723L90.4821 530.077L82.972 529.126L82.6039 520.2L79.2111 518.573L79.0041 511.447L73.1156 508.709L68.4347 513.509L69.5618 516.649L72.1725 518.39L71.0914 519.353L66.445 518.172L61.0855 529.068L56.7611 531.222L51.9997 541.992L60.89 543.608L58.0838 548.271L64.7314 560.279L69.3433 562.18V565.767L64.1678 571.828L62.6152 565.526L54.4839 564.655L53.4143 566.03L55.8296 569.364L59.7744 571.679L58.7508 574.073L54.2539 575.643L53.3453 578.782L47.8364 580.822L46.8243 586.631L52.4483 590.446L54.9325 588.212L57.6352 581.246L58.7623 588.647L59.6594 600.987L54.2539 608.618L54.2884 613.155L52.4368 618.047L64.6048 623.879L76.4279 634.408L73.1616 634.878L70.4589 632.862L69.5618 636.906L63.2592 627.488L54.0239 623.455L52.4483 629.516L55.151 634.901L58.7623 636.241L61.4651 642.967L57.1867 642.52L50.8726 634.443L47.7213 621.656L43.5465 615.95L37.1289 627.488L40.2572 641.019L44.5586 647.012L46.8128 646.336L50.5736 648.879L51.7697 652.649L54.4724 652.397L57.1752 650.151L63.2592 658.457L61.9251 669.537L66.859 671.015L63.9263 677.74L60.9015 680.8L61.6836 682.908L66.6405 683.584L66.7785 691.535L76.7729 690.321L80.8328 684.042L80.6028 680.674L95.0135 672.825L93.6564 667.44L99.959 665.87L103.375 660.039L102.869 645.877L105.376 643.654L107.182 647.47L106.963 656.441L104.939 665.859L108.09 671.244L116.877 675.277L118.901 671.691L125.204 673.49L127.458 668.998L130.609 669.674L129.885 660.382L133.542 658.904L134.255 656.235L133.082 654.081L136.245 645.442L142.996 643.643L148.183 638.934L155.382 635.348L159.96 636.837L163.939 635.577L167.09 629.069L172.266 627.946L174.29 630.41H181.501L182.18 636.688L180.156 641.844L175.67 642.44L174.52 636.688L169.575 636.917L167.55 644.101L169.103 648.02L165.975 653.978L162.145 653.302L163.272 648.363L161.627 646.714L156.509 663.843L169.115 666.088L171.599 672.596L175.199 677.305L183.307 675.506L186.688 669.228L192.324 665.641L193.221 661.597L188.494 658.973L189.172 655.994L196.832 656.67L199.753 658.469L202.824 655.88L201.329 647.023L188.379 634.397L188.724 630.203L191.875 627.064L189.621 624.372L194.348 619.88L190.944 617.509L192.542 616.294H200.65L206.056 614.278L206.504 610.015L209.207 612.032L210.104 621.003L214.348 624.177L216.407 630.203L212.807 636.711L210.817 644.8L213.025 649.28L216.694 649.223L221.352 651.961L222.249 647.47L221.122 643.654L223.595 635.806L227.425 632.438L232.336 636.998L238.684 634.019L239.363 629.081L242.066 625.265L243.871 619.204L241.629 615.836L238.247 616.283L238.224 616.271ZM145.871 561.275L143.617 563.968L142.938 567.554C141.604 568.883 135.727 571.816 135.727 571.816H131.449L128.516 569.8C126.779 570.59 121.305 571.816 121.305 571.816L118.372 573.833L110.494 572.939L110.275 569.8L111.563 568.562L107.124 564.415L95.1861 563.074L97.0492 558.995L96.9917 554.767L100.373 553.198L98.5674 548.707C99.8555 548.328 105.997 546.908 105.997 546.908V544.215L110.505 541.523H113.887V545.109L116.589 546.461L117.602 545.349L134.163 542.646L138.671 548.707V552.293L142.559 555.146L145.883 558.571V561.264L145.871 561.275Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-miterlimit="10"/> +<path id="광주광역시" d="M98.5788 548.707C99.8669 548.329 106.008 546.908 106.008 546.908V544.215L110.517 541.523H113.898V545.109L116.601 546.461L117.613 545.35L134.174 542.646L138.683 548.707V552.293L142.57 555.146L145.894 558.572V561.264L143.64 563.957L142.961 567.543C141.627 568.872 135.762 571.805 135.762 571.805H131.483L128.55 569.789C126.814 570.579 121.339 571.805 121.339 571.805L118.407 573.822L110.528 572.928L110.31 569.789L111.598 568.551L107.159 564.403L95.2205 563.063L97.0836 558.984L97.0261 554.756L100.407 553.187L98.6018 548.695L98.5788 548.707Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path id="제주특별자치도" d="M50.8266 732.613L44.9726 734.641H36.1858L33.2646 738.009L20.1994 742.718L14.794 748.321L1.49878 759.091L3.75298 772.107L17.0482 783.095L21.5451 778.615L45.1911 778.844L50.8266 781.983L64.1103 779.062L69.2973 776.151L84.3866 775.693L91.8163 772.336L95.1976 771.878L99.9245 763.353L109.16 754.165L108.481 740.026L96.0947 730.379L83.248 729.027L77.854 730.597L65.2374 730.826L50.8266 732.613Z" fill="white" stroke="#E2EDF3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/apps/dm/public/next.svg b/apps/dm/public/next.svg new file mode 100644 index 000000000..5174b28c5 --- /dev/null +++ b/apps/dm/public/next.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg> \ No newline at end of file diff --git a/apps/dm/public/vercel.svg b/apps/dm/public/vercel.svg new file mode 100644 index 000000000..770539603 --- /dev/null +++ b/apps/dm/public/vercel.svg @@ -0,0 +1 @@ +<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg> \ No newline at end of file diff --git a/apps/dm/public/window.svg b/apps/dm/public/window.svg new file mode 100644 index 000000000..b2b2a44f6 --- /dev/null +++ b/apps/dm/public/window.svg @@ -0,0 +1 @@ +<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg> \ No newline at end of file diff --git a/apps/dm/src/entities/auth/api/login-service.ts b/apps/dm/src/entities/auth/api/login-service.ts new file mode 100644 index 000000000..614aadbf9 --- /dev/null +++ b/apps/dm/src/entities/auth/api/login-service.ts @@ -0,0 +1,79 @@ +import type { LoginService, ILogin } from "../model/login-interface"; +import { tokenManager } from "../lib/token-manager"; +import {PROXY_HOST, ACCESS_TOKEN, AUTH_USER_ID,SITE_ID, CLAIM_NAME, ENDPOINT, JWT_EXPIRATION_TIME } from "../../../shared/config"; + +export class LoginApiService implements LoginService { + private static instance: LoginApiService; + + private constructor() {} + + public static getInstance(): LoginApiService { + if (!LoginApiService.instance) { + LoginApiService.instance = new LoginApiService(); + } + return LoginApiService.instance; + } + + public async login(payload: ILogin): Promise<void> { + try { + console.log(' ENDPOINT.USER_SERVICE.LOGIN ', ENDPOINT.USER_SERVICE.LOGIN) + const response = await fetch( PROXY_HOST+ ENDPOINT.USER_SERVICE.LOGIN, { + method: "POST", + body: JSON.stringify({ + userId: payload.userId, + password: payload.password, + }), + }); + // const data = response.headers.get(ACCESS_TOKEN); + const accessToken = response.headers.get(ACCESS_TOKEN); + const tokenId = response.headers.get(AUTH_USER_ID); + + console.log('accessToken ', accessToken) + console.log('tokenId ', tokenId) + + if (!accessToken || !tokenId) { + throw new Error("Missing required authentication tokens"); + } + + tokenManager.setToken(CLAIM_NAME, accessToken); + tokenManager.setToken(AUTH_USER_ID, tokenId); + tokenManager.setToken("site-id-key", SITE_ID); + tokenManager.setupRefreshTimer( + () => this.silentRefresh(), + JWT_EXPIRATION_TIME, + ); + } catch (error) { + + console.log(' login err ' , error) + throw new Error(`Login failed: ${error}`); + } + } + + + public async silentRefresh(): Promise<void> { + console.log(' silentRefresh ') + + try { + const response = await fetch(PROXY_HOST+ + `${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`, + { method: "PUT" }, + ); + + const data = await response.json(); + const accessToken = data[ACCESS_TOKEN]; + const tokenId = data[AUTH_USER_ID]; + + if (!accessToken || !tokenId) { + throw new Error("Missing required authentication tokens"); + } + + tokenManager.setToken(CLAIM_NAME, accessToken); + tokenManager.setToken(AUTH_USER_ID, tokenId); + } catch (error) { + tokenManager.clearAuthToken(); + throw new Error(`Silent refresh failed: ${error}`); + } + } +} + +export const LoginApi = LoginApiService.getInstance(); diff --git a/apps/dm/src/entities/auth/index.ts b/apps/dm/src/entities/auth/index.ts new file mode 100644 index 000000000..9dc9dd368 --- /dev/null +++ b/apps/dm/src/entities/auth/index.ts @@ -0,0 +1,2 @@ +export { LoginApi } from "./api/login-service"; +//export { AuthWrapper } from "./lib/auth-wrapper"; diff --git a/apps/dm/src/entities/auth/lib/auth-wrapper.tsx b/apps/dm/src/entities/auth/lib/auth-wrapper.tsx new file mode 100644 index 000000000..678ff50d5 --- /dev/null +++ b/apps/dm/src/entities/auth/lib/auth-wrapper.tsx @@ -0,0 +1,45 @@ +// "use client"; + +// import { useEffect } from "react"; +// import { useRouter } from "next/navigation"; +// import { LoginApi } from "../api/login-service"; + +// const AuthWrapper = ({ children }: { children: React.ReactNode }) => { +// const router = useRouter(); +// const { userInfo, setUserInfo } = useUserStore((state) => state); + +// async function authCheck() { +// const userId = getCookie(AUTH_USER_ID); +// if (!userId) { +// throw new Error("Missing required authentication tokens"); +// } + +// const response = await client.get<IUserInfo>( +// `${ENDPOINT.USER_SERVICE.USERS}/info/me`, +// ); +// if (!userInfo) setUserInfo(response.data); +// } + +// useEffect(() => { +// (async () => { +// try { +// await authCheck(); +// } catch (error) { +// console.error(error); + +// try { +// await LoginApi.silentRefresh(); +// await authCheck(); +// } catch (refreshError) { +// console.error("Token refresh failed:", refreshError); +// setUserInfo(null); +// router.push("/auth/login"); +// } +// } +// })(); +// }, []); + +// return <>{children}</>; +// }; + +// export { AuthWrapper }; diff --git a/apps/sn/app/components/lib/auth/lib/token-manager.tsx b/apps/dm/src/entities/auth/lib/token-manager.ts similarity index 86% rename from apps/sn/app/components/lib/auth/lib/token-manager.tsx rename to apps/dm/src/entities/auth/lib/token-manager.ts index 9feab4af0..40d32f33e 100644 --- a/apps/sn/app/components/lib/auth/lib/token-manager.tsx +++ b/apps/dm/src/entities/auth/lib/token-manager.ts @@ -1,7 +1,5 @@ -import { getCookie, setCookie, removeCookie } from "@utils"; - -const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; -const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; +import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config"; +import { getCookie, removeCookie, setCookie } from "@/src/shared/lib/cookie"; export class TokenManager { private static instance: TokenManager; diff --git a/apps/sn/app/components/lib/auth/model/login-interface.tsx b/apps/dm/src/entities/auth/model/login-interface.ts similarity index 100% rename from apps/sn/app/components/lib/auth/model/login-interface.tsx rename to apps/dm/src/entities/auth/model/login-interface.ts diff --git a/apps/dm/src/shared/config/api.ts b/apps/dm/src/shared/config/api.ts new file mode 100644 index 000000000..a531bed85 --- /dev/null +++ b/apps/dm/src/shared/config/api.ts @@ -0,0 +1,58 @@ +import { PROXY_HOST } from "./env"; + +export const BASE_URL = `${PROXY_HOST}/server`; + +export const ENDPOINT = { + ATTACH_SERVICE: { + TUS_UPLOAD: "/attach-service/api/v1/tus/upload", + EDITOR_UPLOAD: "/attach-service/api/v1/upload/editor", + IMAGES: "/attach-service/api/v1/images", + MOVIE_THUMBNAIL: "/attach-service/api/v1/movie/thumbnail", + ATTACHMENTS: "/attach-service/api/v1/atfl/attachments", + DOWNLOAD: "/attach-service/api/v1/atflcd/download", + STREAM_VIDEO: "/attach-service/api/v1/stream/video", + }, + USER_SERVICE: { + LOGIN: "/user-service/login", + USERS: "/user-service/api/v1/users", + SILENT_REFRESH: "/user-service/api/v1/users/token/refresh", + AUTHORIZATIONS: "/user-service/api/v1/authorizations", + ROLE_AUTHORIZATIONS: "/user-service/api/v1/role-authorizations", + ROLES: "/user-service/api/v1/roles", + + }, + CMS_SERVICE: { + DMSITES:'cms-service/api/v1/menu/anonymous/user/DM', + SITES: "/cms-service/api/v1/sites", + IPS: "/cms-service/api/v1/site-ips", + DOMAINS: "/cms-service/api/v1/site-dmns", + POSTINGS: "/cms-service/api/v1/site-pstgs", + CODE: "/cms-service/api/v1/code/cache", + MENUS: "/cms-service/api/v1/menus", + MENU_ROLES: "/cms-service/api/v1/menu-roles", + MENU_AUTHRTS: "/cms-service/api/v1/menu-authrts", + CONTENTS: "/cms-service/api/v1/contents", + CONTENTS_DETAILS: "/cms-service/api/v1/contents-details", + ORGANIZATIONS: "/cms-service/api/v1/ognzs", + USER_ORGANIZATION: "/cms-service/api/v1/user-ognz", + IMAGES: "/cms-service/api/v1/images", + BANNERS: "/cms-service/api/v1/bnrs", + TEMPLATE: "/cms-service/api/v1/tmplt", + TERMS: "/cms-service/api/v1/trms", + LWPO_CODE: "/cms-service/api/v1/lwpo-cds", + UP_CODE: "/cms-service/api/v1/up-codes", + POPUPS: "/cms-service/api/v1/popups", + SCHEDULES: "/cms-service/api/v1/schdls", + SURVEY: "/cms-service/api/v1/srvy", + SURVEY_QUESTION: "/cms-service/api/v1/srvy-qitem", + SURVEY_ARTICLE: "/cms-service/api/v1/srvy-artcl", + }, + BOARD_SERVICE: { + BBS: "/board-service/api/v1/bbs", + BBS_TEMPLATES: "/board-service/api/v1/bbsTmplt", + POSTS: "/board-service/api/v1/posts", + POSTS_COMMENTS: "/board-service/api/v1/posts-cmnts", + }, +}; + +export const JWT_EXPIRATION_TIME = 1800000; diff --git a/apps/dm/src/shared/config/env.ts b/apps/dm/src/shared/config/env.ts new file mode 100644 index 000000000..b01588df0 --- /dev/null +++ b/apps/dm/src/shared/config/env.ts @@ -0,0 +1,13 @@ +export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "DM"; +export const PORT = process.env.NEXT_PUBLIC_PORT || "3000"; +export const PROXY_HOST = process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`; +export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL ||'http://con.mind-one.co.kr:11035/api/v1'; +export const SERVER_API_FO_URL = process.env.NEXT_PUBLIC_FO_SERVER_API_URL||' http://con.mind-one.co.kr:11082/api/v1'; +export const SERVER_API_WR_URL = process.env.NEXT_PUBLIC_WR_SERVER_API_URL||' http://con.mind-one.co.kr:11080/api/v1'; +export const SERVER_API_CS_URL = process.env.NEXT_PUBLIC_CS_SERVER_API_URL||'http://dev.onasys.co.kr:8000'; +export const SERVER_API_CS_TUS_URL = process.env.NEXT_PUBLIC_CS_TUS_UPLOAD ||'/attach-service/api/v1/tus/upload' +export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; +export const ACCESS_TOKEN = process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token"; +export const REFRESH_TOKEN =process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token"; +export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; +export const SERVER_SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL; \ No newline at end of file diff --git a/apps/dm/src/shared/config/index.ts b/apps/dm/src/shared/config/index.ts new file mode 100644 index 000000000..3ac40ddb4 --- /dev/null +++ b/apps/dm/src/shared/config/index.ts @@ -0,0 +1,2 @@ +export * from "./api"; +export * from "./env"; \ No newline at end of file diff --git a/apps/dm/src/shared/lib/cookie.ts b/apps/dm/src/shared/lib/cookie.ts new file mode 100644 index 000000000..48254bb18 --- /dev/null +++ b/apps/dm/src/shared/lib/cookie.ts @@ -0,0 +1,30 @@ +import Cookies from "js-cookie"; + + +export function getCookie(name: string): string | undefined { + if (typeof window === "undefined") { + return undefined; + } + return Cookies.get(name); +} + +export function setCookie( + name: string, + value: string, + options?: Cookies.CookieAttributes, +): void { + if (typeof window === "undefined") { + return; + } + Cookies.set(name, value, options); +} + +export function removeCookie( + name: string, + options?: Cookies.CookieAttributes, +): void { + if (typeof window === "undefined") { + return; + } + Cookies.remove(name, options); +} diff --git a/apps/dm/tailwind.config.ts b/apps/dm/tailwind.config.ts new file mode 100644 index 000000000..42cfc69f5 --- /dev/null +++ b/apps/dm/tailwind.config.ts @@ -0,0 +1,69 @@ +import baseConfig from "../../packages/common/components/tailwind.config"; + +import type { Config } from "tailwindcss"; + +export default { + darkMode: ["class"], + presets: [baseConfig], + content: [ + "./app/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{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}", + ], + plugins: [require("tailwindcss-animate")], + theme: { + extend: { + fontFamily: { + NotoSansKR: ['NotoSansKR'], + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + }, + colors: { + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + chart: { + '1': 'hsl(var(--chart-1))', + '2': 'hsl(var(--chart-2))', + '3': 'hsl(var(--chart-3))', + '4': 'hsl(var(--chart-4))', + '5': 'hsl(var(--chart-5))' + } + } + } + } +} satisfies Config; diff --git a/apps/dm/tsconfig.json b/apps/dm/tsconfig.json new file mode 100644 index 000000000..493584782 --- /dev/null +++ b/apps/dm/tsconfig.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "noImplicitAny": false, + "baseUrl": ".", + "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" + } + ], + "paths": { + "@/*": ["./*"], + "@common/components/*": ["../../packages/common/components/src/*"], + "@common/components": ["../../packages/common/components/src/index"], + "@common/business_components/*": [ + "../../packages/common/business_components/src/*" + ], + "@common/business_components": [ + "../../packages/common/business_components/src/index" + ], + "@common/network": ["../../packages/common/network"] + } + }, + "include": [ + ".eslintrc.*", + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + "**/*.mts", + "**/*.js", + "**/*.cjs", + "**/*.mjs", + "**/*.jsx", + "**/*.json", + ".next/types/**/*.ts" + ], + "exclude": ["**/node_modules", "**/.*/*"] +} diff --git a/apps/fo/.env.development b/apps/fo/.env.development new file mode 100644 index 000000000..94488f0cd --- /dev/null +++ b/apps/fo/.env.development @@ -0,0 +1,16 @@ +NEXT_PUBLIC_SITE_ID = FO +NEXT_PUBLIC_PORT = 11300 +NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11300 +NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 +NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver +NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver +NEXT_PUBLIC_CLAIM_NAME = Authorization +NEXT_PUBLIC_AUTH_USER_ID = token-id +NEXT_PUBLIC_ACCESS_TOKEN = access-token +NEXT_PUBLIC_REFRESH_TOKEN = refresh-token + +NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API + + diff --git a/apps/fo/.env.local b/apps/fo/.env.local new file mode 100644 index 000000000..76fb868cd --- /dev/null +++ b/apps/fo/.env.local @@ -0,0 +1,16 @@ +NEXT_PUBLIC_SITE_ID = FO +NEXT_PUBLIC_PORT = 3000 +NEXT_PUBLIC_PROXY_HOST = http://localhost:3000 +NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 +NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver +NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver +NEXT_PUBLIC_CLAIM_NAME = Authorization +NEXT_PUBLIC_AUTH_USER_ID = token-id +NEXT_PUBLIC_ACCESS_TOKEN = access-token +NEXT_PUBLIC_REFRESH_TOKEN = refresh-token + +NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API + + diff --git a/apps/fo/.env.production b/apps/fo/.env.production new file mode 100644 index 000000000..16675eaec --- /dev/null +++ b/apps/fo/.env.production @@ -0,0 +1,28 @@ +# NEXT_PUBLIC_SITE_ID = WR +# NEXT_PUBLIC_PORT = 11301 +# NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11301 +# NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 +# NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver +# NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver +# NEXT_PUBLIC_CLAIM_NAME = Authorization +# NEXT_PUBLIC_AUTH_USER_ID = token-id +# NEXT_PUBLIC_ACCESS_TOKEN = access-token +# NEXT_PUBLIC_REFRESH_TOKEN = refresh-token + + + +NEXT_PUBLIC_SITE_ID = FO +NEXT_PUBLIC_PORT = 11300 +NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11300 +NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 +NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver +NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver +NEXT_PUBLIC_CLAIM_NAME = Authorization +NEXT_PUBLIC_AUTH_USER_ID = token-id +NEXT_PUBLIC_ACCESS_TOKEN = access-token +NEXT_PUBLIC_REFRESH_TOKEN = refresh-token +NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API + + diff --git a/apps/fo/.gitignore b/apps/fo/.gitignore new file mode 100644 index 000000000..8c5b2ee3a --- /dev/null +++ b/apps/fo/.gitignore @@ -0,0 +1,41 @@ +# 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/ + +# 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 diff --git a/apps/fo/app/base.tsx b/apps/fo/app/base.tsx new file mode 100644 index 000000000..a01ce8643 --- /dev/null +++ b/apps/fo/app/base.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { BaseLayout } from "@common/components"; +import type { MenuItem } from "./store/common"; +import { useFoStore } from "./store"; +import { usePathname, useRouter } from "next/navigation"; + +export default function Base({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + const { getMenuAuth } = useFoStore((state) => state); + const pathname = usePathname(); + const router = useRouter(); + + const handleClick = (id: MenuItem) => { + getMenuAuth(id.menuCd); + router.push(id.urlPath); + }; + + return ( + <BaseLayout currentPath={pathname ?? ""} handleMenuClick={handleClick}> + {children} + </BaseLayout> + ); +} diff --git a/apps/fo/app/components/api/config/env.ts b/apps/fo/app/components/api/config/env.ts new file mode 100644 index 000000000..d419bfd1c --- /dev/null +++ b/apps/fo/app/components/api/config/env.ts @@ -0,0 +1,5 @@ +import { PROXY_HOST, SERVER_API_URL, SERVER_API_WR_URL } from "@/src/shared/config/env"; + +export const DEV_URL_FO = SERVER_API_URL; +export const LOCAL_URL = PROXY_HOST; +export const DEV_URL_WR = SERVER_API_WR_URL; \ No newline at end of file diff --git a/apps/fo/app/components/api/config/index.ts b/apps/fo/app/components/api/config/index.ts new file mode 100644 index 000000000..b9f9f76d6 --- /dev/null +++ b/apps/fo/app/components/api/config/index.ts @@ -0,0 +1 @@ +export * from './env'; \ No newline at end of file diff --git a/apps/fo/app/components/api/index.ts b/apps/fo/app/components/api/index.ts new file mode 100644 index 000000000..3bba85af2 --- /dev/null +++ b/apps/fo/app/components/api/index.ts @@ -0,0 +1,79 @@ +import { DEV_URL_FO, DEV_URL_WR, LOCAL_URL } from "./config/env"; + +export const getFetch = async (part : string, url: string) => { + let reqUrl = ""; + reqUrl = part === "fo" ? DEV_URL_FO+url : part === 'local' ? LOCAL_URL+url : DEV_URL_WR+url; + const response = await fetch(reqUrl , { + method: "GET", + headers: { + accept: "application/json", + } + }); + if(!response.ok) { + throw new Error(`오류 발생: ${response.status}`); + } + const res = await response.json() + return res; +} + +export const getFetchAreaList = async (url: string) => { + const response = await fetch(url , { + method: "GET", + headers: { + accept: "application/json", + } + }); + if(!response.ok) { + throw new Error(`오류 발생: ${response.status}`); + } + const res = await response.json() + return res; +} + +export const postFetch = async (part : string, url: string, params: unknown) => { + let reqUrl = ""; + reqUrl = part === "fo" ? DEV_URL_FO+url : DEV_URL_WR+url; + const response = await fetch(reqUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(params), + }); + if(!response.ok) { + throw new Error(`오류 발생: ${response.status}`); + } + return await response.json(); +} + +export const putFetch = async (part : string, url: string, params: unknown) => { + let reqUrl = ""; + reqUrl = part === "fo" ? DEV_URL_FO+url : DEV_URL_WR+url; + const response = await fetch(reqUrl, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(params), + }); + if(!response.ok) { + throw new Error(`오류 발생: ${response.status}`); + } + return response; +} + +export const deleteFetch = async (part : string, url: string, params: unknown) => { + let reqUrl = ""; + reqUrl = part === "fo" ? DEV_URL_FO+url : DEV_URL_WR+url; + const response = await fetch(reqUrl, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(params), + }); + if(!response.ok) { + throw new Error(`오류 발생: ${response.status}`); + } + return response; +} \ No newline at end of file diff --git a/apps/fo/app/components/common/DataForm.tsx b/apps/fo/app/components/common/DataForm.tsx new file mode 100644 index 000000000..66530aed9 --- /dev/null +++ b/apps/fo/app/components/common/DataForm.tsx @@ -0,0 +1,46 @@ +type DataFormProps = { + type: string; + column: string; + item: Record<string, unknown>; +} + +export default function DataForm({ type, column, item }: DataFormProps) { + if(item) { + const value = item[column]; + switch(type){ + case 'date': + if(value && typeof value === 'string' && value.length >= 8) + item[column] = value.substring(0, 4) + "-" + value.substring(4, 6) + "-" + value.substring(6, 8); + break; + case 'number': + if(value && value != '') { + item[column] = value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); + } + break; + case 'float1': + if(value) { + item[column] = String(value).includes('.')?String(Math.round(parseFloat(value.toString().replace(/,/g, '')) * 10) / 10).split('.').map((part, index) => + index === 0 ? part.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : part + ).join('.') + : String(value).replace(/\B(?=(\d{3})+(?!\d))/g, ","); + } + break; + case 'float2': + if(value) { + item[column] = String(value).includes('.')?String(Math.round(parseFloat(value.toString().replace(/,/g, '')) * 100) / 100).split('.').map((part, index) => + index === 0 ? part.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : part + ).join('.') + : String(value).replace(/\B(?=(\d{3})+(?!\d))/g, ","); + } + break; + case 'float3': + if(value) { + item[column] = String(value).includes('.')?String(Math.round(parseFloat(value.toString().replace(/,/g, '')) * 1000) / 1000).split('.').map((part, index) => + index === 0 ? part.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : part + ).join('.') + : String(value).replace(/\B(?=(\d{3})+(?!\d))/g, ","); + } + break; + } + } +} diff --git a/apps/fo/app/components/common/FacilityTermModalHeader.tsx b/apps/fo/app/components/common/FacilityTermModalHeader.tsx new file mode 100644 index 000000000..30c77405f --- /dev/null +++ b/apps/fo/app/components/common/FacilityTermModalHeader.tsx @@ -0,0 +1,279 @@ +import { useFoStore } from '@/app/store'; +import { DatePickerRange, SelectBox } from '@common/business_components/ui'; +import ListDiv from '@common/business_components/ui/list_div'; +import { Table, TableCell, TableHead, TableRow } from '@common/components/ui'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +type FacilityTermModalHeaderProps<T extends { msrDt: string }> = { + fcltyCd : string; + fcltyName: string; + tabName: string; + searchYear: string; + searchList: T[]; + ref: React.RefObject<ReturnType<T>>; + queryClick:(returnType:ReturnType<T>) => void; +} + +export type ReturnType<T> = { + startDate: Date; + endDate: Date; + tabName: string; + filteredList: T[]; +} + +export default function FacilityTermModalHeader<T extends { msrDt: string }>({ + searchList, + fcltyName, + tabName, + searchYear, + queryClick, + ref +}: FacilityTermModalHeaderProps<T & { msrDt: string }>) { + const {Common} = useFoStore((state) => state); + const { selectFlowPanel } = Common; + const [startDate, setStartDate] = useState<Date>(new Date(Number(searchYear), 0, 1)); + const [endDate, setEndDate] = useState<Date>(new Date(Number(searchYear), 11, 31)); + const startPickerRef = useRef<HTMLDivElement>(null); + const endPickerRef = useRef<HTMLDivElement>(null); + const [flowPanel, setFlowPanel] = useState<string>(selectFlowPanel[0]?.val ?? '00'); + const [copySearchList, setCopySearchList] = useState<T[]>(searchList); + const [standSearchList, setStandSearchList] = useState<T[]>(searchList); + + const isDateInRanges = (date: Date) => { + const standYear = parseInt(searchYear); + const dateRanges:{start:Date, end:Date}[] = []; + + if (tabName.includes("_summer")) { + dateRanges.push({ + start: new Date(standYear, 5, 1), + end: new Date(standYear, 7, 31) + }); + } else if (tabName.includes("_winter")) { + dateRanges.push({ + start: new Date(standYear, 0, 1), + end: new Date(standYear, 1, 28) + }); + } else { + dateRanges.push({ + start: new Date(standYear, 0, 1), + end: new Date(standYear, 11, 31) + }); + } + + return dateRanges.some(range => { + const start = new Date(range.start); + const end = new Date(range.end); + return date >= start && date <= end; + }); + }; + + useEffect(() => { + let startDate = new Date(Number(searchYear), 0, 1); + let endDate = new Date(Number(searchYear), 11, 31); + + if(tabName.includes("_summer")) { + startDate = new Date(Number(searchYear), 5, 1); + endDate = new Date(Number(searchYear), 7, 31); + setStartDate(startDate); + setEndDate(endDate); + } else if (tabName.includes("_winter")) { + startDate = new Date(Number(searchYear), 0, 1); + endDate = new Date(Number(searchYear), 1, 28); + setStartDate(startDate); + setEndDate(endDate); + } else { + startDate = new Date(Number(searchYear), 0, 1); + endDate = new Date(Number(searchYear), 11, 31); + setStartDate(startDate); + setEndDate(endDate); + } + + let filteredData: T[] = []; + + if(tabName.includes("_rain")) { + filteredData = searchList.filter((item) => { + const itemDate = new Date((item as T & { msrDt: string }).msrDt); + const itemDateOnly = new Date(itemDate.getFullYear(), itemDate.getMonth(), itemDate.getDate()); + const startDateOnly = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate()); + const endDateOnly = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate()); + const rainEffectDay = (item as T & { rainEffectDay: number }).rainEffectDay; + if(flowPanel === '1') { + const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay; + return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && infoRnDay>0 && infoRnDay < 3; + } else if(flowPanel === '2'){ + const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay; + return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && infoRnDay >= 3 && infoRnDay < 10; + } else if(flowPanel === '3'){ + const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay; + return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && infoRnDay >= 10 && infoRnDay < 20; + } else if(flowPanel === '4'){ + const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay; + return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && infoRnDay >= 20 && infoRnDay < 30; + } else if(flowPanel === '5'){ + const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay; + return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && infoRnDay >= 30 && infoRnDay < 40; + } else if(flowPanel === '6'){ + const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay; + return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && infoRnDay >= 40 && infoRnDay < 50; + } else if(flowPanel === '7'){ + const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay; + return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && infoRnDay >= 50 && infoRnDay < 60; + } else if(flowPanel === '8'){ + const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay; + return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && infoRnDay >= 60; + } else { + const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay; + return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && (infoRnDay>0 || rainEffectDay ===1); + } + }); + } else if(tabName.includes("_chuncheon")) { + filteredData = searchList.filter((item) => { + const itemDate = new Date((item as T & { msrDt: string }).msrDt); + const itemDateOnly = new Date(itemDate.getFullYear(), itemDate.getMonth(), itemDate.getDate()); + const startDateOnly = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate()); + const endDateOnly = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate()); + const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay; + return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && infoRnDay==0; + }); + } else { + filteredData = searchList.filter((item) => { + const itemDate = new Date((item as T & { msrDt: string }).msrDt); + const itemDateOnly = new Date(itemDate.getFullYear(), itemDate.getMonth(), itemDate.getDate()); + const startDateOnly = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate()); + const endDateOnly = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate()); + return itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly; + }); + } + + setCopySearchList(filteredData); + setStandSearchList(filteredData); + }, [searchList, searchYear, tabName, flowPanel]); + + const handleStartDateSelect = (day: Date | undefined) => { + if(day && day > endDate) { + alert('종료일은 시작일 이전일 수 없습니다.'); + return; + } + + const selectedDate = day ?? startDate; + setStartDate(selectedDate); + const button = startPickerRef.current?.querySelector('button'); + if (button) { + setTimeout(() => button.click(), 0); + } + + const filteredData = standSearchList.filter((item) => { + const itemDate = new Date((item as T & { msrDt: string }).msrDt); + const itemDateOnly = new Date(itemDate.getFullYear(), itemDate.getMonth(), itemDate.getDate()); + const startDateOnly = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate()); + const endDateOnly = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate()); + return itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly; + }); + setCopySearchList(filteredData); + }; + + const handleEndDateSelect = (day: Date | undefined) => { + if(day && day < startDate) { + alert('종료일은 시작일 이후일 수 없습니다.'); + return; + } + + const selectedDate = day ?? endDate; + setEndDate(selectedDate); + const button = endPickerRef.current?.querySelector('button'); + if (button) { + setTimeout(() => button.click(), 0); + } + + const filteredData = standSearchList.filter((item) => { + const itemDate = new Date((item as T & { msrDt: string }).msrDt); + const itemDateOnly = new Date(itemDate.getFullYear(), itemDate.getMonth(), itemDate.getDate()); + const startDateOnly = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate()); + const endDateOnly = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate()); + return itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly; + }); + setCopySearchList(filteredData); + }; + + const fncSetFilteredList = useCallback(() => { + queryClick({ + startDate: startDate, + endDate: endDate, + tabName: tabName, + filteredList: copySearchList + }); + }, [queryClick, startDate, endDate, tabName, copySearchList]); + + useEffect(() => { + fncSetFilteredList(); + }, [fncSetFilteredList]); + + useEffect(() => { + if(ref && ref.current) { + const customRef = ref.current; + customRef.startDate = startDate; + customRef.endDate = endDate; + customRef.tabName = tabName; + customRef.filteredList = copySearchList; + } + }, [startDate, endDate, tabName, copySearchList, ref]); + + return ( + <ListDiv className="!h-[80px]"> + <div className="!m-[0.5rem] max-h-[40rem] overflow-auto"> + <Table> + <thead> + <TableRow className="!h-[20px]"> + <TableHead className="w-[120px]">{'시설명'}</TableHead> + {tabName === "tab_term_rain" ? <TableCell className="w-[300px]">{fcltyName}</TableCell> : + <TableCell className="w-[380px]">{fcltyName}</TableCell>} + {tabName === "tab_term_rain" && ( + <> + <TableHead className="w-[120px]">{'강수구분'}</TableHead> + <TableCell className="w-[250px]"> + <div className="justify-center"> + <SelectBox + selectArray={selectFlowPanel} + value={flowPanel} + onSelectValue={(val) => setFlowPanel(val.toString())} + className="mx-3 w-[150px]" + label="" + selectClass="w-full p-1 border rounded bg-white" + /> + </div> + </TableCell> + </> + )} + <TableHead className="w-[120px]">{'년/월'}</TableHead> + <TableCell className=""> + <div className="flex"> + <div ref={startPickerRef} className="flex justify-center"> + <DatePickerRange + mode="single" + selected={startDate} + onSelect={(date) => handleStartDateSelect(date as Date)} + className="w-full p-1 border rounded bg-white" + disabled={(date) => !isDateInRanges(date)} + defaultMonth={startDate} + /> + </div> + <h3 className="justify-center px-5">~</h3> + <div ref={endPickerRef} className="flex justify-center"> + <DatePickerRange + mode="single" + selected={endDate} + onSelect={(date) => handleEndDateSelect(date as Date)} + className="w-full p-1 border rounded bg-white" + disabled={(date) => !isDateInRanges(date)} + defaultMonth={endDate} + /> + </div> + </div> + </TableCell> + </TableRow> + </thead> + </Table> + </div> + </ListDiv> + ); +} \ No newline at end of file diff --git a/apps/fo/app/components/common/FacilityTermModalHeaderMonth.tsx b/apps/fo/app/components/common/FacilityTermModalHeaderMonth.tsx new file mode 100644 index 000000000..a284d04cf --- /dev/null +++ b/apps/fo/app/components/common/FacilityTermModalHeaderMonth.tsx @@ -0,0 +1,276 @@ +import { useFoStore } from '@/app/store'; +import { SelectBox } from '@common/business_components/ui'; +import ListDiv from '@common/business_components/ui/list_div'; +import { DatePicker, Table, TableCell, TableHead, TableRow } from '@common/components/ui'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +type FacilityTermModalHeaderProps<T extends { msrDt: string }> = { + fcltyCd : string; + fcltyName: string; + tabName: string; + searchYear: string; + searchList: T[]; + ref: React.RefObject<ReturnType<T>>; + queryClick:(returnType:ReturnType<T>) => void; +} + +export type ReturnType<T> = { + startDate: Date; + endDate: Date; + tabName: string; + filteredList: T[]; +} + +export default function FacilityTermModalHeaderMonth<T extends { msrDt: string }>({ + searchList, + fcltyName, + tabName, + searchYear, + queryClick, + ref +}: FacilityTermModalHeaderProps<T & { msrDt: string }>) { + const {Common} = useFoStore((state) => state); + const { selectFlowPanel } = Common; + const [startDate, setStartDate] = useState<Date>(new Date(Number(searchYear), 0, 1)); + const [endDate, setEndDate] = useState<Date>(new Date(Number(searchYear), 11, 31)); + const startPickerRef = useRef<HTMLDivElement>(null); + const endPickerRef = useRef<HTMLDivElement>(null); + const [flowPanel, setFlowPanel] = useState<string>(selectFlowPanel[0]?.val ?? '00'); + const [copySearchList, setCopySearchList] = useState<T[]>(searchList); + + /* const isDateInRanges = (date: Date) => { + const standYear = parseInt(searchYear); + const dateRanges = []; + + if (tabName.includes("_summer")) { + dateRanges.push({ + start: new Date(standYear, 5, 1), + end: new Date(standYear, 7, 31) + }); + } else if (tabName.includes("_winter")) { + dateRanges.push({ + start: new Date(standYear, 0, 1), + end: new Date(standYear, 1, 28) + }); + } else { + dateRanges.push({ + start: new Date(standYear, 0, 1), + end: new Date(standYear, 11, 31) + }); + } + + return dateRanges.some(range => { + const start = new Date(range.start); + const end = new Date(range.end); + return date >= start && date <= end; + }); + }; */ + + useEffect(() => { + let startDate = new Date(Number(searchYear), 0, 1); + let endDate = new Date(Number(searchYear), 11, 31); + + if(tabName.includes("_summer")) { + startDate = new Date(Number(searchYear), 5, 1); + endDate = new Date(Number(searchYear), 7, 31); + setStartDate(startDate); + setEndDate(endDate); + } else if (tabName.includes("_winter")) { + startDate = new Date(Number(searchYear), 0, 1); + endDate = new Date(Number(searchYear), 1, 28); + setStartDate(startDate); + setEndDate(endDate); + } else { + startDate = new Date(Number(searchYear), 0, 1); + endDate = new Date(Number(searchYear), 11, 31); + setStartDate(startDate); + setEndDate(endDate); + } + + let filteredData: T[] = []; + + if(tabName.includes("_rain")) { + filteredData = searchList.filter((item) => { + const itemDate = new Date((item as T & { msrDt: string }).msrDt); + const itemDateOnly = new Date(itemDate.getFullYear(), itemDate.getMonth(), itemDate.getDate()); + const startDateOnly = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate()); + const endDateOnly = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate()); + const rainEffectDay = (item as T & { rainEffectDay: number }).rainEffectDay; + + if(flowPanel === '1') { + const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay; + return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && infoRnDay>0 && infoRnDay < 3; + } else if(flowPanel === '2'){ + const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay; + return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && infoRnDay >= 3 && infoRnDay < 10; + } else if(flowPanel === '3'){ + const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay; + return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && infoRnDay >= 10 && infoRnDay < 20; + } else if(flowPanel === '4'){ + const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay; + return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && infoRnDay >= 20 && infoRnDay < 30; + } else if(flowPanel === '5'){ + const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay; + return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && infoRnDay >= 30 && infoRnDay < 40; + } else if(flowPanel === '6'){ + const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay; + return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && infoRnDay >= 40 && infoRnDay < 50; + } else if(flowPanel === '7'){ + const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay; + return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && infoRnDay >= 50 && infoRnDay < 60; + } else if(flowPanel === '8'){ + const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay; + return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && infoRnDay >= 60; + } else { + const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay; + return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && (infoRnDay>0 || rainEffectDay ===1); + } + }); + } else if(tabName.includes("_chuncheon")) { + filteredData = searchList.filter((item) => { + const itemDate = new Date((item as T & { msrDt: string }).msrDt); + const itemDateOnly = new Date(itemDate.getFullYear(), itemDate.getMonth(), itemDate.getDate()); + const startDateOnly = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate()); + const endDateOnly = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate()); + const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay; + return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && infoRnDay==0; + }); + } else { + filteredData = searchList.filter((item) => { + const itemDate = new Date((item as T & { msrDt: string }).msrDt); + const itemDateOnly = new Date(itemDate.getFullYear(), itemDate.getMonth(), itemDate.getDate()); + const startDateOnly = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate()); + const endDateOnly = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate()); + return itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly; + }); + } + + setCopySearchList(filteredData); + }, [searchList, searchYear, tabName, flowPanel]); + + const handleStartDateSelect = (day: Date | undefined) => { + if(day && day > endDate) { + alert('종료일은 시작일 이전일 수 없습니다.'); + return; + } + + const selectedDate = day ?? startDate; + setStartDate(selectedDate); + const button = startPickerRef.current?.querySelector('button'); + if (button) { + setTimeout(() => button.click(), 0); + } + + const filteredData = searchList.filter((item) => { + const itemDate = new Date((item as T & { msrDt: string }).msrDt); + const itemDateOnly = new Date(itemDate.getFullYear(), itemDate.getMonth(), itemDate.getDate()); + const startDateOnly = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate()); + const endDateOnly = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate()); + return itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly; + }); + setCopySearchList(filteredData); + }; + + const handleEndDateSelect = (day: Date | undefined) => { + if(day && day < startDate) { + alert('종료일은 시작일 이후일 수 없습니다.'); + return; + } + + const selectedDate = day ?? endDate; + setEndDate(selectedDate); + const button = endPickerRef.current?.querySelector('button'); + if (button) { + setTimeout(() => button.click(), 0); + } + + const filteredData = searchList.filter((item) => { + const itemDate = new Date((item as T & { msrDt: string }).msrDt); + const itemDateOnly = new Date(itemDate.getFullYear(), itemDate.getMonth(), itemDate.getDate()); + const startDateOnly = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate()); + const endDateOnly = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate()); + return itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly; + }); + setCopySearchList(filteredData); + }; + + const fncSetFilteredList = useCallback(() => { + queryClick({ + startDate: startDate, + endDate: endDate, + tabName: tabName, + filteredList: copySearchList + }); + }, [queryClick, startDate, endDate, tabName, copySearchList]); + + useEffect(() => { + fncSetFilteredList(); + }, [fncSetFilteredList]); + + useEffect(() => { + if(ref && ref.current) { + const customRef = ref.current; + customRef.startDate = startDate; + customRef.endDate = endDate; + customRef.tabName = tabName; + customRef.filteredList = copySearchList; + } + }, [startDate, endDate, tabName, copySearchList, ref]); + + return ( + <ListDiv className="!h-[100px]"> + <div className="card !m-[1.2rem] max-h-[40rem] overflow-auto !p-0r"> + <Table> + <thead> + <TableRow> + <TableHead className="w-[120px]">{'시설명'}</TableHead> + {tabName === "tab_term_rain" ? <TableCell className="w-[300px]">{fcltyName}</TableCell> : + <TableCell className="w-[380px]">{fcltyName}</TableCell>} + {tabName === "tab_term_rain" && ( + <> + <TableHead className="w-[120px]">{'강수구분'}</TableHead> + <TableCell className="w-[250px]"> + <div className="justify-center"> + <SelectBox + selectArray={selectFlowPanel} + value={flowPanel} + onSelectValue={(val) => setFlowPanel(val.toString())} + className="mx-3 w-[150px]" + label="" + selectClass="w-full p-1 border rounded bg-white" + /> + </div> + </TableCell> + </> + )} + <TableHead className="w-[120px]">{'년/월'}</TableHead> + <TableCell className=""> + <div className="flex"> + <div ref={startPickerRef} className="flex justify-center"> + <DatePicker + mode="single" + selected={startDate} + onSelect={(date) => handleStartDateSelect(date as Date)} + className="w-full p-1 border rounded bg-white" + defaultMonth={startDate} + /> + </div> + <h3 className="justify-center px-5">~</h3> + <div ref={endPickerRef} className="flex justify-center"> + <DatePicker + mode="single" + selected={endDate} + onSelect={(date) => handleEndDateSelect(date as Date)} + className="w-full p-1 border rounded bg-white" + defaultMonth={endDate} + /> + </div> + </div> + </TableCell> + </TableRow> + </thead> + </Table> + </div> + </ListDiv> + ); +} \ No newline at end of file diff --git a/apps/fo/app/components/common/HeadMakeColSpan.tsx b/apps/fo/app/components/common/HeadMakeColSpan.tsx new file mode 100644 index 000000000..06617839a --- /dev/null +++ b/apps/fo/app/components/common/HeadMakeColSpan.tsx @@ -0,0 +1,65 @@ +import { TableHead, TableRow } from "@common/components/ui"; +import type { TableUpperProps } from "../../store/dashboard"; + +const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProps[], layoutNum: string }) => { + return ( + layoutNum === 'A' ? ( + <> + <TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) => + (head.id === 'trtFrfNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> : + head.id === 'laeNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> : + head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? <TableHead rowSpan={2} className="w-40" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : + head.id === 'trstOprEtpNm' ? <TableHead className="w-1/15" rowSpan={2} key={head.id}>{head.title}</TableHead> : + head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={2}>{'행정구역'}</TableHead> : + !head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-gray-20" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))} + </TableRow> + <TableRow className="bg-gray-50"> + {upHeadList.map((head: TableUpperProps) => { + if (head.upName === 'upChangeRe') { + return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>; + } + })} + </TableRow> + </> + ) : layoutNum === 'R' ? ( + <> + <TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) => + (head.id === 'trtFrfNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> : + head.id === 'laeNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> : + head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? <TableHead rowSpan={2} className="w-40" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : + head.id === 'trstOprEtpNm' ? <TableHead className="w-1/15" rowSpan={2} key={head.id}>{head.title}</TableHead> : + head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={3}>{'유역'}</TableHead> : + !head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-gray-20" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))} + </TableRow> + <TableRow className="bg-gray-50"> + {upHeadList.map((head: TableUpperProps) => { + if (head.upName === 'upChangeRe') { + return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>; + } + })} + </TableRow> + </> + ) : ( + <> + <TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) => + (head.id === 'trtFrfNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> : + head.id === 'laeNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> : + head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? <TableHead rowSpan={2} className="w-40" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : + head.id === 'trstOprEtpNm' ? <TableHead className="w-1/15" rowSpan={2} key={head.id}>{head.title}</TableHead> : + head.upName === 'upChangeRe1' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={3}>{'권역'}</TableHead> : + head.upName === 'upChangeRe2' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={2}>{'행정구역'}</TableHead> : + !head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-gray-20" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))} + </TableRow> + <TableRow className="bg-gray-50"> + {upHeadList.map((head: TableUpperProps) => { + if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2') { + return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>; + } + })} + </TableRow> + </> + ) + ); +} + +export { HeadMakeColSpan }; diff --git a/apps/fo/app/components/common/SearchInput.tsx b/apps/fo/app/components/common/SearchInput.tsx new file mode 100644 index 000000000..11bdd3676 --- /dev/null +++ b/apps/fo/app/components/common/SearchInput.tsx @@ -0,0 +1,108 @@ +import { useFoStore } from '@/app/store'; +import type { facilityListProps } from '@/app/store/processFacility'; +import { Label } from '@common/components/ui'; +import React, { useEffect, useState, useRef, useCallback } from 'react' + +type thisProps = { + label: string | undefined; + setValue : (fcltyCd?: string, fcltyNm?: string, fcltyCpc?: number | '', trtFrfNm?: string | '', efflWqltStndrdNm?: string | '') => void; + sidoSigun? : string | undefined; + fcltyName? : string | undefined; +} + +export default function SearchInputText({label=undefined, setValue=()=>{}, sidoSigun=undefined, fcltyName=undefined} :thisProps) { + + const { ProcessFacilityList, searchProcessFacilitySearching } = useFoStore((state) => state); + const [isOpen, setIsOpen] = useState(false); + const [textValue, setTextValue] = useState(fcltyName || ''); + const wrapperRef = useRef<HTMLDivElement>(null); + const processFacilityList = ProcessFacilityList?.processFacilityList || []; + + const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { + const value = e.target.value; + setTextValue(value); + if (value) { + setIsOpen(true); + if(sidoSigun) { + searchProcessFacilitySearching(1, 4999, 'A', sidoSigun, '0', '0', '0', value); + } else { + searchProcessFacilitySearching(1, 4999, '0', '0', '0', '0', '0', value); + } + } else { + setIsOpen(false); + } + }, [searchProcessFacilitySearching, sidoSigun]); + + const onInputFocus = useCallback(() => { + setIsOpen(true); + if(sidoSigun) { + searchProcessFacilitySearching(1, 4999, 'A', sidoSigun, '0', '0', '0'); + } else { + searchProcessFacilitySearching(1, 4999, '0', '0', '0', '0', '0'); + } + }, [searchProcessFacilitySearching, sidoSigun]); + + useEffect(() => { + setTextValue(fcltyName || ''); + }, [fcltyName]); + + const handleSuggestionClick = useCallback((bscFctCd: string, bscFctNm: string, fctCpc: number | '', trtFrfNm: string | '', efflWqltStndrdNm: string | '') => { + console.log('bscFctNm : ', bscFctNm); + console.log('bscFctCd : ', bscFctCd); + console.log('fctCpc : ', fctCpc); + console.log('trtFrfNm : ', trtFrfNm); + console.log('efflWqltStndrdNm : ', efflWqltStndrdNm); + setTextValue(bscFctNm); + setValue(bscFctCd, bscFctNm, fctCpc, trtFrfNm, efflWqltStndrdNm); + setIsOpen(false); + }, [setValue]); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + return ( + <div className={'relative'} ref={wrapperRef}> + {label && <Label className="w-20 text-xl text-gray-600 justify-self-center">{label}</Label>} + <input + type="text" + value={textValue} + onChange={handleInputChange} + onFocus={onInputFocus} + className="w-[200px] ml-6 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + placeholder={`${label}을(를) 입력하세요`} + /> + {isOpen && processFacilityList.length > 0 && ( + <div className="absolute z-10 w-[300px] px-2 py-1 !mt-5 ml-20 bg-white border border-gray-300 rounded-md shadow-lg h-90 overflow-auto"> + <div className="max-h-72"> + {processFacilityList.map((item : facilityListProps, index : number) => ( + <div + key={index} + className="w-full text-left pl-1 pr-10 py-2 hover:bg-gray-100 cursor-pointer overflow-auto" + onClick={() => handleSuggestionClick(item.bscFctCd, item.bscFctNm, Number(item.fctCpc), item.trtFrfNm, item.efflWqltStndrdNm)} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + handleSuggestionClick(item.bscFctCd, item.bscFctNm, Number(item.fctCpc), item.trtFrfNm, item.efflWqltStndrdNm); + } + }} + > + {item.sido} {item.gugun} <span className="float-right">{item.bscFctNm}</span> + </div> + ))} + </div> + </div> + )} + </div> + ) +} \ No newline at end of file diff --git a/apps/fo/app/favicon.ico b/apps/fo/app/favicon.ico new file mode 100644 index 000000000..718d6fea4 Binary files /dev/null and b/apps/fo/app/favicon.ico differ diff --git a/apps/fo/app/globals.css b/apps/fo/app/globals.css new file mode 100644 index 000000000..4ff2b8802 --- /dev/null +++ b/apps/fo/app/globals.css @@ -0,0 +1,7 @@ +@import "../../../packages/common/components/src/base.css"; + +html, +body { + font-family: "Noto Sans KR", sans-serif; + font-size: 10px; +} \ No newline at end of file diff --git a/apps/fo/app/layout.tsx b/apps/fo/app/layout.tsx new file mode 100644 index 000000000..352c25034 --- /dev/null +++ b/apps/fo/app/layout.tsx @@ -0,0 +1,20 @@ +"use client" +import Base from "./base"; +import "./globals.css"; +import { StoreProvider } from "./store"; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + <html lang="en"> + <body> + <StoreProvider> + <Base>{children}</Base> + </StoreProvider> + </body> + </html> + ); +} diff --git a/apps/fo/app/page.tsx b/apps/fo/app/page.tsx new file mode 100644 index 000000000..823e5a408 --- /dev/null +++ b/apps/fo/app/page.tsx @@ -0,0 +1,44 @@ +"use client" + +import Link from "next/link"; +import { Button } from "@common/components/ui"; + +export default function Home() { + return ( + <div className="grid grid-cols-12 gap-2"> + <div className="col-span-2"> + <Link href={"./pages/processFacility"}><Button>처리시설 시설정보</Button></Link> + </div> + <div className="col-span-2"> + <Link href={"./pages/flowRate"}><Button>시설별 유량현황</Button></Link> + </div> + <div className="col-span-2"> + <Link href={"./pages/dashboard"}><Button>대시보드</Button></Link> + </div> + <div className="col-span-2"> + <Link href={"./pages/processArea"}><Button>처리구역정보</Button></Link> + </div> + <div className="col-span-2"> + <Link href={"./pages/facilityPrecipitation"}><Button>시설별 강수현황</Button></Link> + </div> + <div className="col-span-2"> + <Link href={"./pages/facilityWaterQlty"}><Button>시설별 수질현황</Button></Link> + </div> + <div className="col-span-2"> + <Link href={"./pages/planSewage"}><Button>계획하수량 산정지표 현황</Button></Link> + </div> + <div className="col-span-2"> + <Link href={"./pages/sewageOccur"}><Button>오수발생량 추정</Button></Link> + </div> + <div className="col-span-2"> + <Link href={"./pages/underWaterInput"}><Button>지하수 유입량 추정</Button></Link> + </div> + <div className="col-span-2"> + <Link href={"./pages/rdi"}><Button>RDII 조회</Button></Link> + </div> + <div className="col-span-2"> + <Link href={"./pages/map"}><Button>지도보기</Button></Link> + </div> + </div> + ); +} diff --git a/apps/fo/app/pages/dashboard/drainageEquipment/page.tsx b/apps/fo/app/pages/dashboard/drainageEquipment/page.tsx new file mode 100644 index 000000000..f6f1188de --- /dev/null +++ b/apps/fo/app/pages/dashboard/drainageEquipment/page.tsx @@ -0,0 +1,98 @@ +"use client" + +import { useFoStore } from '@/app/store'; +import type { DrainageEquipmentListProps, TableUpperProps } from '@/app/store/dashboard'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@common/components'; +import { useEffect, useState } from 'react'; + +export type DrainageEquipmentProps = { + searchYear?: string; + fcltyList?: { bscFctCd: string, bscFctNm: string, fcltyCpc: number | '', trtFrfNm: string, efflWqltStndrdNm: string }[]; +}; + +export default function DrainageEquipment({searchYear = '', fcltyList = []}: DrainageEquipmentProps) { + + const { + Common, + DrainageEquipment, + searchDrainageEquipment, + } = useFoStore((state) => state); + + const { + isInit, + } = Common; + + const { + upHeadListDrainageEquipment, + drainageEquipmentList, + } = DrainageEquipment; + + const [hasSearched, setHasSearched] = useState(false); + // 이전 시설코드 저장 + const [prevFacilityCode, setPrevFacilityCode] = useState<string>(''); + + const tableData = fcltyList; + + useEffect(() => { + if (!isInit && tableData[0]?.bscFctCd && (tableData[0].bscFctCd !== prevFacilityCode || !hasSearched)) { + searchDrainageEquipment(searchYear, tableData[0].bscFctCd, false); + setHasSearched(true); + setPrevFacilityCode(tableData[0].bscFctCd); + } + }, [fcltyList, isInit, searchDrainageEquipment, searchYear, tableData, hasSearched, prevFacilityCode]); + + // 연도가 바뀌면 hasSearched를 false로 리셋 + useEffect(() => { + setHasSearched(false); + }, [searchYear]); + + // 시설계획 table header 변경 + const HeadMakeColSpan = ({ upHeadList }: { upHeadList: TableUpperProps[] }) => { + return ( + <> + <TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) => + (head.id === 'part1' ? <TableHead rowSpan={3} className='w-60' key={head.id}>구분</TableHead> : + head.id === 'part2' ? <TableHead rowSpan={3} className='w-60' key={head.id}>구분</TableHead> : + head.id == 'nowSum' ? <TableHead rowSpan={2} colSpan={3} key={head.id} style={{ whiteSpace: 'pre-line' }}>{'현재\n(기준년도)'}</TableHead> : + head.id == 'stepOneSum' ? <TableHead rowSpan={1} colSpan={12} key={head.id} style={{ whiteSpace: 'pre-line' }}>{'변경(금회계획)'}</TableHead> : + head.id == 'etc' ? <TableHead rowSpan={3} key={head.id}>비고</TableHead> : null))} + </TableRow> + <TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) => + (head.id === 'stepOneSum' ? <TableHead rowSpan={1} colSpan={3} key={head.id}>1단계</TableHead> : + head.id === 'stepTwoSum' ? <TableHead rowSpan={1} colSpan={3} key={head.id}>2단계</TableHead> : + head.id === 'stepThreeSum' ? <TableHead rowSpan={1} colSpan={3} key={head.id}>3단계</TableHead> : + head.id === 'stepFourSum' ? <TableHead rowSpan={1} colSpan={3} key={head.id}>4단계</TableHead> : null))} + </TableRow> + <TableRow className="bg-gray-50"> + {upHeadList.map((head: TableUpperProps) => { + if (head.upName === 'upChangeRe2' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4' || head.upName === 'upChangeRe5' || head.upName === 'upChangeRe6') { + return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>; + } + })} + </TableRow> + </> + ) + } + + const upHeadList = HeadMakeColSpan({upHeadList:upHeadListDrainageEquipment}); + return ( + <div className="overflow-x-auto"> + <Table variant="secondary" className='w-[130%]'> + <TableHeader> + {upHeadList} + </TableHeader> + <TableBody> + {drainageEquipmentList.map((el: DrainageEquipmentListProps, index) => ( + <TableRow key={index+"row"}> + {upHeadListDrainageEquipment.map((head) => ( + (head.id==='nowSum')? + <TableCell className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{333}</TableCell> + :<TableCell className="text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id as keyof DrainageEquipmentListProps]}</TableCell> + ))} + </TableRow> + ))} + </TableBody> + </Table> + </div> + ) +} diff --git a/apps/fo/app/pages/dashboard/facilityInfo/modal/page.tsx b/apps/fo/app/pages/dashboard/facilityInfo/modal/page.tsx new file mode 100644 index 000000000..94bc28500 --- /dev/null +++ b/apps/fo/app/pages/dashboard/facilityInfo/modal/page.tsx @@ -0,0 +1,184 @@ +"use client" +import type { OperationalStatusListProps } from "@/app/store/dashboard"; +import { CHART_FORM, MainChart, type ChartConfig } from "@common/business_components"; +import { Button } from "@common/business_components/ui/table/lib/button"; +import html2canvas from "html2canvas"; +import { useEffect, useState, useRef } from "react"; + +type yearProps = { + label: string; + 평균 : number | string | ''; + 최대 : number | string | ''; + 최소 : number | string | ''; +} + +type summerProps = { + 전기간 : number | string | ''; + 강우일3mm이상 : number | string | ''; + 비강우시 : number | string | ''; +} + +type winterProps = { + 전기간 : number | string | ''; + 청천시최대 : number | string | ''; + 청천시평균 : number | string | ''; + 청천시최소 : number | string | ''; +} + +type dashBoardChartModalProps = { + title : string; + item : string; + period : string; + modalData : OperationalStatusListProps[]; + waterQlty : string; +} + +export default function DashBoardChartModal({item='', period='', title='',modalData=[], waterQlty=''} : dashBoardChartModalProps) { + + const [chartData, setChartData] = useState<yearProps[] | summerProps[] | winterProps[]>([]); + const [chartConfig, setChartConfig] = useState<ChartConfig>({}); + const chartRef = useRef<HTMLDivElement>(null); + const chartOption = { + chartForm : CHART_FORM.BAR, + chartConfig : chartConfig, + chartData : chartData, + chartTitle : title, + chartClassName : "h-[calc(100%-15%)] w-[90%] mt-5", + graphSize : 40, + yFieldText : item === 'flowRate' ? '유량(㎥/일)' : item === 'waterQlty' ? '수질(mg/L)' : item === 'chongin' ? '수질(mg/L)' : '', + showLegend : true + } + + const handleDownload = () => { + if (!chartRef.current) { + return alert('결과 저장에 실패했습니다.'); + } + html2canvas(chartRef.current).then((canvas) => { + const link = document.createElement('a'); + document.body.appendChild(link); + link.href = canvas.toDataURL('image/png'); + link.download = 'result.png'; + link.click(); + document.body.removeChild(link); + }); + } + + /** + * useEffect + */ + useEffect(() => { + if(period === 'year'){ + const filteredData: yearProps[] = modalData.map((item) => { + if(waterQlty.trim().includes('T-N') || waterQlty.trim().includes('T-P')){ + return { + label: item.waterQlty, + 평균: typeof item.yearAvg === 'string' ? Number(item.yearAvg.replace(/,/g, '')).toFixed(3) : (item.yearAvg || 0), + 최대: typeof item.yearMax === 'string' ? Number(item.yearMax.replace(/,/g, '')).toFixed(3) : (item.yearMax || 0), + 최소: typeof item.yearMin === 'string' ? Number(item.yearMin.replace(/,/g, '')).toFixed(3) : (item.yearMin || 0), + } + } + else{ + return { + label: item.waterQlty, + 평균: typeof item.yearAvg === 'string' ? Number(item.yearAvg.replace(/,/g, '')).toFixed(1) : (item.yearAvg || 0), + 최대: typeof item.yearMax === 'string' ? Number(item.yearMax.replace(/,/g, '')).toFixed(1) : (item.yearMax || 0), + 최소: typeof item.yearMin === 'string' ? Number(item.yearMin.replace(/,/g, '')).toFixed(1) : (item.yearMin || 0), + } + } + }) + if (item === 'flowRate') { + setChartData(filteredData.slice(0,5)); + } else if (item === 'waterQlty') { + setChartData(filteredData.slice(5,11)); + } else { + setChartData(filteredData.slice(11,13)); + } + } + else if (period === 'summer') { + const filteredData: summerProps[] = modalData.map((item) => { + if(waterQlty.trim().includes('T-N') || waterQlty.trim().includes('T-P')){ + return { + label: item.waterQlty, + 전기간: typeof item.summerAll === 'string' ? Number(item.summerAll.replace(/,/g, '')).toFixed(3) : (item.summerAll || 0), + 강우일3mm이상: typeof item.summerThree === 'string' ? Number(item.summerThree.replace(/,/g, '')).toFixed(3) : (item.summerThree || 0), + 비강우시: typeof item.summerRain === 'string' ? Number(item.summerRain.replace(/,/g, '')).toFixed(3) : (item.summerRain || 0), + } + }else{ + return { + label: item.waterQlty, + 전기간: typeof item.summerAll === 'string' ? Number(item.summerAll.replace(/,/g, '')).toFixed(1) : (item.summerAll || 0), + 강우일3mm이상: typeof item.summerThree === 'string' ? Number(item.summerThree.replace(/,/g, '')).toFixed(1) : (item.summerThree || 0), + 비강우시: typeof item.summerRain === 'string' ? Number(item.summerRain.replace(/,/g, '')).toFixed(1) : (item.summerRain || 0), + } + } + }) + if (item === 'flowRate') { + setChartData(filteredData.slice(0,5)); + } else if (item === 'waterQlty') { + setChartData(filteredData.slice(5,11)); + } else { + setChartData(filteredData.slice(11,13)); + } + } + else if (period === 'winter') { + const filteredData: winterProps[] = modalData.map((item) => { + return { + label: item.waterQlty, + 전기간: typeof item.winterAll === 'string' ? Number(item.winterAll.replace(/,/g, '')).toFixed(3) : (item.winterAll || 0), + 청천시최대: typeof item.winterSunnyMax === 'string' ? Number(item.winterSunnyMax.replace(/,/g, '')).toFixed(3) : (item.winterSunnyMax || 0), + 청천시평균: typeof item.winterSunnyAvg === 'string' ? Number(item.winterSunnyAvg.replace(/,/g, '')).toFixed(3) : (item.winterSunnyAvg || 0), + 청천시최소: typeof item.winterSunnyMin === 'string' ? Number(item.winterSunnyMin.replace(/,/g, '')).toFixed(3) : (item.winterSunnyMin || 0), + } + }) + setChartData(filteredData); + + if (item === 'flowRate') { + setChartData(filteredData.slice(0,5)); + } else if (item === 'waterQlty') { + setChartData(filteredData.slice(5,11)); + } else { + setChartData(filteredData.slice(11,13)); + } + } else { + setChartData([]); + } + }, [item, modalData, period, waterQlty]); + + useEffect(() => { + const newChartConfig = chartData.reduce((config, element) => { + const label = 'label' in element ? element.label : ''; + if (label) { + config[label] = { + label: label, + color : "hsl(var(--chart-1))" + }; + } + return config; + }, {} as Record<string, { label: string; color: string; }>) satisfies ChartConfig; + setChartConfig(newChartConfig); + }, [chartData]); + + return ( + <> + <div className="flex justify-end"> + <span className="text-[13px] font-bold mt-2 w-[260px] h-[30px] -mb-9"> + (하절기 : 6/1~8/31 동절기 : 1/1~2/28) + </span> + <Button className="text-[13px] w-[80px] h-[30px] -mb-9 mr-20" onClick={handleDownload}>캡처</Button> + </div> + <div ref={chartRef}> + <MainChart + chartForm={chartOption.chartForm} + chartConfig={chartOption.chartConfig} + chartData={chartOption.chartData} + chartTitle={chartOption.chartTitle} + chartClassName={chartOption.chartClassName} + graphSize={chartOption.graphSize} + yFieldText={chartOption.yFieldText} + showLegend={chartOption.showLegend} + /> + </div> + </> + ); +} + diff --git a/apps/fo/app/pages/dashboard/facilityInfo/page.tsx b/apps/fo/app/pages/dashboard/facilityInfo/page.tsx new file mode 100644 index 000000000..767ed54e4 --- /dev/null +++ b/apps/fo/app/pages/dashboard/facilityInfo/page.tsx @@ -0,0 +1,277 @@ +"use client" +import { useFoStore } from "@/app/store"; +import type { TableUpperProps } from "@/app/store/processFacility"; +import { ContentsDiv, UiModal, UiTable } from "@common/business_components"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components"; +import { useEffect, useState } from "react"; +import DashBoardChartModal from "./modal/page"; + +export type facilityInfoProps = { + searchYear?: string; + fcltyList?: { bscFctCd: string, bscFctNm: string, fcltyCpc: number | '', trtFrfNm: string, efflWqltStndrdNm: string }[]; +}; + +// 시설계획 table header 변경 +const headMakeColSpanPlanFclty = (upHeadList: TableUpperProps[], planYearList: string[]) => { + 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={7} style={{ whiteSpace: 'pre-line' }}>{'시설계획\n(금회)'}</TableHead>:null))} + </TableRow> + <TableRow> + {upHeadList.map((head: TableUpperProps, index: number) => + (head.upName === 'upChangeRe'?<TableHead key={head.id} style={{ whiteSpace: 'pre-line' }}>{index===0?head.title:head.title+'\n('+planYearList[index-1]+')'}</TableHead>:null))} + </TableRow> + </> ) +} + +// 시설계획 table header 변경 +const headMakeColSpanOperationStatus = (upHeadList: TableUpperProps[], searchYear: string) => { + return ( <> + <TableRow> + <TableHead rowSpan={1} colSpan={17} style={{ whiteSpace: 'pre-line' }}>{searchYear+'년도 운영실태'}</TableHead> + </TableRow> + <TableRow>{ + upHeadList.map((head: TableUpperProps) => + (head.upName === 'upChangeRe1'&&head.upSequnce===1?<TableHead key={head.id} rowSpan={2}colSpan={2} style={{ whiteSpace: 'pre-line' }}>{head.title}</TableHead>: + !head.upSequnce?<TableHead style={{ whiteSpace: 'pre-line' }} rowSpan={2}key={head.id}>{head.title}</TableHead>: + head.upName === 'upChangeRe2'&&head.upSequnce===1?<TableHead key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'연간 운영실태'}</TableHead>: + head.upName === 'upChangeRe3'&&head.upSequnce===1?<TableHead key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'하절기 운영실태\n(6~8월)'}</TableHead>: + head.upName === 'upChangeRe4'&&head.upSequnce===1?<TableHead key={head.id} colSpan={4} style={{ whiteSpace: 'pre-line' }}>{'동절기 운영실태\n(1~2월)'}</TableHead>: + head.upName === 'upChangeRe5'&&head.upSequnce===1?<TableHead key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'지하수 유입량 추정'}</TableHead>:null))} + </TableRow> + <TableRow> + {upHeadList.map((head: TableUpperProps) => + (head.upName === 'upChangeRe2'||head.upName === 'upChangeRe3'||head.upName === 'upChangeRe4'||head.upName === 'upChangeRe5'?<TableHead style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>:null))} + </TableRow> + </> ) +} + +export default function ProcessFacilitySearch({searchYear = '', fcltyList = []}: facilityInfoProps & { onFacilityChange: (facility: { bscFctCd: string, bscFctNm: string, fcltyCpc: number | '', trtFrfNm: string, efflWqltStndrdNm: string }) => void }) { + // 입력값 상태 관리 + const { + Common, + FcltyMain, + PlanFclty, + OperationStatus, + searchOperationStatus, + searchPlanInfo + } = useFoStore((state) => state); + + const [modalItem, setModalItem] = useState<{item: string, period: string, title: string, waterQlty: string} | null>(null); + const [hasSearched, setHasSearched] = useState(false); + const [prevFacilityCode, setPrevFacilityCode] = useState<string>(''); + + const { + isInit, + } = Common; + + // Tab1 + // fclty 관련 + const { + upHeadListFclty, + } = FcltyMain; + + // 시설계획 관련 + const { + upHeadListPlanFclty, + planFcltyList, + planYearList + } = PlanFclty; + + // 그리드 관련 + const { + upHeadListOperationStatus, + operationalStatusList + } = OperationStatus; + + const closeModal = () => { + setModalItem(null); + }; + + // const fcltyListArr = useMemo(() => [fcltyList], [fcltyList]); + + const tableData = fcltyList; + + // key 변환 + const mappedTableData = tableData.map(item => ({ + fcltyNm: item.bscFctNm, + facilityCapacity: item.fcltyCpc, + publicMethod: item.trtFrfNm, + location: item.efflWqltStndrdNm, + })); + + const fncLoadModal = (item: string, period: string, waterQlty: string) => { + // 유량 + if(item === 'flowRate'){ + if(period === 'year'){ + setModalItem({item: 'flowRate', period: 'year', title: '연간 유량', waterQlty: waterQlty}); + } + else if(period === 'summer'){ + setModalItem({item: 'flowRate', period: 'summer', title: '하절기 유량', waterQlty: waterQlty}); + } + else if(period === 'winter'){ + setModalItem({item: 'flowRate', period: 'winter', title: '동절기 유량', waterQlty: waterQlty}); + } + } + // 수질 + else if (item == 'waterQlty'){ + if(period === 'year'){ + setModalItem({item: 'waterQlty', period: 'year', title: '연간 수질', waterQlty: waterQlty}); + } + else if(period === 'summer'){ + setModalItem({item: 'waterQlty', period: 'summer', title: '하절기 수질', waterQlty: waterQlty}); + } + else if(period === 'winter'){ + setModalItem({item: 'waterQlty', period: 'winter', title: '동절기 수질', waterQlty: waterQlty}); + } + } + // 총인 + else { + if(period === 'year'){ + setModalItem({item: 'chongin', period: 'year', title: '총인설비 연간 수질', waterQlty: waterQlty}); + } + else if(period === 'summer'){ + setModalItem({item: 'chongin', period: 'summer', title: '총인설비 하절기 수질', waterQlty: waterQlty}); + } + else if(period === 'winter'){ + setModalItem({item: 'chongin', period: 'winter', title: '총인설비 동절기 수질', waterQlty: waterQlty}); + } + } + } + + useEffect(() => { + if (!isInit && fcltyList[0]?.bscFctCd && (fcltyList[0].bscFctCd !== prevFacilityCode || !hasSearched)) { + searchOperationStatus(false, searchYear, fcltyList[0].bscFctCd); + searchPlanInfo(searchYear, fcltyList[0].bscFctCd); + setHasSearched(true); + setPrevFacilityCode(fcltyList[0].bscFctCd); + planFcltyList.forEach((el: { [key: string]: string | number | boolean }, index) => { + if (index === 6) { + el['item'] = (Number(searchYear)-2).toString(); + el['provisionalApproval'] = Number(fcltyList[0]?.fcltyCpc)* 0.6; + } else if (index === 7) { + el['item'] = (Number(searchYear)-1).toString(); + el['provisionalApproval'] = Number(fcltyList[0]?.fcltyCpc)* 0.6; + } else if (index === 8) { + el['item'] = (Number(searchYear)-0).toString(); + el['provisionalApproval'] = Number(fcltyList[0]?.fcltyCpc)* 0.6; + } + }); + } + }, [isInit, searchOperationStatus, searchYear, fcltyList, hasSearched, prevFacilityCode, searchPlanInfo, planFcltyList]); + // 연도가 바뀌면 hasSearched를 false로 리셋 + useEffect(() => { + setHasSearched(false); + }, [searchYear]); + + useEffect(() => { + operationalStatusList.map((item: { [key: string]: string | number | boolean },index) => { + if(index===8 || index===9 || index===10 || index===11 || index===12 || index===13 || index===14 || index===15){ + item['yearAvg'] = Number(item['yearAvg']).toFixed(3); + item['yearMax'] = item['yearMax']+'%'; + item['yearMin'] = item['yearMin']+'%'; + item['summerAll'] = item['summerAll']+'%'; + item['summerThree'] = item['summerThree']+'%'; + item['summerRain'] = item['summerRain']+'%'; + item['winterAll'] = item['winterAll']+'%'; + item['winterSunnyAvg'] = item['winterSunnyAvg']+'%'; + item['winterSunnyMax'] = item['winterSunnyMax']+'%'; + item['winterSunnyMin'] = item['winterSunnyMin']+'%'; + } + + }); + }, [operationalStatusList]); + + const upChangeRePlanFclty = headMakeColSpanPlanFclty(upHeadListPlanFclty, planYearList); + const upChangeHeadListOperationStatus = headMakeColSpanOperationStatus(upHeadListOperationStatus, searchYear); + return ( + <> + <UiTable headName={""} tableData={mappedTableData} headlist={upHeadListFclty} pageSize={0} total={0} cellClick={ undefined } onPageChange={() => { } } /> + <ContentsDiv> + <div className="grid grid-cols-12 gap-1"> + <div className="col-span-5"> + <Table variant="secondary"> + <TableHeader> + {upChangeRePlanFclty} + </TableHeader> + <TableBody> + {planFcltyList.map((el: { [key: string]: string | number | boolean },index) => ( + <TableRow key={index+"row"} > + {upHeadListPlanFclty.map((head) => ( + ((index === 6 || index === 7 || index === 8) && (head.id === 'provisionalApproval' || head.id === 'stepTwo'))? + <TableCell className="text-right" colSpan={3} style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell> + :((index === 6 || index === 7 || index === 8) && (head.id === 'now' || head.id === 'stepOne' || head.id === 'stepThree' || head.id === 'stepFour'))? + null + :((index === 5) && (head.id === 'provisionalApproval' || head.id === 'stepTwo'))? + <TableCell colSpan={3} className="text-center border border-[var(--border)] bg-[rgb(234,243,251)] text-[rgb(100,119,132)]" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell> + :((index === 5) && (head.id === 'now' || head.id === 'stepOne' || head.id === 'stepThree'|| head.id === 'stepFour'))? + null + :(index < 5 || index > 5)&& (index!= 4) && (head.id ==='provisionalApproval'||head.id ==='now'||head.id ==='stepOne'||head.id ==='stepTwo'||head.id ==='stepThree'||head.id ==='stepFour') + ?<TableCell className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell> + :(head.id === "provisionalApproval" && index === 4) + ?<TableCell colSpan={6} className="text-right border border-[var(--border)] text-[rgb(100,119,132)]" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{(Number(fcltyList[0]?.fcltyCpc)* 0.6).toLocaleString('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1 })} </TableCell> + :((head.id ==='now'||head.id ==='stepOne'||head.id ==='stepTwo'||head.id ==='stepThree'||head.id ==='stepFour') && index === 4) + ?null + :(head.id === 'item' && index != 5)? + <TableCell className="text-center text-[rgb(100,119,132)]" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell> + :<TableCell className="text-center border border-[var(--border)] bg-[rgb(234,243,251)] text-[rgb(100,119,132)]" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell> + ))} + </TableRow> + ))} + </TableBody> + </Table> + </div> + <div className="col-span-7 h-[600px] overflow-x-auto"> + <div className="w-[200%]"> + <Table variant="secondary"> + <TableHeader> + {upChangeHeadListOperationStatus} + </TableHeader> + <TableBody> + {operationalStatusList.map((el: { [key: string]: string | number | boolean },index) => ( + <TableRow key={index+"row"} > + {upHeadListOperationStatus.map((head) => ( + (head.id === 'flowRate' && index === 0)?<TableCell rowSpan={5} className="text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>: + (head.id === 'flowRate' && index === 5)?<TableCell rowSpan={6} className="text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>: + (head.id === 'flowRate' && index === 11)?<TableCell rowSpan={2} className="text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>: + (head.id === 'groundWaterAvg'||head.id === 'groundWaterSummer'||head.id === 'groundWaterWinter')&&(index === 0)?<TableCell rowSpan={7} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>: + (head.id === 'groundWaterAvg'||head.id === 'groundWaterSummer'||head.id === 'groundWaterWinter')&&(index === 7)?<TableCell rowSpan={6} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>: + (head.id === 'rdi'&&index === 0)?<TableCell rowSpan={7} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>: + (head.id === 'rdi'&&index === 7)?<TableCell rowSpan={6} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}></TableCell>: + (head.id === 'rdiRain'&&index === 0)?<TableCell rowSpan={7} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el['rdiRain']}</TableCell>: + (head.id === 'rdiRain'&&index === 7)?<TableCell rowSpan={6} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}></TableCell>: + (head.id === 'waterQlty')?<TableCell className="text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>: + ((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=0 && index<=4))?<TableCell className="bg-yellow-50 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>: + ((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=5 && index<=10))?<TableCell className="bg-green-50 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>: + ((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=11 && index<=12))?<TableCell className="bg-blue-50 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>: + ((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=0 && index<=4))?<TableCell className="bg-yellow-100 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>: + ((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=5 && index<=10))?<TableCell className="bg-green-100 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>: + ((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=11 && index<=12))?<TableCell className="bg-blue-100 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>: + ((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=0 && index<=4))?<TableCell className="bg-yellow-200 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>: + ((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=5 && index<=10))?<TableCell className="bg-green-200 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>: + ((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=11 && index<=12))?<TableCell className="bg-blue-200 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>: + null + ))} + </TableRow> + ))} + </TableBody> + </Table> + </div> + </div> + </div> + </ContentsDiv> + {modalItem && (modalItem.item === 'flowRate' || modalItem.item === 'waterQlty' || modalItem.item === 'chongin') && ( + <UiModal + body={<DashBoardChartModal item={modalItem.item} period={modalItem.period} title={modalItem.title} modalData={operationalStatusList} waterQlty={modalItem.waterQlty}></DashBoardChartModal>} + bodyClass="sm:max-w-[80%] h-[88%]" + open={true} + onOpenChange={(open) => !open && closeModal()} + noHeaderYn="Y" + noFooterYn="Y" + /> + )} + </> + ); +} diff --git a/apps/fo/app/pages/dashboard/maingraph/page.tsx b/apps/fo/app/pages/dashboard/maingraph/page.tsx new file mode 100644 index 000000000..5ee9eeda0 --- /dev/null +++ b/apps/fo/app/pages/dashboard/maingraph/page.tsx @@ -0,0 +1,243 @@ +"use client" +import { useFoStore } from "@/app/store"; +import type { lineChartDataProps } from "@/app/store/dashboard"; +import { CHART_FORM, type ChartConfig, ContentsDiv, MainChart } from "@common/business_components"; +import { Card, CardContent } from "@common/business_components/ui/chart/lib/card"; +import ListDiv from "@common/business_components/ui/list_div"; +import { useEffect, useState } from "react"; + +type facilityInfoProps = { + searchYear?: string; + fcltyList?: { bscFctCd: string, bscFctNm: string, fcltyCpc: number | '', trtFrfNm: string, efflWqltStndrdNm: string }[]; +}; + +export default function DashBoardChart({searchYear = '', fcltyList = []}: facilityInfoProps) { + const {MainGraph + ,searchMainGraph + } = useFoStore((state) => state); + + const [hasSearched, setHasSearched] = useState(false); + const [prevFacilityCode, setPrevFacilityCode] = useState<string>(''); + const [chart_tpTp, setChart_tpTp] = useState<{ + chartData1: lineChartDataProps[]; + chartData2: lineChartDataProps[]; + }>(); + const [chart_tntn, setChart_tntn] = useState<{ + chartData1: lineChartDataProps[]; + chartData2: lineChartDataProps[]; + }>(); + const [chart_inflBodInflTn, setChart_inflBodInflTn] = useState<{ + chartData1: lineChartDataProps[]; + chartData2: lineChartDataProps[]; + }>(); + const [chart_inflBodInflTp, setChart_inflBodInflTp] = useState<{ + chartData1: lineChartDataProps[]; + chartData2: lineChartDataProps[]; + }>(); + + const { + chart_inflowRain, + chart_bodRain, + chart_inflTp, + chart_chongInTp, + chart_efflTp, + chart_inflTn, + chart_efflTn, + chart_inflBod, + } = MainGraph; + + /** + * 차트 Config + */ + const colors = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#00b50c", "#D4A5A5"]; + + //유입하수량-강수량 + const chartConfig_inflowRain = { + barVal0: { + label: '강수량', + color: colors[2], + }, + lineVal0: { + label: '시설용량', + color: colors[1], + }, + lineVal1: { + label: '유입량', + color: colors[4], + } + } satisfies ChartConfig; + //강우량-유입BOD + const chartConfig_rainBod = { + barVal0: { + label: '강수량', + color: colors[2], + }, + lineVal0: { + label: '유입BOD', + color: colors[0], + }, + } satisfies ChartConfig; + + //총인유입T-P-방류T-P + const chartConfig_tptp = { + chartData1: { + label: '총인유입T-P', + color: colors[0], + }, + chartData2: { + label: '방류T-P', + color: colors[1], + }, + } satisfies ChartConfig; + + // 유입T-N-방류T-N + const chartConfig_tntn = { + chartData1: { + label: '유입T-N', + color: colors[0], + }, + chartData2: { + label: '방류T-N', + color: colors[1], + }, + } satisfies ChartConfig; + + // 유입BOD-유입T-N + const chartConfig_inflBodInflTn = { + chartData1: { + label: '유입BOD', + color: colors[0], + }, + chartData2: { + label: '유입T-N', + color: colors[1], + }, + } satisfies ChartConfig; + + // 유입BOD-유입T-P + const chartConfig_inflBodInflTp = { + chartData1: { + label: '유입BOD', + color: colors[0], + }, + chartData2: { + label: '유입T-P', + color: colors[1], + }, + + } satisfies ChartConfig; + + useEffect(() => { + if (fcltyList.length > 0 && fcltyList[0]?.bscFctCd && (fcltyList[0].bscFctCd !== prevFacilityCode || !hasSearched)) { + searchMainGraph(searchYear, fcltyList[0].bscFctCd); + setHasSearched(true); + setPrevFacilityCode(fcltyList[0].bscFctCd); + } + }, [searchYear, fcltyList, searchMainGraph, hasSearched, chart_inflTp, chart_efflTp, prevFacilityCode]); + + useEffect(() => { + if (!chart_chongInTp || !chart_efflTp) return; + const chartTpTpData = { + chartData1: chart_chongInTp, + chartData2: chart_efflTp + }; + setChart_tpTp(chartTpTpData); + }, [chart_chongInTp, chart_efflTp]); + + useEffect(() => { + if (!chart_inflTn || !chart_efflTn) return; + const chartTntnData = { + chartData1: chart_inflTn, + chartData2: chart_efflTn + }; + setChart_tntn(chartTntnData); + }, [chart_inflTn, chart_efflTn]); + + useEffect(() => { + if (!chart_inflBod || !chart_inflTn) return; + const chartInflBodInflTnData = { + chartData1: chart_inflBod, + chartData2: chart_inflTn + }; + setChart_inflBodInflTn(chartInflBodInflTnData); + }, [chart_inflBod, chart_inflTn]); + + useEffect(() => { + if (!chart_inflBod || !chart_inflTp) return; + const chartInflBodInflTpData = { + chartData1: chart_inflBod, + chartData2: chart_inflTp + }; + setChart_inflBodInflTp(chartInflBodInflTpData); + }, [chart_inflBod, chart_inflTp]); + + // 연도가 바뀌면 hasSearched를 false로 리셋 + useEffect(() => { + setHasSearched(false); + }, [searchYear]); + + return ( + <> + <ContentsDiv> + <ListDiv className="h-[300px]"> + <Card className="h-full"> + <CardContent className="h-full"> + <MainChart chartForm={CHART_FORM.LINEBAR} chartConfig={chartConfig_inflowRain} chartData={chart_inflowRain} chartTitle="유입하수량-강수량" yFieldText="유입하수량(m³/일)" yFieldText2="강수량(mm)"></MainChart> + </CardContent> + </Card> + </ListDiv> + <ListDiv className="h-[300px]"> + <Card className="h-full"> + <CardContent className="h-full"> + <MainChart chartForm={CHART_FORM.LINEBAR} chartConfig={chartConfig_rainBod} chartData={chart_bodRain} chartTitle="유입BOD-강수량" yFieldText="BOD(mg/L)" yFieldText2="강수량(mm)"></MainChart> + </CardContent> + </Card> + </ListDiv> + <ListDiv className="h-[300px]"> + <Card className="h-full"> + <CardContent className="h-full"> + <MainChart chartForm={CHART_FORM.LINE} chartConfig={chartConfig_tptp} chartData={chart_tpTp} chartTitle="총인유입T-P-방류T-P" yFieldText="T-P(mg/L)"></MainChart> + </CardContent> + </Card> + </ListDiv> + {/* 유입T-N-방류T-N */} + <ListDiv className="h-[300px]"> + <Card className="h-full"> + <CardContent className="h-full"> + <MainChart chartForm={CHART_FORM.LINE} chartConfig={chartConfig_tntn} chartData={chart_tntn} chartTitle="유입T-N-방류T-N" yFieldText="T-N(mg/L)"></MainChart> + </CardContent> + </Card> + </ListDiv> + {/* 유입BOD-유입T-N */} + <ListDiv className="h-[300px]"> + <Card className="h-full"> + <CardContent className="h-full"> + <MainChart chartForm={CHART_FORM.LINE} chartConfig={chartConfig_inflBodInflTn} chartData={chart_inflBodInflTn} chartTitle="유입BOD-유입T-N" yFieldText="농도(mg/L)"></MainChart> + </CardContent> + </Card> + </ListDiv> + {/* 유입BOD-유입T-P */} + <ListDiv className="h-[300px]"> + <Card className="h-full"> + <CardContent className="h-full"> + <MainChart chartForm={CHART_FORM.LINE} chartConfig={chartConfig_inflBodInflTp} chartData={chart_inflBodInflTp} chartTitle="유입BOD-유입T-P" yFieldText="농도(mg/L)"></MainChart> + </CardContent> + </Card> + </ListDiv> + </ContentsDiv> + </> + ); + {/* <div>> + <MainChart chartForm={CHART_FORM.DN} chartConfig={chartConfig} chartData={chartData} chartTitle="유입하수량-강수량"></MainChart> + </div> + <div className="col-span-6"> + <MainChart chartForm={CHART_FORM.LINEBAR} chartConfig={chartConfig} chartData={chart_inflowRain} chartTitle="유입BOD-강수량"></MainChart> + </div> + <div className="col-span-6"> + <MainChart chartForm={CHART_FORM.LINE} chartConfig={chartConfig} chartData={chartData} chartTitle="총인유입T-P-방류T-P"></MainChart> + </div> + <div className="col-span-6"> + <MainChart chartForm={CHART_FORM.LINE} chartConfig={chartConfig} chartData={chartData} chartTitle="유입T-N-방류T-N"></MainChart> + </div> + </div> */} +} diff --git a/apps/fo/app/pages/dashboard/page.tsx b/apps/fo/app/pages/dashboard/page.tsx new file mode 100644 index 000000000..09c452da4 --- /dev/null +++ b/apps/fo/app/pages/dashboard/page.tsx @@ -0,0 +1,335 @@ +"use client" +import { useFoStore } from "@/app/store"; +import { ContentsDiv, downloadExcel, downloadMultipleExcel, filePickedBlobArray, MainDiv, SelectBox, UITab, type SelectDataType } from "@common/business_components"; +import { Button, TabsContent } from "@common/components/ui"; +import { useEffect, useState } from "react"; +import DashBoardFclty from "../dashboardMain/page"; +/* import DrainageEquipment from "./drainageEquipment/page"; */ +import ProcessFacilitySearch from "./facilityInfo/page"; +import DashBoardChart from "./maingraph/page"; +import RainManageTab from "./rainManage/page"; +import DrainageEquipment from "./drainageEquipment/page"; + +const tabList = [ + { value: 'facilityInfo', label: '시설정보 및 운영현황' }, + { value: 'mainGraph', label: '주요그래프' }, + { value: 'rainManage', label: '강우일 운영현황' }, + //{ value: 'drainageEquipment', label: '배수설비 정비현황' } +]; + +export default function DashBoard() { + // 입력값 상태 관리 + const { Common, initializeCommonData,searchMainGraph, searchRainManage, searchOperationStatus, OperationStatus, RainManage, MainGraph, PlanFclty } = useFoStore((state) => state); + const [activeTab, setActiveTab] = useState<string>('facilityInfo'); + + const [filteredSigunData, setFilteredSigunData] = useState<SelectDataType[]>([{ text: "전체", val: '0' }]); + const [sidoValue, setSidoValue] = useState<string>('0'); + const [sigunValue, setSigunValue] = useState<string>('0'); + const { + isInit, + selectSearchYear, + btnClass, + selectSidoData, + selectSigunData, + } = Common; + + const { + planFcltyList + } = PlanFclty; + + const { + operationalStatusList + } = OperationStatus; + + const { + excelList + } = RainManage; + + const { + chart_inflowRain, + chart_bodRain, + chart_inflTp, + chart_chongInTp, + chart_efflTp, + chart_inflTn, + chart_efflTn, + } = MainGraph; + + /******************선택된 데이터******************/ + // 연도 + //const [searchYear, setSearchYear] = useState<string>(selectSearchYear[0]?.val || '0'); + const [searchYear, setSearchYear] = useState<string>('2023'); + // 시설명 + const [fcltyList, setFcltyList] = useState<{bscFctCd: string, bscFctNm: string, fcltyCpc: number | '', trtFrfNm: string | '', efflWqltStndrdNm: string | ''}>({ + bscFctCd: '26260PB001R', + bscFctNm: '수영', + fcltyCpc: 452000, + trtFrfNm: '표준활성슬러지법, MLE, MBR', + efflWqltStndrdNm: '500톤 이상(Ⅳ지역)' + }); + + const [searchFcltyList, setSearchFcltyList] = useState<{bscFctCd: string, bscFctNm: string, fcltyCpc: number | '', trtFrfNm: string | '', efflWqltStndrdNm: string | ''}>({ + bscFctCd: '', + bscFctNm: '', + fcltyCpc: '', + trtFrfNm: '', + efflWqltStndrdNm: '' + }); + + useEffect(() => { + if (!isInit) { + initializeCommonData(); + setSearchFcltyList({ + bscFctCd: '26260PB001R', + bscFctNm: '수영', + fcltyCpc: 452000, + trtFrfNm: '표준활성슬러지법, MLE, MBR', + efflWqltStndrdNm: '500톤 이상(Ⅳ지역)' + }); + } + }, [isInit, initializeCommonData]); + + const handleFacilityChange = (facility: { bscFctCd: string, bscFctNm: string, fcltyCpc: number | '', trtFrfNm: string | '', efflWqltStndrdNm: string | '' }) => { + setFcltyList(facility); + }; + + const onSelectValue = (val: string, type: string) => { + switch(type) { + case 'sido': + setSidoValue(val); + initializeSigunData(val); + onSelectValue('0', 'sigun'); + break; + case 'sigun': + setSigunValue(val); + break; + } + } + + // 시군구 세팅 + const initializeSigunData = (sidoCd: string) => { + if (sidoCd === '0') { + setSigunValue('0'); + setFilteredSigunData([{ text: "전체", val: '0' }]); + } else { + const newFilteredData = [ + { text: "전체", val: '0' }, + ...selectSigunData.filter((item: SelectDataType) => item.val.substring(0, 2) === sidoCd) + ]; + setSigunValue('0'); + setFilteredSigunData(newFilteredData); + } + } + + const excelDownload = () => { + if (!fcltyList.bscFctCd) return; + switch (activeTab) { + case 'facilityInfo': + searchOperationStatus(false, searchYear, fcltyList.bscFctCd); + + fetch(`/mainDashBoard.xlsx`, {}).then(async (response) => { + if (!response.ok) { + throw Error(response.statusText); + } + // 받은 파일 바인너리 처리 + const blobRet = await response.arrayBuffer(); + + const excelData1 = planFcltyList.map((item) => { + return [ + item.item, + item.provisionalApproval, + item.now, + item.stepOne, + item.stepTwo, + item.stepThree, + item.stepFour, + ]; + }); + + // operationalStatusList를 2차원 배열로 변환 + const excelData2 = operationalStatusList.map((item, index) => { + const baseData = [ + item.yearAvg, + item.yearMax, + item.yearMin, + item.summerAll, + item.summerThree, + item.summerRain, + item.winterAll, + item.winterSunnyAvg, + item.winterSunnyMax, + item.winterSunnyMin, + item.groundWaterAvg, + item.groundWaterSummer, + item.groundWaterWinter + ]; + + // index가 0일 때만 rdi와 rdiRain 추가 + return index === 0 ? [...baseData, item.rdi, item.rdiRain] : baseData; + }); + + const sheetParameter = [ + { + data: excelData1, + headerNum: 3, + headerRange: 'A1:G2', + sheetName: '운영실태', + startCol: 'A' + }, + { + data: excelData2, + headerNum: 4, + headerRange: 'A1:Q3', + sheetName: '운영실태', + startCol: 'C' + } + ]; + // 버퍼에 있는 엑셀데이터 전달 후 json으로 변환 + await filePickedBlobArray(blobRet, '시설정보및운영현황황', sheetParameter); + }); + break; + + case 'mainGraph': + searchMainGraph(searchYear, fcltyList.bscFctCd); + + const sheetParameter = [ + { + data: [['날짜', '강수량'],...chart_inflowRain.map((item) => [item.label, item.barVal0])], + headerNum: 1, + headerRange: 'A1:B1', + sheetName: '강수량' + }, + { + data: [['날짜', '유입하수량'],...chart_inflowRain.map((item) => [item.label, item.lineVal1])], + headerNum: 1, + headerRange: 'A1:B1', + sheetName: '유입하수량' + }, + { + data: [['날짜', '시설용량'],...chart_inflowRain.map((item) => [item.label, item.lineVal0])], + headerNum: 1, + headerRange: 'A1:B1', + sheetName: '시설용량' + }, + { + data: [['날짜', '유입BOD'],...chart_bodRain.map((item) => [item.label, item.lineVal0])], + headerNum: 1, + headerRange: 'A1:B1', + sheetName: '유입BOD' + }, + { + data: [['날짜', '유입T-P'],...chart_inflTp.map((item) => [item.label, item.val])], + headerNum: 1, + headerRange: 'A1:B1', + sheetName: '유입T-P' + }, + { + data: [['날짜', '총인유입T-P'],...chart_chongInTp.map((item) => [item.label, item.val])], + headerNum: 1, + headerRange: 'A1:B1', + sheetName: '총인유입T-P' + }, + { + data: [['날짜', '방류T-P'],...chart_efflTp.map((item) => [item.label, item.val])], + headerNum: 1, + headerRange: 'A1:B1', + sheetName: '방류T-P' + }, + { + data: [['날짜', '유입T-N'],...chart_inflTn.map((item) => [item.label, item.val])], + headerNum: 1, + headerRange: 'A1:B1', + sheetName: '유입T-N' + }, + { + data: [['날짜', '방류T-N'],...chart_efflTn.map((item) => [item.label, item.val])], + headerNum: 1, + headerRange: 'A1:B1', + sheetName: '방류T-N' + } + ]; + + downloadMultipleExcel('주요그래프', sheetParameter); + break; + + case 'rainManage': + searchRainManage(searchYear, fcltyList.bscFctCd, true); + if (excelList && excelList.length > 0) { + downloadExcel({ data: excelList as unknown as JSON[], fileName: '강우일 운영현황', sheetNameArray: ['강우일 운영현황'], headerRange: 'A1:T1' }); + } + break; + // Add other cases for different tabs as needed + default: + break; + } + } + + const initVal = () => { + setFcltyList({ + bscFctCd: '26260PB001R', + bscFctNm: '수영', + fcltyCpc: 452000, + trtFrfNm: '표준활성슬러지법, MLE, MBR', + efflWqltStndrdNm: '500톤 이상(Ⅳ지역)' + }); + + setSearchFcltyList({ + bscFctCd: '26260PB001R', + bscFctNm: '수영', + fcltyCpc: 452000, + trtFrfNm: '표준활성슬러지법, MLE, MBR', + efflWqltStndrdNm: '500톤 이상(Ⅳ지역)' + }); + setSearchYear('2023'); + searchFcltyList.bscFctNm = '수영'; + } + + const searchVal = () => { + setSearchFcltyList(fcltyList); + if(searchFcltyList.bscFctCd) { + searchOperationStatus(false, searchYear, fcltyList.bscFctCd); + } + } + + return ( + <MainDiv> + <ContentsDiv> + <div className="flex card card-border !mt-6"> + <div className="grid grid-cols-12 gap-5 w-8/12 m-4"> + <SelectBox className="col-span-2" label={'연도'} selectArray={selectSearchYear} onSelectValue={ (val) => setSearchYear(val.toString()) } value={searchYear} selectClass={undefined}/> + <SelectBox className="col-span-3" label={'시도'} selectArray={selectSidoData} onSelectValue={(val) => onSelectValue(val.toString(), 'sido')} value={sidoValue} selectClass={undefined}/> + <SelectBox className="col-span-3" label={'시군구'} selectArray={filteredSigunData} onSelectValue={(val) => onSelectValue(val.toString(), 'sigun')} value={sigunValue} selectClass={undefined}/> + <div className="col-span-4 flex"> + <DashBoardFclty onFacilityChange={handleFacilityChange} sidoSigun={sigunValue!='0'?sigunValue:sidoValue!='0'?sidoValue:''} facilityNm={searchFcltyList.bscFctNm}></DashBoardFclty> + </div> + </div> + <div className="w-4/12 mx-5 flex items-center justify-end space-x-2"> + <Button className={btnClass} onClick={searchVal}> + 조회 + </Button> + <Button className={btnClass} onClick={initVal}> + 초기화 + </Button> + <Button className={btnClass} color="green" onClick={excelDownload}> + 엑셀다운로드 + </Button> + </div> + </div> + <UITab tabList={tabList} defaultValue="facilityInfo" onValueChange={setActiveTab}> + <TabsContent value="facilityInfo"> + <ProcessFacilitySearch searchYear={searchYear} fcltyList={[searchFcltyList]} onFacilityChange={handleFacilityChange} /> + </TabsContent> + <TabsContent value="mainGraph"> + <DashBoardChart searchYear={searchYear} fcltyList={[searchFcltyList]}/> + </TabsContent> + <TabsContent value="rainManage"> + <RainManageTab searchYear={searchYear} fcltyList={[searchFcltyList]}/> + </TabsContent> + <TabsContent value="drainageEquipment"> + <DrainageEquipment searchYear={searchYear} fcltyList={[searchFcltyList]}/> + </TabsContent> + </UITab> + </ContentsDiv> + </MainDiv> + ); +} diff --git a/apps/fo/app/pages/dashboard/rainManage/page.tsx b/apps/fo/app/pages/dashboard/rainManage/page.tsx new file mode 100644 index 000000000..ea20eb92c --- /dev/null +++ b/apps/fo/app/pages/dashboard/rainManage/page.tsx @@ -0,0 +1,59 @@ +"use client" +import { useFoStore } from "@/app/store"; +import { UiTable } from "@common/business_components/ui/table"; +import { useEffect, useState } from "react"; + +export type facilityInfoProps = { + searchYear?: string; + fcltyList?: { bscFctCd: string, bscFctNm: string, fcltyCpc: number | '', trtFrfNm: string, efflWqltStndrdNm: string }[]; +}; + +export default function RainManageTab({searchYear = '', fcltyList = []}: facilityInfoProps) { + + const { + Common, + searchRainManage, + RainManage + } = useFoStore((state) => state); + + const [hasSearched, setHasSearched] = useState(false); + // 이전 시설코드 저장 + const [prevFacilityCode, setPrevFacilityCode] = useState<string>(''); + + const { + isInit, + } = Common; + + const { + upHeadListRainManage, + rainManageList, + } = RainManage; + + const tableData = fcltyList; + + useEffect(() => { + if (!isInit && tableData[0]?.bscFctCd && (tableData[0].bscFctCd !== prevFacilityCode || !hasSearched)) { + searchRainManage(searchYear, tableData[0].bscFctCd, false); + setHasSearched(true); + setPrevFacilityCode(tableData[0].bscFctCd); + } + }, [fcltyList, isInit, searchRainManage, searchYear, tableData, hasSearched, prevFacilityCode]); + + // 연도가 바뀌면 hasSearched를 false로 리셋 + useEffect(() => { + setHasSearched(false); + }, [searchYear]); + + return ( + <UiTable + headName="" + tableData={rainManageList} + headlist={upHeadListRainManage} + pageSize={0} + total={0} + cellClick={undefined} + onPageChange={() => {}} + > + </UiTable> + ) +} diff --git a/apps/fo/app/pages/dashboardMain/page.tsx b/apps/fo/app/pages/dashboardMain/page.tsx new file mode 100644 index 000000000..143a78acd --- /dev/null +++ b/apps/fo/app/pages/dashboardMain/page.tsx @@ -0,0 +1,56 @@ +"use client" +import { useFoStore } from "@/app/store"; +import { useEffect, useState } from "react"; +import SearchInput from "../../components/common/SearchInput"; + +export default function DashBoardFclty({ onFacilityChange, sidoSigun, facilityNm, }: { onFacilityChange?: (facility: { bscFctCd: string, bscFctNm: string, fcltyCpc: number | '', trtFrfNm: string | '', efflWqltStndrdNm: string | '' }) => void, sidoSigun?: string, facilityNm?: string }) { + // 입력값 상태 관리 + const { Common, initializeCommonData } = useFoStore((state) => state); + + const { + isInit, + } = Common; + + // 시설명 + const [bscFctNm, setFcltyName] = useState<string>(facilityNm || '수영'); + const [bscFctCd, setbscFctCd] = useState<string>('11000SW001R'); + const [fcltyCpc, setFcltyCpc] = useState<number | ''>(860000); + const [trtFrfNm, setTrtFrfNm] = useState<string | ''>('표준활성슬러지법, MLE, A2O'); + const [efflWqltStndrdNm, setEfflWqltStndrdNm] = useState<string | ''>('500톤 이상(Ⅲ지역)'); + + useEffect(() => { + if (!isInit) { + initializeCommonData(); + } + }, [isInit, initializeCommonData ]); + + useEffect(() => { + if (facilityNm) { + setFcltyName(facilityNm); + } + }, [facilityNm]); + + const fncNextPage = (bscFctCd?: string, bscFctNm?: string, fcltyCpc?: number | '', trtFrfNm?: string | '', efflWqltStndrdNm?: string | '') => { + setbscFctCd(bscFctCd || ''); + setFcltyName(bscFctNm || ''); + setFcltyCpc(fcltyCpc || ''); + setTrtFrfNm(trtFrfNm || ''); + setEfflWqltStndrdNm(efflWqltStndrdNm || ''); + + if (bscFctCd && onFacilityChange) { + onFacilityChange({ + bscFctCd: bscFctCd || '', + bscFctNm: bscFctNm || '', + fcltyCpc: fcltyCpc || '', + trtFrfNm: trtFrfNm || '', + efflWqltStndrdNm: efflWqltStndrdNm || '' + }); + } + } + + return ( + <div className="z-50"> + <SearchInput label={'시설명'} setValue={fncNextPage} sidoSigun={sidoSigun} fcltyName={bscFctNm}/> + </div> + ); +} \ No newline at end of file diff --git a/apps/fo/app/pages/facilityPrecipitation/modal/page.tsx b/apps/fo/app/pages/facilityPrecipitation/modal/page.tsx new file mode 100644 index 000000000..ead9b56fd --- /dev/null +++ b/apps/fo/app/pages/facilityPrecipitation/modal/page.tsx @@ -0,0 +1,203 @@ +"use client" +import { useFoStore } from "@/app/store"; +import type { PrecipitationPropsDetailProps } from "@/app/store/facilityPrecipitation"; +import { DatePickerRange, UiTable } from "@common/business_components/ui"; +import ListDiv from "@common/business_components/ui/list_div"; +import { Table, TableCell, TableHead, TableRow } from "@common/components/ui"; +import { useEffect, useRef, useState } from "react"; + +type PrecipitationModalProps = { + searchYear: string; + fcltyName: string; + facilityCd: string; + detailList: PrecipitationPropsDetailProps[]; +} + +type DateRange = { + start: Date; + end: Date; +} + +export default function PrecipitationModal(props: PrecipitationModalProps) { + const { PrecipitationDetail } = useFoStore((state) => state); + const { upHeadListPrecipitationDetail } = PrecipitationDetail; + + // Refs + const startPickerRef = useRef<HTMLDivElement>(null); + const endPickerRef = useRef<HTMLDivElement>(null); + + // States + const [startDate, setStartDate] = useState<Date | null>(null); + const [endDate, setEndDate] = useState<Date | null>(null); + const [filteredDetailList, setFilteredDetailList] = useState<PrecipitationPropsDetailProps[]>([]); + const [copyDetailList, setCopyDetailList] = useState<PrecipitationPropsDetailProps[]>([]); + + const getDateRanges = (): DateRange[] => { + const standYear = parseInt(props.searchYear); + return [{ + start: new Date(standYear, 0, 1), + end: new Date(standYear, 11, 31) + }]; + }; + + const isDateInRanges = (date: Date): boolean => { + return getDateRanges().some(range => { + const start = new Date(range.start); + const end = new Date(range.end); + return date >= start && date <= end; + }); + }; + + const filterDataByDateRange = (start: Date | null, end: Date | null): PrecipitationPropsDetailProps[] => { + if (!start || !end) return filteredDetailList; + return filteredDetailList.filter((item) => { + const itemDate = new Date(item.rainDt); + return itemDate >= start && itemDate <= end; + }); + }; + + const handleStartDateSelect = (date: Date | null) => { + if (!date || !endDate) return; + + if (date > endDate) { + alert('시작일은 종료일 이후일 수 없습니다.'); + return; + } + + const filteredData = filterDataByDateRange(date, endDate); + setCopyDetailList(filteredData); + setStartDate(date); + + const button = startPickerRef.current?.querySelector('button'); + if (button) { + setTimeout(() => button.click(), 0); + } + }; + + const handleEndDateSelect = (date: Date | null) => { + if (!date || !startDate) return; + + if (date < startDate) { + alert('종료일은 시작일 이전일 수 없습니다.'); + return; + } + + const filteredData = filterDataByDateRange(startDate, date); + setCopyDetailList(filteredData); + setEndDate(date); + + const button = endPickerRef.current?.querySelector('button'); + if (button) { + setTimeout(() => button.click(), 0); + } + }; + + const calculateTotals = () => { + return copyDetailList.reduce((acc, item) => ({ + infoRnDay: acc.infoRnDay + Number(item.infoRnDay), + rainDay: acc.rainDay + Number(item.rainDay), + rainEffectDay: acc.rainEffectDay + Number(item.rainEffectDay) + }), { infoRnDay: 0, rainDay: 0, rainEffectDay: 0 }); + }; + + const getTableData = () => { + const totals = calculateTotals(); + return [ + { + bscFctCd: "", + rainDt: '합계', + ...totals + }, + ...copyDetailList + ]; + }; + + useEffect(() => { + if (props.detailList.length > 0) { + const filteredList = props.detailList.filter((item) => + props.facilityCd === item.bscFctCd + ); + + setFilteredDetailList(filteredList); + setCopyDetailList(filteredList); + + if (filteredList.length > 0) { + const firstDateStr = filteredList[0]?.rainDt; + const lastDateStr = filteredList[filteredList.length - 1]?.rainDt; + + if (firstDateStr) { + const firstDate = new Date(firstDateStr); + if (!isNaN(firstDate.getTime())) { + setStartDate(firstDate); + } + } + + if (lastDateStr) { + const lastDate = new Date(lastDateStr); + if (!isNaN(lastDate.getTime())) { + setEndDate(lastDate); + } + } + } + } + }, [props.detailList, props.facilityCd]); + + if (!startDate || !endDate) { + return <div>로딩 중...</div>; + } + + return ( + <> + <ListDiv className="!h-[80px] !mt-10"> + <div className="card !m-[1.2rem] max-h-[40rem] overflow-auto !p-0r"> + <Table> + <thead> + <TableRow> + <TableHead className="w-1/6">{'시설명'}</TableHead> + <TableCell className="w-1/3">{props.fcltyName}</TableCell> + <TableHead className="w-1/6">{'년월'}</TableHead> + <TableCell className="w-1/3"> + <div className="flex"> + <div ref={startPickerRef} className="flex justify-center"> + <DatePickerRange + mode="single" + selected={startDate} + onSelect={(date) => handleStartDateSelect(date as Date)} + className="w-full p-1 border rounded bg-white" + disabled={(date) => !isDateInRanges(date)} + defaultMonth={startDate} + /> + </div> + <span className="mx-3">~</span> + <div ref={endPickerRef} className="flex justify-center"> + <DatePickerRange + mode="single" + selected={endDate} + onSelect={(date) => handleEndDateSelect(date as Date)} + className="w-full p-1 border rounded bg-white" + disabled={(date) => !isDateInRanges(date)} + defaultMonth={endDate} + /> + </div> + </div> + </TableCell> + </TableRow> + </thead> + </Table> + </div> + </ListDiv> + <ListDiv className="h-[450px] overflow-y-auto"> + <UiTable + headName={"강수현황"} + tableData={getTableData()} + headlist={upHeadListPrecipitationDetail} + pageSize={0} + total={0} + cellClick={undefined} + onPageChange={() => {}} + boldArray={[0]} + /> + </ListDiv> + </> + ); +} \ No newline at end of file diff --git a/apps/fo/app/pages/facilityPrecipitation/page.tsx b/apps/fo/app/pages/facilityPrecipitation/page.tsx new file mode 100644 index 000000000..96456a71b --- /dev/null +++ b/apps/fo/app/pages/facilityPrecipitation/page.tsx @@ -0,0 +1,250 @@ +"use client" +import { useFoStore } from "@/app/store"; +import type { TableUpperProps } from "@/app/store/common"; +import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; +import ListDiv from "@common/business_components/ui/list_div"; +import { useCallback, useEffect, useRef, useState } from "react"; +import PrecipitationModal from "./modal/page"; + +export default function FacilityPrecipitation() { + const {Common + ,FacilityPrecipitation + ,FacilityPrecipitationSearch + ,PrecipitationDetail + ,initializeCommonData + ,searchFacilityPrecipitationList + } = useFoStore((state) => state) || { processAreaList: { processAreaList: [] }}; + + const { + isInit, + selectSidoData, + selectSigunData, + selectRegionData, + selectUnitWaterShedData, + selectMiddleRegionData, + selectEnvPartData, + selectUpdownData, + selectOperationData, + selectSearchYear, + btnClass + } = Common; + + const { + labelArray, + } = FacilityPrecipitationSearch; + + const { + upHeadList1, + upHeadList2, + upHeadList3, + excelList, + facilityPrecipitation, + facilityPrecipitationCount + } = FacilityPrecipitation; + + const { + gridListPrecipitationDetail + } = PrecipitationDetail; + + const ref = useRef<ReturnType>({ + partValue: '', + sidoValue: '', + sigunValue: '', + regionValue: '', + unitWaterShedValue: '', + middleRegionValue: '', + envPartValue: '', + sidoText: '', + sigunText: '' + }); + + const facilityPrecipitationList = FacilityPrecipitation?.facilityPrecipitation || []; + + // 그리드 관련 + const searchInputRef = useRef<HTMLDivElement & HTMLInputElement>(null); + + // 모달 상태 추가 + const [selectedRow, setSelectedRow] = useState<number | null>(null); + const [fcltyNm, setFcltyNm] = useState<string>(""); + const [fcltyCd, setFcltyCd] = useState<string>(""); + + // 초기화 변수 + const [resetPage, setResetPage] = useState<boolean>(false); + // 표 너비 + const [widthSize, setWidthSize] = useState<string>("w-[120%]"); + // 구분별 그리드 헤더 + const [upheadList, setUpheadList] = useState<TableUpperProps[]>(upHeadList1); + // 용량별 + const [fcltCpct, setFcltCpct] = useState<string>("0"); + // 시설명 + const [fcltyName, setFcltyName] = useState<string>(""); + // 시설상태 + const [operationValue, setOperationValue] = useState<string>("0"); + // 조회기간 + //const [searchYear, setSearchYear] = useState<string>(selectSearchYear[0]?.val || "0"); + const [searchYear, setSearchYear] = useState<string>("2023"); + + /** + * 페이지 함수 + */ + // 초기화 + const resetVal = () => { + setWidthSize("w-[120%]"); + setUpheadList(upHeadList1) + // 상태값 설정 + setFcltCpct(selectUpdownData[0]?.val || "0"); + setOperationValue(selectOperationData[0]?.val || "0"); + setSearchYear(selectSearchYear[0]?.val || "0"); + // SearchInputSelect 초기화 + setFcltyName(""); + if (searchInputRef.current) { + const input = searchInputRef.current.querySelector('input'); + if (input) { + input.value = ""; + } + } + searchFacilityPrecipitationList(1, 10, selectSearchYear[0]?.val || "0"); + } + + // 모달 닫기 + const closeModal = () => { + setSelectedRow(null); + }; + + // 셀 클릭 시 모달 표시 + const cellClick = (index: number) => { + if (facilityPrecipitation[index]) { + setFcltyNm(facilityPrecipitation[index].bscFctNm); + setFcltyCd(facilityPrecipitation[index].bscFctCd); + setSelectedRow(index); // 클릭한 행의 index 저장 + } + }; + + /** + * useEffect + */ + useEffect(() => { + if (!isInit) { + initializeCommonData(); + searchFacilityPrecipitationList(1, 10, searchYear || "0"); + } + }, [isInit, initializeCommonData, searchFacilityPrecipitationList, searchYear]); + + useEffect(() => { + if (excelList && excelList.length > 0) { + downloadExcel({ + data: excelList as unknown as JSON[], + fileName: '시설별 강수현황', + sheetNameArray: ['시설별 강수현황'], + headerRange: 'A1:N1' + }); + } + }, [excelList]); + + useEffect(() => { + if (facilityPrecipitation.length > 0) { + + } + }, [facilityPrecipitation]); + + /** + * 콜백함수 + */ + // 조회 + const searchFacilityPrecipitation = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => { + console.log('partValue : ', partValue); + console.log('regionValue : ', regionValue); + console.log('unitWaterShedValue : ', unitWaterShedValue); + console.log('middleRegionValue : ', middleRegionValue); + console.log('envPartValue : ', envPartValue); + console.log('sidoValue : ', sidoValue); + console.log('sigunValue : ', sigunValue); + + let nameCd, partStr + switch (partValue) { + case 'R': + if (regionValue !== '0') { + if(unitWaterShedValue==='0') nameCd = regionValue; + else if (middleRegionValue==='0') nameCd = unitWaterShedValue; + else nameCd = middleRegionValue; + } + partStr = partValue + break; + default: + if(sidoValue !== '0') nameCd = sidoValue; + if(sigunValue !== '0') nameCd = sigunValue; + partStr = envPartValue !== '0' ? envPartValue:partValue; + break; + } + if (partStr.length > 1 || operationValue!== '0' || fcltCpct !== '0' ||nameCd ||(fcltyName.trim() !== "" && fcltyName.trim() !== null)) { + searchFacilityPrecipitationList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined); + } else { + searchFacilityPrecipitationList(pageNo, pageCnt, searchYear); + } + }, [operationValue, fcltyName, searchFacilityPrecipitationList, searchYear, fcltCpct]); + + const chkVal = useCallback((returntype:ReturnType) => { + setWidthSize(returntype.partValue=='A' ?"w-[120%]":"w-[130%]") + setUpheadList(returntype.partValue == 'R' ? upHeadList2 : returntype.partValue == 'G'?upHeadList3:upHeadList1); + setResetPage(true); + }, [upHeadList1, upHeadList2, upHeadList3]); + + // 엑셀다운로드 + const excelDownload = useCallback((searchReturn: ReturnType) => { + searchFacilityPrecipitation(searchReturn, 1, 10000); + }, [searchFacilityPrecipitation]); + + // 페이지 변경 + const handlePageChange = useCallback((page: number) => { + setResetPage(false); + searchFacilityPrecipitation(ref.current, page, 10); + }, [searchFacilityPrecipitation]); + + const mainList = facilityPrecipitationList || []; + const total = facilityPrecipitationCount || 0; + + return ( + <MainDiv> + <Title title="시설별 강수현황" /> + <ContentsDiv> + <SearchBaseForm + leftChildren={<> + <SelectBox className="col-span-3" label={labelArray[3] as string} selectArray={selectUpdownData} onSelectValue={(val) => setFcltCpct(val.toString())} value={fcltCpct} selectClass={undefined} /> + <SearchInput className={"col-span-3"} label={labelArray[4] as string} textValue={fcltyName} setText={setFcltyName} /> + <SelectBox className="col-span-3" label={labelArray[6] as string} selectArray={selectSearchYear} onSelectValue={(val) => setSearchYear(val.toString())} value={searchYear} selectClass={undefined} /> + <SelectBox className="col-span-3" label={labelArray[5] as string} selectArray={selectOperationData} onSelectValue={(val) => setOperationValue(val.toString())} value={operationValue} selectClass={undefined} /> + </>} + rightChildren={undefined} + selectSidoData={selectSidoData} + selectSigunData={selectSigunData} + selectRegionData={selectRegionData} + selectUnitWaterShedData={selectUnitWaterShedData} + selectMiddleRegionData={selectMiddleRegionData} + selectEnvPartData={selectEnvPartData} + btnClass={btnClass} + queryClick={chkVal} + excelClick={excelDownload} + initClick={resetVal} + ref={ref} isAdmin={false} /> + <ListDiv> + <div className="h-full w-full overflow-x-scroll"> + <div className={widthSize}> + <UiTable cellClick={(index: number | undefined) => index !== undefined && cellClick(index)} headName={"시설별 강수현황"} tableData={mainList} headlist={upheadList} pageSize={10} total={total} resetPageNo={resetPage} onPageChange={handlePageChange} /> + </div> + <span className="text-2xl font-bold text-gray-500">☞ 강우일은 3mm 이상만 해당 / 강우영향일은 강우일 및 강우일 후 연속 2일간으로 정의</span> + </div> + </ListDiv> + </ContentsDiv> + {selectedRow !== null && ( + <UiModal + body={<PrecipitationModal searchYear={searchYear} fcltyName={fcltyNm} facilityCd={fcltyCd} detailList={gridListPrecipitationDetail}></PrecipitationModal>} + bodyClass="sm:max-w-[90vw] w-[72vw] h-[40vw]" + open={selectedRow !== null} + onOpenChange={(open) => !open && closeModal()} + noHeaderYn="Y" + noFooterYn="Y" + /> + )} + </MainDiv> + ); +} \ No newline at end of file diff --git a/apps/fo/app/pages/facilityWaterQlty/HeadMakeColSpan.tsx b/apps/fo/app/pages/facilityWaterQlty/HeadMakeColSpan.tsx new file mode 100644 index 000000000..61076c105 --- /dev/null +++ b/apps/fo/app/pages/facilityWaterQlty/HeadMakeColSpan.tsx @@ -0,0 +1,176 @@ +import { TableHead, TableRow } from "@common/components/ui"; + +interface TableUpperProps { + id: string; + title: string; + upName?: string; + upSequnce?: number; +} + +const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProps[], layoutNum: number }) => { + return ( + layoutNum === 0 ? ( + <> + <TableRow className="bg-gray-50"> + {upHeadList.map((head: TableUpperProps) => { + if(!head.upName) { + return <TableHead className="bg-gray-50" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> + }else if(head.upName==="upChangeRe1"&&head.upSequnce===1) { + return <TableHead className="bg-gray-50" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>행정구역</TableHead> + }else if(head.upName==="upChangeRe3"&&head.upSequnce===1) { + return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead> + }else if(head.upName==="upChangeRe4"&&head.upSequnce===1) { + return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유출</TableHead> + } + })} + </TableRow> + <TableRow className="bg-gray-50"> + {upHeadList.map((head: TableUpperProps, idx: number) => { + if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4') { + if (idx == 1 || idx == 2 || idx == 3) { + return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>; + } + else { + return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>; + } + } + })} + </TableRow> + </> + ) : layoutNum === 1 ? ( + <> + <TableRow className="bg-gray-50"> + {upHeadList.map((head: TableUpperProps) => { + if(!head.upName) { + return <TableHead className="bg-gray-50" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> + }else if(head.upName==="upChangeRe1"&&head.upSequnce===1) { + return <TableHead className="bg-gray-50" colSpan={3} style={{ whiteSpace: 'pre-line' }} key={head.id}>권역</TableHead> + }else if(head.upName==="upChangeRe3"&&head.upSequnce===1) { + return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead> + }else if(head.upName==="upChangeRe4"&&head.upSequnce===1) { + return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유출</TableHead> + } + })} + </TableRow> + <TableRow className="bg-gray-50"> + {upHeadList.map((head: TableUpperProps, idx: number) => { + if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4') { + if (idx == 1 || idx == 2 || idx == 3) { + return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>; + } + else { + return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>; + } + } + })} + </TableRow> + </> + ) : layoutNum === 2 ? ( + <> + <TableRow className="bg-gray-50"> + {upHeadList.map((head: TableUpperProps) => { + if(!head.upName) { + return <TableHead className="bg-gray-50" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> + }else if(head.upName==="upChangeRe1"&&head.upSequnce===1) { + return <TableHead className="bg-gray-50" colSpan={3} style={{ whiteSpace: 'pre-line' }} key={head.id}>권역</TableHead> + }else if(head.upName==="upChangeRe2"&&head.upSequnce===1) { + return <TableHead className="bg-gray-50" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>행정구역</TableHead> + }else if(head.upName==="upChangeRe3"&&head.upSequnce===1) { + return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead> + }else if(head.upName==="upChangeRe4"&&head.upSequnce===1) { + return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유출</TableHead> + } + })} + </TableRow> + <TableRow className="bg-gray-50"> + {upHeadList.map((head: TableUpperProps, idx: number) => { + if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4') { + if (idx == 1 || idx == 2 || idx == 3) { + return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>; + } + else { + return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>; + } + } + })} + </TableRow> + </> + ) : layoutNum === 3 ? ( + <> + <TableRow className="bg-gray-50"> + {upHeadList.map((head: TableUpperProps) => { + if(!head.upName) { + return <TableHead className="bg-gray-50" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> + }else if(head.upName==="upChangeRe1"&&head.upSequnce===1) { + return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead> + }else if(head.upName==="upChangeRe2"&&head.upSequnce===1) { + return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>방류</TableHead> + } + })} + </TableRow> + <TableRow className="bg-gray-50"> + {upHeadList.map((head: TableUpperProps, idx: number) => { + if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2') { + if (idx == 1 || idx == 2 || idx == 3) { + return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>; + } + else { + return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>; + } + } + })} + </TableRow> + </> + ) : layoutNum === 4 ? ( + <> + <TableRow className="bg-gray-50"> + {upHeadList.map((head: TableUpperProps) => { + if(!head.upName) { + return <TableHead className="bg-gray-50" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> + }else if(head.upName==="upChangeRe"&&head.upSequnce===1) { + return <TableHead className="bg-gray-50" colSpan={4} style={{ whiteSpace: 'pre-line' }} key={head.id}>방류</TableHead> + } + })} + </TableRow> + <TableRow className="bg-gray-50"> + {upHeadList.map((head: TableUpperProps, idx: number) => { + if (head.upName === 'upChangeRe') { + if (idx == 1 || idx == 2 || idx == 3) { + return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>; + } + else { + return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>; + } + } + })} + </TableRow> + </> + ) : layoutNum === 5 ? ( // 총인설비 수질현황 헤더 + <> + <TableRow className="bg-gray-50"> + {upHeadList.map((head: TableUpperProps) => { + if(!head.upName) { + return <TableHead className="w-[180px] bg-gray-50" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> + }else if(head.upName==="upChangeRe1" && head.upSequnce===1) { + return <TableHead className="bg-gray-50" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead> + }else if(head.upName==="upChangeRe2" && head.upSequnce===1) { + return <TableHead className="bg-gray-50" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>방류</TableHead> + } + })} + </TableRow> + <TableRow className="bg-gray-50"> + {upHeadList.map((head: TableUpperProps) => { + if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2') { + return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>; + } + })} + </TableRow> + </> + ): ( + <> + </> + ) + ) +} + +export { HeadMakeColSpan }; diff --git a/apps/fo/app/pages/facilityWaterQlty/modal/page.tsx b/apps/fo/app/pages/facilityWaterQlty/modal/page.tsx new file mode 100644 index 000000000..eadfa9ba8 --- /dev/null +++ b/apps/fo/app/pages/facilityWaterQlty/modal/page.tsx @@ -0,0 +1,577 @@ +"use client" +import { useFoStore } from "@/app/store"; +import type { FacilityWaterQltyProps, modalExcelType } from "@/app/store/facilityWaterQlty"; +import { downloadExcel, UITab, UiTable } from "@common/business_components/ui"; +import { Button, TabsContent } from "@common/components/ui"; +import { useCallback, useEffect, useRef, useState } from "react"; +import type { ReturnType } from "../../../components/common/FacilityTermModalHeader"; +import FacilityTermModalHeader from "../../../components/common/FacilityTermModalHeader"; +import { HeadMakeColSpan } from "../HeadMakeColSpan"; + +type ModalProps ={ + fcltyName: string; + facilityCd : string; + searchYear : string; +} + +const tabList1 = [ + { value: 'tab_num1', label: '처리시설 수질현황' }, + { value: 'tab_num2', label: '총인설비 수질현황' }, +]; + +const tabList2 = [ + { value: 'tab_term_year', label: '연간' }, + { value: 'tab_term_summer', label: '하절기' }, + { value: 'tab_term_winter', label: '동절기' }, + { value: 'tab_term_chuncheon', label: '청천시' }, + { value: 'tab_term_rain', label: '강우시' } +]; + +const tabList3 = [ + { value: 'tab_term_year_chongin', label: '연간' }, + { value: 'tab_term_summer_chongin', label: '하절기' }, + { value: 'tab_term_winter_chongin', label: '동절기' } +]; + +export default function WaterQualityModal({fcltyName='', facilityCd='', searchYear=new Date().getFullYear().toString()} : ModalProps) { + // 그리드 관련 + const {FacilityWaterQltyModal, + searchFacilityWaterQltyListHist, + Common + } = useFoStore((state) => state); + const {btnClass} = Common; + const { + upHeadListModalYear, + facilityWaterQltyYearList, + upHeadListModalTms, + tmsList, + msrmtDate, + upHeadListModalChongIn, + } = FacilityWaterQltyModal; + + const prevExcelListRef = useRef<modalExcelType[] | null>(null); + + // 필터링 된 후 최대, 최소, 평균 값 계산 + const setFilterAfterData = (filteredData: FacilityWaterQltyProps[]) => { + // 쉼표가 포함된 문자열을 숫자로 변환하는 함수 + const parseNumber = (value: string | number | null | undefined): number => { + if (value === null || value === undefined || value === '') return 0; + if (typeof value === 'number') return value; + return Number(value.replace(/,/g, '')); + }; + + let maxEfflWqltBod = 0; + let maxEfflWqltTn = 0; + let maxEfflWqltTp = 0; + let maxefflWqltToc = 0; + let maxEfflWqltSs = 0; + + let maxInflAssctAftWqltBod = 0; + let maxInflAssctAftWqltTn = 0; + let maxInflAssctBfrWqltTp = 0; + let maxInflAssctAftWqltToc = 0; + let maxInflAssctAftWqltSs = 0; + + let maxTpInflWqltBod = 0; + let maxTpInflWqltTp = 0; + let maxInfoRnDay = 0; + + // 초기값 설정 + let minEfflWqltBod = filteredData.length > 0 ? parseNumber(filteredData[0]?.efflWqltBod) : 0; + let minEfflWqltTn = filteredData.length > 0 ? parseNumber(filteredData[0]?.efflWqltTn) : 0; + let minEfflWqltTp = filteredData.length > 0 ? parseNumber(filteredData[0]?.efflWqltTp) : 0; + let minefflWqltToc = filteredData.length > 0 ? parseNumber(filteredData[0]?.efflWqltToc) : 0; + let minEfflWqltSs = filteredData.length > 0 ? parseNumber(filteredData[0]?.efflWqltSs) : 0; + + let minInflAssctAftWqltBod = filteredData.length > 0 ? parseNumber(filteredData[0]?.inflAssctAftWqltBod) : 0; + let minInflAssctAftWqltTn = filteredData.length > 0 ? parseNumber(filteredData[0]?.inflAssctAftWqltTn) : 0; + let minInflAssctBfrWqltTp = filteredData.length > 0 ? parseNumber(filteredData[0]?.inflAssctBfrWqltTp) : 0; + let minInflAssctAftWqltToc = filteredData.length > 0 ? parseNumber(filteredData[0]?.inflAssctAftWqltToc) : 0; + let minInflAssctAftWqltSs = filteredData.length > 0 ? parseNumber(filteredData[0]?.inflAssctAftWqltSs) : 0; + + let minTpInflWqltBod = filteredData.length > 0 ? parseNumber(filteredData[0]?.tpInflWqltBod) : 0; + let minTpInflWqltTp = filteredData.length > 0 ? parseNumber(filteredData[0]?.tpInflWqltTp) : 0; + + let minInfoRnDay = filteredData.length > 0 ? parseNumber(filteredData[0]?.infoRnDay) : 0; + + let avgEfflWqltBod = 0; + let avgEfflWqltTn = 0; + let avgEfflWqltTp = 0; + let avgefflWqltToc = 0; + let avgEfflWqltSs = 0; + + let avgInflAssctAftWqltBod = 0; + let avgInflAssctAftWqltTn = 0; + let avgInflAssctBfrWqltTp = 0; + let avgInflAssctAftWqltToc = 0; + let avgInflAssctAftWqltSs = 0; + + let avgTpInflWqltBod = 0; + let avgTpInflWqltTp = 0; + + let avgInfoRnDay = 0; + + filteredData.forEach((item: FacilityWaterQltyProps) => { + const efflWqltBod = parseNumber(item.efflWqltBod); + const efflWqltTn = parseNumber(item.efflWqltTn); + const efflWqltTp = parseNumber(item.efflWqltTp); + const efflWqltToc = parseNumber(item.efflWqltToc); + const efflWqltSs = parseNumber(item.efflWqltSs); + + const inflAssctAftWqltBod = parseNumber(item.inflAssctAftWqltBod); + const inflAssctAftWqltTn = parseNumber(item.inflAssctAftWqltTn); + const inflAssctBfrWqltTp = parseNumber(item.inflAssctBfrWqltTp); + const inflAssctAftWqltToc = parseNumber(item.inflAssctAftWqltToc); + const inflAssctAftWqltSs = parseNumber(item.inflAssctAftWqltSs); + + + const tpInflWqltBod = parseNumber(item.tpInflWqltBod); + const tpInflWqltTp = parseNumber(item.tpInflWqltTp); + + const infoRnDay = parseNumber(item.infoRnDay); + + maxEfflWqltBod = Math.max(maxEfflWqltBod, efflWqltBod); + maxEfflWqltTn = Math.max(maxEfflWqltTn, efflWqltTn); + maxEfflWqltTp = Math.max(maxEfflWqltTp, efflWqltTp); + maxefflWqltToc = Math.max(maxefflWqltToc, efflWqltToc); + maxEfflWqltSs = Math.max(maxEfflWqltSs, efflWqltSs); + + maxInflAssctAftWqltBod = Math.max(maxInflAssctAftWqltBod, inflAssctAftWqltBod); + maxInflAssctAftWqltTn = Math.max(maxInflAssctAftWqltTn, inflAssctAftWqltTn); + maxInflAssctBfrWqltTp = Math.max(maxInflAssctBfrWqltTp, inflAssctBfrWqltTp); + maxInflAssctAftWqltToc = Math.max(maxInflAssctAftWqltToc, inflAssctAftWqltToc); + maxInflAssctAftWqltSs = Math.max(maxInflAssctAftWqltSs, inflAssctAftWqltSs); + + maxTpInflWqltBod = Math.max(maxTpInflWqltBod, tpInflWqltBod); + maxTpInflWqltTp = Math.max(maxTpInflWqltTp, tpInflWqltTp); + + maxInfoRnDay = Math.max(maxInfoRnDay, infoRnDay); + + minEfflWqltBod = Math.min(minEfflWqltBod, efflWqltBod); + minEfflWqltTn = Math.min(minEfflWqltTn, efflWqltTn); + minEfflWqltTp = Math.min(minEfflWqltTp, efflWqltTp); + minefflWqltToc = Math.min(minefflWqltToc, efflWqltToc); + minEfflWqltSs = Math.min(minEfflWqltSs, efflWqltSs); + + minInflAssctAftWqltBod = Math.min(minInflAssctAftWqltBod, inflAssctAftWqltBod); + minInflAssctAftWqltTn = Math.min(minInflAssctAftWqltTn, inflAssctAftWqltTn); + minInflAssctBfrWqltTp = Math.min(minInflAssctBfrWqltTp, inflAssctBfrWqltTp); + minInflAssctAftWqltToc = Math.min(minInflAssctAftWqltToc, inflAssctAftWqltToc); + minInflAssctAftWqltSs = Math.min(minInflAssctAftWqltSs, inflAssctAftWqltSs); + + minTpInflWqltBod = Math.min(minTpInflWqltBod, tpInflWqltBod); + minTpInflWqltTp = Math.min(minTpInflWqltTp, tpInflWqltTp); + + minInfoRnDay = Math.min(minInfoRnDay, infoRnDay); + + avgEfflWqltBod += efflWqltBod; + avgEfflWqltTn += efflWqltTn; + avgEfflWqltTp += efflWqltTp; + avgefflWqltToc += efflWqltToc; + avgEfflWqltSs += efflWqltSs; + + avgInflAssctAftWqltBod += inflAssctAftWqltBod; + avgInflAssctAftWqltTn += inflAssctAftWqltTn; + avgInflAssctBfrWqltTp += inflAssctBfrWqltTp; + avgInflAssctAftWqltToc += inflAssctAftWqltToc; + avgInflAssctAftWqltSs += inflAssctAftWqltSs; + + avgTpInflWqltBod += tpInflWqltBod; + avgTpInflWqltTp += tpInflWqltTp; + + avgInfoRnDay += infoRnDay; + }); + + const newFilteredData = [ + { + msrDt: '평균', + efflWqltBod: filteredData.length==0?'0.0':(Math.round(avgEfflWqltBod / filteredData.length * 10) / 10).toFixed(1), + efflWqltToc: filteredData.length==0?'0.0':(Math.round(avgefflWqltToc / filteredData.length * 10) / 10).toFixed(1), + efflWqltSs: filteredData.length==0?'0.0':(Math.round(avgEfflWqltSs / filteredData.length * 10) / 10).toFixed(1), + efflWqltTn: filteredData.length==0?'0.000':(Math.round(avgEfflWqltTn / filteredData.length * 1000) / 1000).toFixed(3), + efflWqltTp: filteredData.length==0?'0.000':(Math.round(avgEfflWqltTp / filteredData.length * 1000) / 1000).toFixed(3), + inflAssctAftWqltBod: filteredData.length==0?'0.0':(Math.round(avgInflAssctAftWqltBod / filteredData.length * 10) / 10).toFixed(1), + inflAssctAftWqltTn: filteredData.length==0?'0.000':(Math.round(avgInflAssctAftWqltTn / filteredData.length * 1000) / 1000).toFixed(3), + inflAssctBfrWqltTp: filteredData.length==0?'0.000':(Math.round(avgInflAssctBfrWqltTp / filteredData.length * 1000) / 1000).toFixed(3), + inflAssctAftWqltToc: filteredData.length==0?'0.0':(Math.round(avgInflAssctAftWqltToc / filteredData.length * 10) / 10).toFixed(1), + inflAssctAftWqltSs: filteredData.length==0?'0.0':(Math.round(avgInflAssctAftWqltSs / filteredData.length * 10) / 10).toFixed(1), + bscFctCd: "", + bscFctNm: "", + lclgvrnmntNm: "", + sidoNm: "", + sggNm: "", + addr: "", + fctCpc: 0, + infoRnDay: filteredData.length==0?'0.0':(Math.round(avgInfoRnDay / filteredData.length * 10) / 10).toFixed(1), + tpInflWqltBod: filteredData.length==0?'0.0':(Math.round(avgTpInflWqltBod / filteredData.length * 10) / 10).toFixed(1), + tpInflWqltTp: filteredData.length==0?'0.000':(Math.round(avgTpInflWqltTp / filteredData.length * 1000) / 1000).toFixed(3) + }, + { + msrDt : '최대', + efflWqltBod : Math.round(maxEfflWqltBod * 10) / 10?(Math.round(maxEfflWqltBod * 10) / 10).toFixed(1):'0.0', + efflWqltToc : Math.round(maxefflWqltToc * 10) / 10?(Math.round(maxefflWqltToc * 10) / 10).toFixed(1):'0.0', + efflWqltSs : Math.round(maxEfflWqltSs * 10) / 10?(Math.round(maxEfflWqltSs * 10) / 10).toFixed(1):'0.0', + efflWqltTn : Math.round(maxEfflWqltTn * 1000) / 1000?(Math.round(maxEfflWqltTn * 1000) / 1000).toFixed(3):'0.000', + efflWqltTp : Math.round(maxEfflWqltTp * 1000) / 1000?(Math.round(maxEfflWqltTp * 1000) / 1000).toFixed(3):'0.000', + inflAssctAftWqltBod : Math.round(maxInflAssctAftWqltBod * 10) / 10?(Math.round(maxInflAssctAftWqltBod * 10) / 10).toFixed(1):'0.0', + inflAssctAftWqltTn : Math.round(maxInflAssctAftWqltTn * 1000) / 1000?(Math.round(maxInflAssctAftWqltTn * 1000) / 1000).toFixed(3):'0.000', + inflAssctBfrWqltTp : Math.round(maxInflAssctBfrWqltTp * 1000) / 1000?(Math.round(maxInflAssctBfrWqltTp * 1000) / 1000).toFixed(3):'0.000', + inflAssctAftWqltToc : Math.round(maxInflAssctAftWqltToc * 10) / 10?(Math.round(maxInflAssctAftWqltToc * 10) / 10).toFixed(1):'0.0', + inflAssctAftWqltSs : Math.round(maxInflAssctAftWqltSs * 10) / 10?(Math.round(maxInflAssctAftWqltSs * 10) / 10).toFixed(1):'0.0', + bscFctCd: "", + bscFctNm: "", + lclgvrnmntNm: "", + sidoNm: "", + sggNm: "", + addr: "", + fctCpc: 0, + infoRnDay: 0, + tpInflWqltBod: Math.round(maxTpInflWqltBod * 10) / 10?(Math.round(maxTpInflWqltBod * 10) / 10).toFixed(1):'0.0', + tpInflWqltTp: Math.round(maxTpInflWqltTp * 1000) / 1000?(Math.round(maxTpInflWqltTp * 1000) / 1000).toFixed(3):'0.000' + }, + { + msrDt : '최소', + efflWqltBod : Math.round(minEfflWqltBod * 10) / 10?(Math.round(minEfflWqltBod?minEfflWqltBod:0 * 10) / 10).toFixed(1) : '0.0', + efflWqltToc : Math.round(minefflWqltToc * 10) / 10?(Math.round(minefflWqltToc?minefflWqltToc:0 * 10) / 10).toFixed(1) : '0.0', + efflWqltSs : Math.round(minEfflWqltSs * 10) / 10?(Math.round(minEfflWqltSs * 10) / 10).toFixed(1) : '0.0', + efflWqltTn : Math.round(minEfflWqltTn * 1000) / 1000?(Math.round(minEfflWqltTn * 1000) / 1000).toFixed(3) : '0.000', + efflWqltTp : Math.round(minEfflWqltTp * 1000) / 1000?(Math.round(minEfflWqltTp * 1000) / 1000).toFixed(3) : '0.000', + inflAssctAftWqltBod : Math.round(minInflAssctAftWqltBod * 10) / 10?(Math.round(minInflAssctAftWqltBod * 10) / 10).toFixed(1) : '0.0', + inflAssctAftWqltTn : Math.round(minInflAssctAftWqltTn * 1000) / 1000?(Math.round(minInflAssctAftWqltTn * 1000) / 1000).toFixed(3) : '0.000', + inflAssctBfrWqltTp : Math.round(minInflAssctBfrWqltTp * 1000) / 1000?(Math.round(minInflAssctBfrWqltTp * 1000) / 1000).toFixed(3) : '0.000', + inflAssctAftWqltToc : Math.round(minInflAssctAftWqltToc * 10) / 10?(Math.round(minInflAssctAftWqltToc * 10) / 10).toFixed(1) : '0.0', + inflAssctAftWqltSs : Math.round(minInflAssctAftWqltSs * 10) / 10?(Math.round(minInflAssctAftWqltSs * 10) / 10).toFixed(1) : '0.0', + bscFctCd: "", + bscFctNm: "", + lclgvrnmntNm: "", + sidoNm: "", + sggNm: "", + addr: "", + fctCpc: 0, + infoRnDay: 0, + tpInflWqltBod: Math.round(minTpInflWqltBod * 10) / 10?'0.0':(Math.round(minTpInflWqltBod * 10) / 10).toFixed(1), + tpInflWqltTp: Math.round(minTpInflWqltTp * 1000) / 1000?'0.000':(Math.round(minTpInflWqltTp * 1000) / 1000).toFixed(3) + }, + ...filteredData + ]; + return newFilteredData; + } + + // 날짜 필터링 관련 상태 + const [copyYearList, setCopyYearList] = useState<FacilityWaterQltyProps[]>([]); + const [chongInWaterQltyYearList, setChongInWaterQltyYearList] = useState<FacilityWaterQltyProps[]>([]); + + useEffect(() => { + searchFacilityWaterQltyListHist(searchYear, facilityCd); + }, [searchYear, facilityCd, searchFacilityWaterQltyListHist]); + + useEffect(() => { + const filteredYearData = setFilterAfterData(facilityWaterQltyYearList); + setCopyYearList(filteredYearData as FacilityWaterQltyProps[]); + setChongInWaterQltyYearList(filteredYearData as FacilityWaterQltyProps[]); + }, [facilityWaterQltyYearList]); + + const yesterDay = new Date(); + yesterDay.setDate(yesterDay.getDate() - 1); + const yesterDayStr = yesterDay.toISOString().split('T')[0]; + + const ref = useRef<ReturnType<FacilityWaterQltyProps>>({ + startDate: new Date(), + endDate: new Date(), + tabName: '', + filteredList: [], + }); + + const filterData = useCallback((returnType: ReturnType<FacilityWaterQltyProps>) => { + const filteredData = setFilterAfterData(returnType.filteredList); + setCopyYearList(filteredData as FacilityWaterQltyProps[]); + }, []); + + const excelDownload = () => { + const excelDownList: modalExcelType[] = []; + if(copyYearList.length > 0){ + copyYearList.forEach((item) => { + excelDownList.push({ + 날짜: item.msrDt, + "유입BOD(mg/L)": item.inflAssctAftWqltBod, + "유입TOC(COD)(mg/L)": item.inflAssctAftWqltToc, + "유입SS(mg/L)": item.inflAssctAftWqltSs, + "유입TN(mg/L)": item.inflAssctAftWqltTn, + "유입TP(mg/L)": item.inflAssctBfrWqltTp, + "유출BOD(mg/L)": item.efflWqltBod, + "유출TOC(COD)(mg/L)": item.efflWqltToc, + "유출SS(mg/L)": item.efflWqltSs, + "유출TN(mg/L)": item.efflWqltTn, + "유출TP(mg/L)": item.efflWqltTp, +/* "강우량(mm/일)": item.infoRnDay?item.infoRnDay.toString():"0.0", */ + }); + }); + } + if (excelDownList && excelDownList.length > 0) { + downloadExcel({ data: JSON.parse(JSON.stringify(excelDownList)), fileName: searchYear+'년 '+fcltyName + ' 수질현황', sheetNameArray: [fcltyName + ' 수질현황'], headerRange: 'A1:K1' }); + } else if (prevExcelListRef.current && prevExcelListRef.current.length > 0) { + alert('다운받을 데이터가 없습니다.'); + } + prevExcelListRef.current = excelDownList; + } + + return ( + <> + <UITab tabList={tabList1} defaultValue="tab_num1"> + <Button className={btnClass} style={{float: 'right'}} color="green" onClick={excelDownload}> + {"엑셀다운로드"} + </Button> + {/* 처리시설 수질현황 */} + <TabsContent value="tab_num1"> + <UITab tabList={tabList2} defaultValue="tab_term_year"> + <TabsContent value="tab_term_year"> + <FacilityTermModalHeader<FacilityWaterQltyProps> + searchList={facilityWaterQltyYearList} + fcltyCd={facilityCd} + fcltyName={fcltyName} + tabName={"tab_term_year"} + searchYear={searchYear} + ref={ref} + queryClick={filterData} + /> + <div className="overflow-y-auto h-[350px]"> + <UiTable + headName={undefined} + tableData={copyYearList} + headlist={upHeadListModalYear} + pageSize={0} + total={0} + cellClick={undefined} + onPageChange={() => {}} + className="h-[310px]" + boldArray={[0,1,2]} + > + <HeadMakeColSpan upHeadList={upHeadListModalYear} layoutNum={3} /> + </UiTable> + </div> + <div className="card-header"> + <h3 className="text-2xl font-medium text-label">수질 TMS</h3> + {/*<h3 className="text-2xl font-medium text-label text-right">({msrmtDate?msrmtDate.substring(0,4)+'-'+msrmtDate.substring(4,6)+'-'+msrmtDate.substring(6,8):yesterDayStr})</h3> */} + </div> + <div className="overflow-y-auto h-[250px]"> + <UiTable + headName={undefined} + tableData={tmsList} + headlist={upHeadListModalTms} + pageSize={0} + total={0} + cellClick={undefined} + onPageChange={() => {}} + boldArray={[0,1,2]} + > + <HeadMakeColSpan upHeadList={upHeadListModalTms} layoutNum={4} /> + </UiTable> + {msrmtDate != yesterDayStr?.replaceAll('-','')?<h3 className="text-2xl text-left text-gray-500">※전날 데이터 결측으로 인해 가장 최근 데이터 표출함</h3> : ''} + </div> + </TabsContent> + + <TabsContent value="tab_term_summer"> + <FacilityTermModalHeader<FacilityWaterQltyProps> + searchList={facilityWaterQltyYearList} + fcltyCd={facilityCd} + fcltyName={fcltyName} + tabName="tab_term_summer" + searchYear={searchYear} + ref={ref} + queryClick={filterData} + /> + <div className="overflow-y-auto h-[550px]"> + <UiTable + headName={undefined} + tableData={copyYearList} + headlist={upHeadListModalYear} + pageSize={0} + total={0} + cellClick={undefined} + onPageChange={() => { } } + className="h-[500px]" + boldArray={[0,1,2]} + > + <HeadMakeColSpan upHeadList={upHeadListModalYear} layoutNum={3} /> + </UiTable> + </div> + </TabsContent> + + <TabsContent value="tab_term_winter"> + <FacilityTermModalHeader<FacilityWaterQltyProps> + searchList={facilityWaterQltyYearList} + fcltyCd={facilityCd} + fcltyName={fcltyName} + tabName="tab_term_winter" + searchYear={searchYear} + ref={ref} + queryClick={filterData} + /> + <div className="overflow-y-auto h-[550px]"> + <UiTable + headName={undefined} + tableData={copyYearList} + headlist={upHeadListModalYear} + pageSize={0} + total={0} + cellClick={undefined} + onPageChange={() => {}} + className="h-[500px]" + boldArray={[0,1,2]} + > + <HeadMakeColSpan upHeadList={upHeadListModalYear} layoutNum={3} /> + </UiTable> + </div> + </TabsContent> + + <TabsContent value="tab_term_chuncheon"> + <FacilityTermModalHeader<FacilityWaterQltyProps> + searchList={facilityWaterQltyYearList} + fcltyCd={facilityCd} + fcltyName={fcltyName} + tabName="tab_term_chuncheon" + searchYear={searchYear} + ref={ref} + queryClick={filterData} + /> + <div className="overflow-y-auto h-[550px]"> + <UiTable + headName={undefined} + tableData={copyYearList} + headlist={upHeadListModalYear} + pageSize={0} + total={0} + cellClick={undefined} + onPageChange={() => {}} + className="h-[500px]" + boldArray={[0,1,2]} + > + <HeadMakeColSpan upHeadList={upHeadListModalYear} layoutNum={3} /> + </UiTable> + </div> + <div className="mt-14"> + <span className="text-2xl font-bold text-gray-500">☞ 청천시 : 강우량이 3mm미만의 날</span> + </div> + </TabsContent> + + <TabsContent value="tab_term_rain"> + <FacilityTermModalHeader<FacilityWaterQltyProps> + searchList={facilityWaterQltyYearList} + fcltyCd={facilityCd} + fcltyName={fcltyName} + tabName="tab_term_rain" + searchYear={searchYear} + ref={ref} + queryClick={filterData} + /> + <div className="overflow-y-auto h-[550px]"> + <UiTable + headName={undefined} + tableData={copyYearList} + headlist={upHeadListModalYear} + pageSize={0} + total={0} + cellClick={undefined} + onPageChange={() => {}} + className="h-[500px]" + boldArray={[0,1,2]} + > + <HeadMakeColSpan upHeadList={upHeadListModalYear} layoutNum={3} /> + </UiTable> + </div> + <div className="mt-14"> + <span className="text-2xl font-bold text-gray-500">☞ 강우량 3mm 이상만 날과 강우영향일(강우일+2일)의 날</span> + </div> + </TabsContent> + </UITab> + </TabsContent> + + {/* 총인설비 수질현황 */} + <TabsContent value="tab_num2"> + <UITab tabList={tabList3} defaultValue="tab_term_year_chongin"> + <TabsContent value="tab_term_year_chongin"> + <FacilityTermModalHeader<FacilityWaterQltyProps> + searchList={chongInWaterQltyYearList} + fcltyCd={facilityCd} + fcltyName={fcltyName} + tabName="tab_term_year" + searchYear={searchYear} + ref={ref} + queryClick={filterData} + /> + <div className="overflow-y-auto h-[550px]"> + <UiTable + headName={undefined} + tableData={copyYearList} + headlist={upHeadListModalChongIn} + pageSize={0} + total={0} + cellClick={undefined} + onPageChange={() => {}} + className="h-[500px]" + boldArray={[0,1,2]} + > + <HeadMakeColSpan upHeadList={upHeadListModalChongIn} layoutNum={5} /> + </UiTable> + </div> + </TabsContent> + + <TabsContent value="tab_term_summer_chongin"> + <FacilityTermModalHeader<FacilityWaterQltyProps> + searchList={chongInWaterQltyYearList} + fcltyCd={facilityCd} + fcltyName={fcltyName} + tabName="tab_term_summer" + searchYear={searchYear} + ref={ref} + queryClick={filterData} + /> + <div className="overflow-y-auto h-[550px]"> + <UiTable + headName={undefined} + tableData={copyYearList} + headlist={upHeadListModalChongIn} + pageSize={0} + total={0} + cellClick={undefined} + onPageChange={() => {}} + className="h-[500px]" + boldArray={[0,1,2]} + > + <HeadMakeColSpan upHeadList={upHeadListModalChongIn} layoutNum={5} /> + </UiTable> + </div> + </TabsContent> + + <TabsContent value="tab_term_winter_chongin"> + <FacilityTermModalHeader<FacilityWaterQltyProps> + searchList={chongInWaterQltyYearList} + fcltyCd={facilityCd} + fcltyName={fcltyName} + tabName="tab_term_winter" + searchYear={searchYear} + ref={ref} + queryClick={filterData} + /> + <div className="overflow-y-auto h-[550px]"> + <UiTable + headName={undefined} + tableData={copyYearList} + headlist={upHeadListModalChongIn} + pageSize={0} + total={0} + cellClick={undefined} + onPageChange={() => {}} + className="h-[500px]" + boldArray={[0,1,2]} + > + <HeadMakeColSpan upHeadList={upHeadListModalChongIn} layoutNum={5} /> + </UiTable> + </div> + </TabsContent> + </UITab> + </TabsContent> + </UITab> + </> + ); +} \ No newline at end of file diff --git a/apps/fo/app/pages/facilityWaterQlty/page.tsx b/apps/fo/app/pages/facilityWaterQlty/page.tsx new file mode 100644 index 000000000..7d571a69f --- /dev/null +++ b/apps/fo/app/pages/facilityWaterQlty/page.tsx @@ -0,0 +1,209 @@ +"use client" + +import { useFoStore } from "@/app/store"; +import type { TableUpperProps } from "@/app/store/common"; +import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; +import ListDiv from "@common/business_components/ui/list_div"; +import { useCallback, useEffect, useRef, useState } from "react"; +import WaterQualityModal from "./modal/page"; +import type { FacilityWaterQltyProps } from "@/app/store/facilityWaterQlty"; + +export default function ProcessFacilitySearch() { + const { Common + , FacilityWaterQltyMain + , initializeCommonData + , searchFacilityWaterQltyList + } = useFoStore((state) => state) || { FacilityWaterQltyList: { facilityWaterQltyList: [], facilityWaterQltyCount: 0 } }; + + const { + isInit, + selectUpdownData, + selectOperationData, + selectSidoData, + selectSigunData, + selectRegionData, + selectUnitWaterShedData, + selectMiddleRegionData, + selectEnvPartData, + selectSearchYear, + btnClass + } = Common; + + const { + mainLabelArray, + upHeadList1, + upHeadList2, + upHeadList3, + facilityWaterQltyList, + facilityWaterQltyCount, + excelList + } = FacilityWaterQltyMain; + + const mainList: FacilityWaterQltyProps[] = facilityWaterQltyList || []; + + // 그리드 관련 + const [resetPage, setResetPage] = useState<boolean>(false); + const searchInputRef = useRef<HTMLDivElement & HTMLInputElement>(null); + const [widthSize, setWidthSize] = useState<string>("w-[120%]"); + const [upheadList, setUpheadList] = useState<TableUpperProps[]>(upHeadList1); + const [fcltyName, setFcltyName] = useState<string>(""); + const [operationValue, setOperationValue] = useState<string>("0"); + //const [searchYearValue, setSearchYearValue] = useState<string>(selectSearchYear[0]?.val || "0"); + const [searchYearValue, setSearchYearValue] = useState<string>("2023"); + const [fcltCpct, setFcltCpct] = useState<string>("0"); + + // 모달 상태 추가 + const [selectedRow, setSelectedRow] = useState<number | null>(null); + const [fcltyNm, setFcltyNm] = useState<string>(""); + const [fcltyCd, setFcltyCd] = useState<string>(""); + const [modalYear, setModalYear] = useState<string>(""); + + const searchFcltyQltyList = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => { + let nameCd, partStr; + switch (partValue) { + case 'R': + if (regionValue !== '0') { + if(unitWaterShedValue==='0') nameCd = regionValue; + else if (middleRegionValue==='0') nameCd = unitWaterShedValue; + else nameCd = middleRegionValue; + } + partStr = partValue + break; + default: + if(sidoValue !== '0') nameCd = sidoValue; + if(sigunValue !== '0') nameCd = sigunValue; + partStr = envPartValue !== '0' ? envPartValue:partValue; + break; + } + + if (partStr.length > 1 || fcltCpct !== '0' || operationValue!== '0' ||nameCd ||(fcltyName.trim() !== "" && fcltyName.trim() !== null)) { + searchFacilityWaterQltyList(pageNo, pageCnt, searchYearValue, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined); + } else { + searchFacilityWaterQltyList(pageNo, pageCnt, searchYearValue); + } + }, [operationValue, fcltyName, searchFacilityWaterQltyList, searchYearValue, fcltCpct]); + + const resetVal = () => { + setWidthSize("w-[120%]"); + setUpheadList(upHeadList1) + // 상태값 설정 + setFcltCpct(selectUpdownData[0]?.val || "0"); + setOperationValue(selectOperationData[0]?.val || "0"); + setSearchYearValue(selectSearchYear[0]?.val || "0"); + // SearchInputSelect 초기화 + setFcltyName(""); + if (searchInputRef.current) { + const input = searchInputRef.current.querySelector('input'); + if (input) { + input.value = ""; + } + } + searchFacilityWaterQltyList(1, 10, selectSearchYear[0]?.val || "0"); + } + + // 셀 클릭 시 모달 표시 + const cellClick = (index: number) => { + const facility = facilityWaterQltyList[index]; + if (facility) { + setFcltyNm(facility.bscFctNm); + setFcltyCd(facility.bscFctCd); + setModalYear(searchYearValue); + setSelectedRow(index); // 클릭한 행의 index 저장 + } + } + + // 모달 닫기 + const closeModal = () => { + setSelectedRow(null); + } + const ref = useRef<ReturnType>({ + partValue: '', + sidoValue: '', + sigunValue: '', + regionValue: '', + unitWaterShedValue: '', + middleRegionValue: '', + envPartValue: '', + sidoText: '', + sigunText: '' + }); + + useEffect(() => { + if (!isInit) { + initializeCommonData(); + searchFacilityWaterQltyList(1, 10, selectSearchYear[0]?.val || "0"); + } + }, [isInit, initializeCommonData, searchFacilityWaterQltyList, selectSearchYear]); + + useEffect(() => { + if (excelList && excelList.length > 0) { + downloadExcel({ + data: excelList as unknown as JSON[], + fileName: '시설별 수질현황', + sheetNameArray: ['시설별 수질현황'], + headerRange: 'A1:T1' + }); + } + }, [excelList]); + + const chkVal = useCallback((returntype:ReturnType) => { + setWidthSize(returntype.partValue=='A' ?"w-[120%]":"w-[130%]") + setUpheadList(returntype.partValue == 'R' ? upHeadList2 : returntype.partValue == 'G'?upHeadList3:upHeadList1); + setResetPage(true); + }, [upHeadList1, upHeadList2, upHeadList3]); + + const excelDownload = useCallback(() => { + searchFcltyQltyList(ref.current, 1, 10000); + }, [searchFcltyQltyList]); + + const handlePageChange = useCallback((page: number) => { + setResetPage(false); + searchFcltyQltyList(ref.current, page, 10); + }, [searchFcltyQltyList]); + + const total = facilityWaterQltyCount || 1; + console.log('total : ', total); + return ( + <MainDiv> + <Title title="시설별 수질현황" /> + <ContentsDiv> + <SearchBaseForm + leftChildren={<> + <SelectBox className="col-span-3" label={mainLabelArray[3] as string} selectArray={selectUpdownData} onSelectValue={(val) => setFcltCpct(val.toString())} value={fcltCpct} selectClass={undefined} /> + <SearchInput className="col-span-3" label={mainLabelArray[4] as string} textValue={fcltyName} setText={setFcltyName} /> + <SelectBox className="col-span-3" label={mainLabelArray[5] as string} selectArray={selectSearchYear} onSelectValue={(val) => setSearchYearValue(val.toString())} value={searchYearValue} selectClass={undefined} /> + <SelectBox className="col-span-3" label={mainLabelArray[6] as string} selectArray={selectOperationData} onSelectValue={(val) => setOperationValue(val.toString())} value={operationValue} selectClass={undefined} /> + </>} + rightChildren={undefined} + selectSidoData={selectSidoData} + selectSigunData={selectSigunData} + selectRegionData={selectRegionData} + selectUnitWaterShedData={selectUnitWaterShedData} + selectMiddleRegionData={selectMiddleRegionData} + selectEnvPartData={selectEnvPartData} + btnClass={btnClass} + queryClick={chkVal} + excelClick={excelDownload} + initClick={resetVal} + ref={ref} isAdmin={false} /> + <ListDiv> + <div className="h-full w-full overflow-x-scroll"> + <div className={widthSize}> + <UiTable cellClick={(index: number | undefined) => index !== undefined && cellClick(index)} headName={"시설별 수질현황"} tableData={mainList} headlist={upheadList} pageSize={10} total={total<10?facilityWaterQltyList.length:total} resetPageNo={resetPage} onPageChange={handlePageChange} /> + </div> + </div> + </ListDiv> + </ContentsDiv> + {selectedRow !== null && ( + <UiModal + body={<WaterQualityModal fcltyName={fcltyNm} facilityCd={fcltyCd} searchYear={modalYear}></WaterQualityModal>} + bodyClass="sm:max-w-[90vw] w-[1300px] h-[920px]" + open={selectedRow !== null} + onOpenChange={(open) => !open && closeModal()} + noHeaderYn="Y" + noFooterYn="Y" + /> + )} + </MainDiv> + ); +} \ No newline at end of file diff --git a/apps/fo/app/pages/flowRate/modal/page.tsx b/apps/fo/app/pages/flowRate/modal/page.tsx new file mode 100644 index 000000000..409dab7eb --- /dev/null +++ b/apps/fo/app/pages/flowRate/modal/page.tsx @@ -0,0 +1,307 @@ +"use client" +import FacilityTermModalHeader from "@/app/components/common/FacilityTermModalHeader"; +import { useFoStore } from "@/app/store"; +import type { FlowRateModalInterface, modalExcelType } from "@/app/store/flowRate"; +import { downloadExcel, UITab, UiTable } from "@common/business_components/ui"; +import { Button, TabsContent } from "@common/components/ui"; +import { useCallback, useEffect, useRef, useState } from "react"; +import type { ReturnType } from "../../../components/common/FacilityTermModalHeader"; + +const tabList = [ + { value: 'tab_term_year', label: '연간' }, + { value: 'tab_term_summer', label: '하절기' }, + { value: 'tab_term_winter', label: '동절기' }, + { value: 'tab_term_chuncheon', label: '청천시' }, + { value: 'tab_term_rain', label: '강우시' } +]; + +export default function FlowRateModal(props: { fcltyName: string; facilityCd: string; searchYear: string }) { + // 그리드 관련 + const { Common, FlowRateModal, searchFlowRateModalList } = useFoStore((state) => state); + const { btnClass } = Common; + const [copyYearList, setCopyYearList] = useState<FlowRateModalInterface[]>([]); + const [isLoading, setIsLoading] = useState(true); + const prevExcelListRef = useRef<modalExcelType[] | null>(null); + const { + modalFlowRateListYear, + upHeadListFlowRateModalYear, + } = FlowRateModal; + + const ref = useRef<ReturnType<FlowRateModalInterface>>({ + startDate: new Date(), + endDate: new Date(), + tabName: '', + filteredList: [], + }); + + const filterData = useCallback((returnType: ReturnType<FlowRateModalInterface>) => { + if (!returnType.filteredList) return; + // 타입을 FlowRateModalInterface[]로 강제 변환하여 타입 오류 해결 + const filteredData = setFilterAfterData(returnType.filteredList as FlowRateModalInterface[]); + setCopyYearList(filteredData); + }, []); + + // 필터링 된 후 최대, 최소, 평균 값 계산 + const setFilterAfterData = (filteredData: FlowRateModalInterface[]) => { + if (!filteredData || filteredData.length === 0) return []; + + // 쉼표가 포함된 문자열을 숫자로 변환하는 함수 + const parseNumber = (value: string | number | null | undefined): number => { + if (value === null || value === undefined || value === '') return 0; + if (typeof value === 'number') return value; + return Number(value.replace(/,/g, '')); + }; + + console.log('filteredData : ', filteredData); + let maxtotInflAmnt = 0; + let maxinflAmnt = 0; + let maxsumFlow = 0; + let maxreturnAmnt = 0; + let maxefflAmnt = 0; + let maxinfoRnDay = 0; + + // 초기값 설정 + let mintotInflAmnt = parseNumber(filteredData[0]?.totInflAmnt); + let mininflAmnt = parseNumber(filteredData[0]?.inflAmnt); + let minsumFlow = parseNumber(filteredData[0]?.sumFlow); + let minreturnAmnt = parseNumber(filteredData[0]?.returnAmnt); + let minefflAmnt = parseNumber(filteredData[0]?.efflAmnt); + let mininfoRnDay = parseNumber(filteredData[0]?.infoRnDay && filteredData[0]?.infoRnDay.toString().trim() !== 'x' ? filteredData[0]?.infoRnDay.toString().trim() : 0); + + let avgtotInflAmnt = 0; + let avginflAmnt = 0; + let avgsumFlow = 0; + let avgreturnAmnt = 0; + let avgefflAmnt = 0; + let avginfoRnDay = 0; + + filteredData.forEach((item: FlowRateModalInterface) => { + const totInflAmnt = parseNumber(item.totInflAmnt); + const inflAmnt = parseNumber(item.inflAmnt); + const sumFlow = parseNumber(item.sumFlow); + const returnAmnt = parseNumber(item.returnAmnt); + const efflAmnt = parseNumber(item.efflAmnt); + const infoRnDay = parseNumber(item.infoRnDay && item.infoRnDay.toString().trim() !== 'x' ? item.infoRnDay.toString().trim() : 0); + maxtotInflAmnt = Math.max(maxtotInflAmnt, totInflAmnt); + maxinflAmnt = Math.max(maxinflAmnt, inflAmnt); + maxsumFlow = Math.max(maxsumFlow, sumFlow); + maxreturnAmnt = Math.max(maxreturnAmnt, returnAmnt); + maxefflAmnt = Math.max(maxefflAmnt, efflAmnt); + maxinfoRnDay = Math.max(maxinfoRnDay, infoRnDay); + + mintotInflAmnt = Math.min(mintotInflAmnt, totInflAmnt); + mininflAmnt = Math.min(mininflAmnt, inflAmnt); + minsumFlow = Math.min(minsumFlow, sumFlow); + minreturnAmnt = Math.min(minreturnAmnt, returnAmnt); + minefflAmnt = Math.min(minefflAmnt, efflAmnt); + mininfoRnDay = Math.min(mininfoRnDay, infoRnDay); + + avgtotInflAmnt += totInflAmnt; + avginflAmnt += inflAmnt; + avgsumFlow += sumFlow; + avgreturnAmnt += returnAmnt; + avgefflAmnt += efflAmnt; + avginfoRnDay += infoRnDay; + }); + + const newFilteredData = [ + { + msrDt: '평균', + totInflAmnt: Math.round(avgtotInflAmnt / filteredData.length * 10) / 10, + inflAmnt: Math.round(avginflAmnt / filteredData.length * 10) / 10, + sumFlow: Math.round(avgsumFlow / filteredData.length * 10) / 10, + returnAmnt: Math.round(avgreturnAmnt / filteredData.length * 10) / 10, + efflAmnt: Math.round(avgefflAmnt / filteredData.length * 10) / 10, + infoRnDay: avginfoRnDay > 0 ? Math.round(avginfoRnDay / filteredData.length * 10) / 10 : 0, + rainEffectDay: 0, + }, + { + msrDt: '최대', + totInflAmnt: Math.round(maxtotInflAmnt * 10) / 10, + inflAmnt: Math.round(maxinflAmnt * 10) / 10, + sumFlow: Math.round(maxsumFlow * 10) / 10, + returnAmnt: Math.round(maxreturnAmnt * 10) / 10, + efflAmnt: Math.round(maxefflAmnt * 10) / 10, + infoRnDay: maxinfoRnDay > 0 ? (Math.round(maxinfoRnDay * 10) / 10).toFixed(1) : '0.0', + rainEffectDay: 0, + }, + { + msrDt: '최소', + totInflAmnt: Math.round(mintotInflAmnt * 10) / 10, + inflAmnt: Math.round(mininflAmnt * 10) / 10, + sumFlow: Math.round(minsumFlow * 10) / 10, + returnAmnt: Math.round(minreturnAmnt * 10) / 10, + efflAmnt: Math.round(minefflAmnt * 10) / 10, + infoRnDay: mininfoRnDay > 0 ? (Math.round(mininfoRnDay * 10) / 10).toFixed(1) : '0.0', + rainEffectDay: 0, + }, + ...filteredData + ]; + return newFilteredData; + } + + useEffect(() => { + const fetchData = async () => { + setIsLoading(true); + try { + await searchFlowRateModalList(props.facilityCd, props.searchYear); + } finally { + setIsLoading(false); + } + }; + fetchData(); + }, [props.facilityCd, props.searchYear, searchFlowRateModalList]); + + if (isLoading) { + return <div>로딩 중...</div>; + } + + const excelDownload = () => { + const excelDownList: modalExcelType[] = []; + if(copyYearList.length > 0){ + copyYearList.forEach((item) => { + excelDownList.push({ + "날짜": item.msrDt, + "총유입량(m³/일)": item.totInflAmnt, + "하수유입량(m³/일)": item.inflAmnt, + "연계처리량(m³/일)": item.sumFlow, + "총인반류수량(m³/일)": item.returnAmnt, + "방류량(m³/일)": item.efflAmnt, + /* "강우량(mm/일)": item.infoRnDay, */ + }); + }); + } + if (excelDownList && excelDownList.length > 0) { + downloadExcel({ data: JSON.parse(JSON.stringify(excelDownList)), fileName: props.searchYear+'년 '+props.fcltyName + ' 유량현황', sheetNameArray: [props.fcltyName + ' 유량현황'], headerRange: 'A1:G1' }); + } else if (prevExcelListRef.current && prevExcelListRef.current.length > 0) { + alert('다운받을 데이터가 없습니다.'); + } + prevExcelListRef.current = excelDownList; + } + + return ( + <> + <UITab tabList={tabList} defaultValue="tab_term_year"> + <Button className={btnClass} style={{float: 'right'}} color="green" onClick={excelDownload}> + {"엑셀다운로드"} + </Button> + <TabsContent value="tab_term_year"> + <FacilityTermModalHeader<FlowRateModalInterface> + searchList={modalFlowRateListYear} + fcltyCd={props.facilityCd} + fcltyName={props.fcltyName} + tabName={"tab_term_year"} + searchYear={props.searchYear} + ref={ref} + queryClick={filterData} + /> + <div className="h-[370px] overflow-y-auto overflow-x-hidden"> + <UiTable + headName="유량현황" + tableData={copyYearList} + headlist={upHeadListFlowRateModalYear} + pageSize={0} + total={0} + cellClick={undefined} + onPageChange={() => {}} + className="h-[370px]" + boldArray={[0,1,2]} + /> + </div> + </TabsContent> + <TabsContent value="tab_term_summer"> + <FacilityTermModalHeader<FlowRateModalInterface> + searchList={modalFlowRateListYear} + fcltyCd={props.facilityCd} + fcltyName={props.fcltyName} + tabName={"tab_term_summer"} + searchYear={props.searchYear} + ref={ref} + queryClick={filterData} + /> + <div className="h-[370px] overflow-y-auto overflow-x-hidden"> + <UiTable + headName="유량현황" + tableData={copyYearList} + headlist={upHeadListFlowRateModalYear} + pageSize={0} + total={0} + cellClick={undefined} + onPageChange={() => {}} + className="h-[370px]" + boldArray={[0,1,2]} + /> + </div> + </TabsContent> + <TabsContent value="tab_term_winter"> + <FacilityTermModalHeader<FlowRateModalInterface> + searchList={modalFlowRateListYear} + fcltyCd={props.facilityCd} + fcltyName={props.fcltyName} + tabName={"tab_term_winter"} + searchYear={props.searchYear} + ref={ref} + queryClick={filterData} + /> + <div className="h-[370px] overflow-y-auto overflow-x-hidden"> + <UiTable + headName="유량현황" + tableData={copyYearList} + headlist={upHeadListFlowRateModalYear} + pageSize={0} + total={0} + cellClick={undefined} + onPageChange={() => {}} + className="h-[370px]" + boldArray={[0,1,2]} + /> + </div> + </TabsContent> + <TabsContent value="tab_term_chuncheon"> + <FacilityTermModalHeader<FlowRateModalInterface> + searchList={modalFlowRateListYear} + fcltyCd={props.facilityCd} + fcltyName={props.fcltyName} + tabName={"tab_term_chuncheon"} + searchYear={props.searchYear} + ref={ref} + queryClick={filterData} + /> + <div className="h-[370px] overflow-y-auto overflow-x-hidden"> + <UiTable + headName="유량현황" + tableData={copyYearList} + headlist={upHeadListFlowRateModalYear} + pageSize={0} + total={0} + cellClick={undefined} + onPageChange={() => {}} + className="h-[370px]" + boldArray={[0,1,2]} + /> + </div> + <div className="mt-14"> + <span className="text-2xl font-bold text-gray-500">☞ 청천시 : 강우량이 3mm미만의 날</span> + </div> + </TabsContent> + <TabsContent value="tab_term_rain"> + <FacilityTermModalHeader<FlowRateModalInterface> + searchList={modalFlowRateListYear} + fcltyCd={props.facilityCd} + fcltyName={props.fcltyName} + tabName={"tab_term_rain"} + searchYear={props.searchYear} + ref={ref} + queryClick={filterData} + /> + <div className="h-[370px] overflow-y-auto overflow-x-hidden"> + <UiTable headName="유량현황" tableData={copyYearList} headlist={upHeadListFlowRateModalYear} pageSize={0} total={0} cellClick={undefined} onPageChange={() => {}} className="h-[370px]" boldArray={[0,1,2]}/> + </div> + <div className="mt-14"> + <span className="text-2xl font-bold text-gray-500">☞ 강우량 3mm 이상만 날과 강우영향일(강우일+2일)의 날</span> + </div> + </TabsContent> + </UITab> + </> + ); +} \ No newline at end of file diff --git a/apps/fo/app/pages/flowRate/page.tsx b/apps/fo/app/pages/flowRate/page.tsx new file mode 100644 index 000000000..0c4f503e9 --- /dev/null +++ b/apps/fo/app/pages/flowRate/page.tsx @@ -0,0 +1,209 @@ +"use client" +import { useFoStore } from "@/app/store"; +import type { TableUpperProps } from "@/app/store/common"; +import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; +import ListDiv from "@common/business_components/ui/list_div"; +import { useCallback, useEffect, useRef, useState } from "react"; +import FlowRateModal from "./modal/page"; +import type { ExcelType } from "@/app/store/flowRate"; + +export default function ProcessFacilitySearch() { + const {Common + ,FlowRateSearch + ,FlowRateList + ,initializeCommonData + ,searchFlowRateList + } = useFoStore((state) => state); + + const searchInputRef = useRef<HTMLDivElement & HTMLInputElement>(null); + + const { + isInit, + selectUpdownData, + selectSearchYear, + selectOperationData, + selectSidoData, + selectSigunData, + selectRegionData, + selectUnitWaterShedData, + selectMiddleRegionData, + selectEnvPartData, + btnClass + } = Common; + + const { + flowRateSearchLabelArray + } = FlowRateSearch; + + // 그리드 관련 + const { + upHeadList1, + upHeadList2, + upHeadList3, + flowRateList, + flowRateCount, + excelList + } = FlowRateList; + + // 입력값 상태 관리 + const [fcltCpct, setFcltCpct] = useState<string>("0"); + const [fcltyName, setFcltyName] = useState<string>(""); + const [operationValue, setOperationValue] = useState<string>("0"); + //const [searchYearValue, setSearchYearValue] = useState<string>(selectSearchYear[0]?.val || "0"); + const [searchYearValue, setSearchYearValue] = useState<string>("2023"); + const [widthSize, setWidthSize] = useState<string>("w-[120%]"); + const [upheadList, setUpheadList] = useState<TableUpperProps[]>(upHeadList1); + const [resetPage, setResetPage] = useState<boolean>(false); + const prevExcelListRef = useRef<ExcelType[] | null>(null); + + // 모달 상태 추가 + const [selectedRow, setSelectedRow] = useState<number | null>(null); + const [fcltyNm, setFcltyNm] = useState<string>(""); + const [fcltyCd, setFcltyCd] = useState<string>(""); + + const searchFlowRate = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => { + let nameCd, partStr + switch (partValue) { + case 'R': + if (regionValue !== '0') { + if(unitWaterShedValue==='0') nameCd = regionValue; + else if (middleRegionValue==='0') nameCd = unitWaterShedValue; + else nameCd = middleRegionValue; + } + partStr = partValue + break; + default: + if(sidoValue !== '0') nameCd = sidoValue; + if(sigunValue !== '0') nameCd = sigunValue; + partStr = envPartValue !== '0' ? envPartValue:partValue; + break; + } + if (partStr.length > 1 || fcltCpct !== '0' || operationValue!== '0' ||nameCd ||(fcltyName.trim() !== "" && fcltyName.trim() !== null)) { + searchFlowRateList(pageNo, pageCnt, searchYearValue, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined); + } else { + searchFlowRateList(pageNo, pageCnt, searchYearValue); + } + }, [operationValue, fcltyName, searchFlowRateList, searchYearValue, fcltCpct]); + + const resetVal = () => { + setWidthSize("w-[120%]"); + setUpheadList(upHeadList1) + // 상태값 설정 + setFcltCpct(selectUpdownData[0]?.val || "0"); + setOperationValue(selectOperationData[0]?.val || "0"); + setSearchYearValue(selectSearchYear[0]?.val || "0"); + // SearchInputSelect 초기화 + setFcltyName(""); + if (searchInputRef.current) { + const input = searchInputRef.current.querySelector('input'); + if (input) { + input.value = ""; + } + } + searchFlowRateList(1, 10, selectSearchYear[0]?.val || "0"); + } + + // 셀 클릭 시 모달 표시 + const cellClick = (index: number) => { + if (flowRateList[index]) { + setFcltyNm(flowRateList[index].bscFctNm); + setFcltyCd(flowRateList[index].bscFctCd); + setSelectedRow(index); // 클릭한 행의 index 저장 + } + }; + + // 페이지 네비게이션션 + const handlePageChange = useCallback((page: number) => { + setResetPage(false); + searchFlowRate(ref.current, page, 10); + }, [searchFlowRate]); + + // 모달 닫기 + const closeModal = () => { + setSelectedRow(null); + }; + + // 시도 데이터 초기화 + useEffect(() => { + if(!isInit) { + console.log('searchYearValue', searchYearValue, isInit); + initializeCommonData(); + searchFlowRateList(1, 10, searchYearValue || "0"); + } + }, [isInit, initializeCommonData, searchFlowRateList, searchYearValue]); + + useEffect(() => { + if (excelList && excelList.length > 0) { + downloadExcel({ data: JSON.parse(JSON.stringify(excelList)), fileName: '시설별 유량현황', sheetNameArray: ['시설별 유량현황'], headerRange: 'A1:O1' }); + } else if (prevExcelListRef.current && prevExcelListRef.current.length > 0) { + alert('다운받을 데이터가 없습니다.'); + } + prevExcelListRef.current = excelList; + }, [excelList]); + + const ref = useRef<ReturnType>({ + partValue: '', + sidoValue: '', + sigunValue: '', + regionValue: '', + unitWaterShedValue: '', + middleRegionValue: '', + envPartValue: '', + sidoText: '', + sigunText: '' + }); + + const chkVal = useCallback((returntype:ReturnType) => { + setWidthSize(returntype.partValue=='A' ?"w-[120%]":"w-[130%]") + setUpheadList(returntype.partValue == 'R' ? upHeadList2 : returntype.partValue == 'G'?upHeadList3:upHeadList1); + setResetPage(true); + }, [upHeadList1, upHeadList2, upHeadList3]); + + const excelDownload = useCallback(() => { + searchFlowRate(ref.current, 1, 10000); + }, [searchFlowRate]); + + const total = flowRateCount; + + return ( + <MainDiv> + <Title title="시설별 유량현황" /> + <ContentsDiv> + <SearchBaseForm + leftChildren={<> + <SelectBox className="col-span-3" label={flowRateSearchLabelArray[3] as string} selectArray={selectUpdownData} onSelectValue={(val) => setFcltCpct(val.toString())} value={fcltCpct} selectClass={undefined} /> + <SearchInput className="col-span-3" label={flowRateSearchLabelArray[4] as string} textValue={fcltyName} setText={setFcltyName} /> + <SelectBox className="col-span-3" label={flowRateSearchLabelArray[5] as string} selectArray={selectSearchYear} onSelectValue={(val) => setSearchYearValue(val.toString())} value={searchYearValue} selectClass={undefined} /> + <SelectBox className="col-span-3" label={flowRateSearchLabelArray[6] as string} selectArray={selectOperationData} onSelectValue={(val) => setOperationValue(val.toString())} value={operationValue} selectClass={undefined} /> + </>} + rightChildren={undefined} + selectSidoData={selectSidoData} + selectSigunData={selectSigunData} + selectRegionData={selectRegionData} + selectUnitWaterShedData={selectUnitWaterShedData} + selectMiddleRegionData={selectMiddleRegionData} + selectEnvPartData={selectEnvPartData} + btnClass={btnClass} + queryClick={chkVal} + excelClick={excelDownload} + initClick={resetVal} + ref={ref} isAdmin={false} /> + <ListDiv> + <div className="h-full w-full overflow-x-scroll"> + <div className={widthSize}> + <UiTable cellClick={(index: number | undefined) => index !== undefined && cellClick(index)} headName={"시설별 유량현황"} tableData={flowRateList} headlist={upheadList} pageSize={10} total={total} resetPageNo={resetPage} onPageChange={handlePageChange} /> + </div> + </div> + </ListDiv> + </ContentsDiv> + {selectedRow !== null && ( + <UiModal + body={<FlowRateModal fcltyName={fcltyNm} facilityCd={fcltyCd} searchYear={searchYearValue}></FlowRateModal>} + bodyClass="sm:max-w-[90vw] w-[1250px] h-[780px]" + open={selectedRow !== null} + onOpenChange={(open: boolean) => !open && closeModal()} + /> + )} + </MainDiv> + ); +} diff --git a/apps/fo/app/pages/map/page.tsx b/apps/fo/app/pages/map/page.tsx new file mode 100644 index 000000000..cbbe0dd11 --- /dev/null +++ b/apps/fo/app/pages/map/page.tsx @@ -0,0 +1,145 @@ +"use client"; + +import Gis from "@common/business_components/ui/map/Gis"; +import GisApp from "@common/business_components/ui/map/lib/gis.core"; +import { useEffect, useRef, useState } from "react"; + +interface FeatureData { + fid?: string; + ftrIdn?: string; + id?: string; + getGeometry?: () => { getExtent: () => number[] }; + setStyle?: () => void; +} + +interface MapInstance { + getLayers: () => { getArray: () => Array<{ getSource?: () => { getFeatureById?: (id: string) => FeatureData | null } }> }; + getView: () => { fit: (extent: number[], options: object) => void }; +} + +const Map = () => { + const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 + const gisMaps = useRef<MapInstance | null>(null); + const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 + const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태 + const [featureData, setFeatureData] = useState<FeatureData | null>(null); // 선택된 피쳐 데이터 + + const fetchData = async () => { + try { + const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}/v1/map` + const reqUrl = `${API_URL}/lyrStngs/FO_NETWORK`; + const response = await fetch(reqUrl, { + method: "GET", + headers: { + accept: "application/json", + } + }); + + if (!response.ok) { + throw new Error(`HTTP 오류! 상태: ${response.status}`); + } + + const data = await response.json(); // 여기서 json 파싱 + setTreeData(data); // 데이터 설정 + } catch (error) { + console.error("데이터 불러오기 실패:", error); + } + }; + + // 컴포넌트 마운트 시 1회 실행 + useEffect(() => { + fetchData(); + }, []); + + // SearchModal, GisContent 등에서 호출 + const onFeatureSelect = (data: FeatureData) => { + const map = gisMaps.current; + if (!map) { + console.warn("지도가 초기화되지 않았습니다."); + return; + } + + // 항상 같은 select 객체 사용 + const select = (GisApp.Module.select as unknown) as { resetSelectedFeature: () => void; selectFeature: (feature: FeatureData, selected: boolean) => void } | undefined; + if (!select) return; + + // 1. 기존 선택 초기화 + select.resetSelectedFeature(); + + // 2. data가 OpenLayers Feature 인스턴스인지 확인 + let foundFeature: FeatureData | null = null; + if ( + data && + typeof data.getGeometry === "function" && + typeof data.setStyle === "function" + ) { + // 이미 Feature 인스턴스라면 그대로 사용 + foundFeature = data; + } else { + // 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용 + const featureId = data.fid || data.ftrIdn || data.id; + map + .getLayers() + .getArray() + .forEach((layer) => { + const source = layer.getSource?.(); + if (source?.getFeatureById) { + const feature = source.getFeatureById(String(featureId).trim()); + if (feature) foundFeature = feature; + } + }); + } + + // 3. 피처 선택 + if (foundFeature && foundFeature.getGeometry) { + const geometry = foundFeature.getGeometry(); + if (geometry) { + const extent = geometry.getExtent(); + + map.getView().fit(extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 16, + constrainResolution: true, + nearest: true, + }); + } + select.selectFeature(foundFeature, true); + } else { + console.warn("OpenLayers Feature를 찾을 수 없습니다:", data); + } + + setFeatureData(data); + setIsOpen(true); + setIsSearchModalOpen(false); + }; + + // onCheckedLayersChange handler + const handleCheckedLayersChange = (checkedIds: string[], checkedNodesData: unknown[]) => { + // Handle checked layers change if needed + console.log('Checked layers changed:', checkedIds, checkedNodesData); + }; + + return ( + <div className="bg-container"> + <Gis + treeData={treeData} + isSearch={false} + gisMaps={gisMaps} + isMiniMap={true} + searchView={null} + detailView={null} + onFeatureSelect={onFeatureSelect} + setIsOpen={setIsOpen} + setFeatureData={setFeatureData} + featureData={featureData} + setIsSearchModalOpen={setIsSearchModalOpen} + isOpen={isOpen} + isSearchModalOpen={isSearchModalOpen} + onCheckedLayersChange={handleCheckedLayersChange} + /> + </div> + ); +}; + +export default Map; diff --git a/apps/fo/app/pages/planSewage/page.tsx b/apps/fo/app/pages/planSewage/page.tsx new file mode 100644 index 000000000..c9324c246 --- /dev/null +++ b/apps/fo/app/pages/planSewage/page.tsx @@ -0,0 +1,203 @@ +"use client" +import { useFoStore } from "@/app/store"; +import type { TableUpperProps } from "@/app/store/common"; +import { downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiTable, type ReturnType } from "@common/business_components"; +import ListDiv from "@common/business_components/ui/list_div"; +import { useCallback, useEffect, useRef, useState } from "react"; + +export default function PlanSewage() { + const {Common + ,PlanSewage + ,PlanSewageSearch + ,initializeCommonData + ,searchPlanSewageList + } = useFoStore((state) => state) || { processAreaList: { processAreaList: [] }}; + + const { + isInit, + selectUpdownData, + selectOperationData, + selectSidoData, + selectSigunData, + selectRegionData, + selectUnitWaterShedData, + selectMiddleRegionData, + selectEnvPartData, + selectSearchYear, + btnClass + } = Common; + + const { + labelArray, + } = PlanSewageSearch; + + const { + upHeadList1, + upHeadList2, + upHeadList3, + planSewageList, + planSewageCount, + excelList + } = PlanSewage; + + const ref = useRef<ReturnType>({ + partValue: '', + sidoValue: '', + sigunValue: '', + regionValue: '', + unitWaterShedValue: '', + middleRegionValue: '', + envPartValue: '', + sidoText: '', + sigunText: '' + }); + + // 그리드 관련 + const searchInputRef = useRef<HTMLDivElement & HTMLInputElement>(null); + + // 모달 상태 추가 + + // 초기화 변수 + const [resetPage, setResetPage] = useState<boolean>(false); + // 표 너비 + const [widthSize, setWidthSize] = useState<string>("w-[120%]"); + // 구분별 그리드 헤더 + const [upheadList, setUpheadList] = useState<TableUpperProps[]>(upHeadList1); + // 용량별 + const [fcltCpct, setFcltCpct] = useState<string>("0"); + // 시설명 + const [fcltyName, setFcltyName] = useState<string>(""); + // 시설상태 + const [operationValue, setOperationValue] = useState<string>("0"); + // 조회기간 + //const [searchYear, setSearchYear] = useState<string>(selectSearchYear[0]?.val || "0"); + const [searchYear, setSearchYear] = useState<string>("2023"); + + /** + * 페이지 함수 + */ + // 초기화 + const resetVal = () => { + setWidthSize("w-[120%]"); + setUpheadList(upHeadList1); + // 상태값 설정 + setFcltCpct(selectUpdownData[0]?.val || "0"); + setOperationValue(selectOperationData[0]?.val || "0"); + setSearchYear(selectSearchYear[0]?.val || "0"); + // SearchInputSelect 초기화 + setFcltyName(""); + if (searchInputRef.current) { + const input = searchInputRef.current.querySelector('input'); + if (input) { + input.value = ""; + } + } + } + + /** + * useEffect + */ + useEffect(() => { + if (!isInit) { + initializeCommonData(); + searchPlanSewageList(1, 10, searchYear, ref.current.partValue); + } + }, [isInit, initializeCommonData, searchYear, searchPlanSewageList]); + + useEffect(() => { + console.log('excelList' , excelList); + if (excelList && excelList.length > 0) { + downloadExcel({ + data: excelList as unknown as JSON[], + fileName: '계획하수량 산정지표 현황', + sheetNameArray: ['계획하수량 산정지표 현황'], + headerRange: 'A1:M1' + }); + } + }, [excelList]); + + /** + * 콜백함수 + */ + // 조회 + const searchPlanSewage = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => { + let nameCd, partStr + switch (partValue) { + case 'R': + if (regionValue !== '0') { + if(unitWaterShedValue==='0') nameCd = regionValue; + else if (middleRegionValue==='0') nameCd = unitWaterShedValue; + else nameCd = middleRegionValue; + } + partStr = partValue + break; + default: + if(sidoValue !== '0') nameCd = sidoValue; + if(sigunValue !== '0') nameCd = sigunValue; + partStr = envPartValue !== '0' ? envPartValue:partValue; + break; + } + if (partStr.length > 1 || fcltCpct !== '0' || operationValue!== '0' ||nameCd ||(fcltyName.trim() !== "" && fcltyName.trim() !== null)) { + searchPlanSewageList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined); + } else { + searchPlanSewageList(pageNo, pageCnt, searchYear, partValue); + } + }, [operationValue, fcltyName, searchPlanSewageList, searchYear, fcltCpct]); + + const chkVal = useCallback((returntype:ReturnType) => { + setWidthSize(returntype.partValue=='A' ?"w-[120%]":"w-[130%]") + setUpheadList(returntype.partValue == 'R' ? upHeadList2 : returntype.partValue == 'G'?upHeadList3:upHeadList1); + setResetPage(true); + }, [upHeadList1, upHeadList2, upHeadList3]); + + // 엑셀다운로드 + const excelDownload = useCallback((searchReturn: ReturnType) => { + searchPlanSewage(searchReturn, 1, 10000); + }, [searchPlanSewage]); + + // 페이지 변경 + const handlePageChange = useCallback((page: number) => { + setResetPage(false); + searchPlanSewage(ref.current, page, 10); + }, [searchPlanSewage]); + + const mainList = planSewageList || []; + const total = planSewageCount || 0; + + return ( + <MainDiv> + <Title title="계획하수량 산정지표 현황" /> + <SearchBaseForm + leftChildren={<> + <SelectBox className="col-span-3" label={labelArray[3] as string} selectArray={selectUpdownData} onSelectValue={(val) => setFcltCpct(val.toString())} value={fcltCpct} selectClass={undefined} /> + <SearchInput className={"col-span-3"} label={labelArray[4] as string} textValue={fcltyName} setText={setFcltyName} /> + <SelectBox className="col-span-3" label={labelArray[5] as string} selectArray={selectSearchYear} onSelectValue={(val) => setSearchYear(val.toString())} value={searchYear} selectClass={undefined} /> + <SelectBox className="col-span-3" label={labelArray[6] as string} selectArray={selectOperationData} onSelectValue={(val) => setOperationValue(val.toString())} value={operationValue} selectClass={undefined} /> + </>} + rightChildren={undefined} + selectSidoData={selectSidoData} + selectSigunData={selectSigunData} + selectRegionData={selectRegionData} + selectUnitWaterShedData={selectUnitWaterShedData} + selectMiddleRegionData={selectMiddleRegionData} + selectEnvPartData={selectEnvPartData} + btnClass={btnClass} + queryClick={chkVal} + excelClick={excelDownload} + initClick={resetVal} + ref={ref} isAdmin={false}/> + <ListDiv> + <div className="h-full w-full overflow-x-scroll"> + <div className={widthSize}> + <UiTable cellClick={ undefined } headName={"계획하수량 산정지표 현황"} tableData={mainList} headlist={upheadList} pageSize={10} total={total} resetPageNo={resetPage} onPageChange={handlePageChange} /> + </div> + <div className="mt-14"> + <span className="text-2xl font-bold text-gray-500">☞ 여유용량 = 하수처리시설 시설용량 – 건기시 유입유량(평균)​</span><br/> + <span className="text-2xl font-bold text-gray-500">☞ 변동부하율 = 건기시 일최대 유량/일평균​유량(기준 최근 3개년)​</span><br/> + <span className="text-2xl font-bold text-gray-500">☞ 분류식화율 = 분류식지역 면적(㎢) / 처리​구역 면적합계(㎢) ×100</span><br/> + </div> + </div> + </ListDiv> + </MainDiv> + ); +} \ No newline at end of file diff --git a/apps/fo/app/pages/processArea/HeadMakeColSpan.tsx b/apps/fo/app/pages/processArea/HeadMakeColSpan.tsx new file mode 100644 index 000000000..ffd556b2e --- /dev/null +++ b/apps/fo/app/pages/processArea/HeadMakeColSpan.tsx @@ -0,0 +1,87 @@ +import { TableHead, TableRow } from "@common/components/ui"; +import type { TableUpperProps } from "../../store/dashboard"; + +const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProps[], layoutNum: number }) => { + return ( + layoutNum === 0 ? ( + <> + <TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) => + (head.id === 'trtFrfNm' ? <TableHead className="w-1/12" rowSpan={3} key={head.id}>{head.title}</TableHead> : + head.id === 'laeNm' ? <TableHead className="w-1/12" rowSpan={3} key={head.id}>{head.title}</TableHead> : + head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? <TableHead rowSpan={3} className="w-40" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : + head.id === 'trstOprEtpNm' ? <TableHead className="w-1/15" rowSpan={3} key={head.id}>{head.title}</TableHead> : + head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={2}>{'행정구역'}</TableHead> : + head.upName === 'upChangeRe3' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={6}>{'증설계획'}</TableHead> : + head.upName === 'upChangeRe4' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={19}>{'시설계획'}</TableHead> : + !head.upName ? <TableHead rowSpan={3} className="w-1/20 bg-gray-20" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))} + </TableRow> + <TableRow className="bg-gray-50"> + {upHeadList.map((head: TableUpperProps) => ( + head.upName === 'upChangeRe4' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'증설계획(m³/일)'}</TableHead> : + head.upName === 'upChangeRe5' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'처리인구(인)'}</TableHead> : + head.upName === 'upChangeRe6' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={7}>{'처리구역(k㎡)'}</TableHead> : + null + ))} + </TableRow> + <TableRow className="bg-gray-50"> + {upHeadList.map((head: TableUpperProps) => { + if (head.upName === 'upChangeRe' || head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4' || head.upName === 'upChangeRe5' || head.upName === 'upChangeRe6') { + return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>; + } + })} + </TableRow> + </> + ) : layoutNum === 1 ? ( + <> + <TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) => + (head.id === 'trtFrfNm' ? <TableHead className="w-1/12" rowSpan={3} key={head.id}>{head.title}</TableHead> : + head.id === 'laeNm' ? <TableHead className="w-1/12" rowSpan={3} key={head.id}>{head.title}</TableHead> : + head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? <TableHead rowSpan={3} className="w-40" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : + head.id === 'trstOprEtpNm' ? <TableHead className="w-1/15" rowSpan={3} key={head.id}>{head.title}</TableHead> : + head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={3}>{'유역'}</TableHead> : + head.upName === 'upChangeRe3' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={6}>{'증설계획'}</TableHead> : + !head.upName ? <TableHead rowSpan={3} className="w-1/20 bg-gray-20" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))} + </TableRow> + <TableRow className="bg-gray-50"> + {upHeadList.map((head: TableUpperProps) => ( + head.upName === 'upChangeRe4' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'증설계획(m³/일)'}</TableHead> : + head.upName === 'upChangeRe5' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'처리인구(인)'}</TableHead> : + head.upName === 'upChangeRe6' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={7}>{'처리구역(k㎡)'}</TableHead> : + null + ))} + </TableRow> + <TableRow className="bg-gray-50"> + {upHeadList.map((head: TableUpperProps) => { + if (head.upName === 'upChangeRe' || head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4' || head.upName === 'upChangeRe5' || head.upName === 'upChangeRe6') { + return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>; + } + })} + </TableRow> + </> + ) : ( + <> + <TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) => + (head.id === 'trtFrfNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> : + head.id === 'laeNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> : + head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? <TableHead rowSpan={2} className="w-40" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : + head.id === 'trstOprEtpNm' ? <TableHead className="w-1/15" rowSpan={2} key={head.id}>{head.title}</TableHead> : + head.upName === 'upChangeRe1' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={3}>{'권역'}</TableHead> : + head.upName === 'upChangeRe2' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={2}>{'환경청'}</TableHead> : + head.upName === 'upChangeRe3' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'증설계획(m³/일)'}</TableHead> : + head.upName === 'upChangeRe5' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'처리인구(인)'}</TableHead> : + head.upName === 'upChangeRe6' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={7}>{'처리구역(k㎡)'}</TableHead> : + !head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-gray-20" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))} + </TableRow> + <TableRow className="bg-gray-50"> + {upHeadList.map((head: TableUpperProps) => { + if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2' ||head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4' || head.upName === 'upChangeRe5' || head.upName === 'upChangeRe6') { + return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>; + } + })} + </TableRow> + </> + ) + ); +} + +export { HeadMakeColSpan }; \ No newline at end of file diff --git a/apps/fo/app/pages/processArea/modal/page.tsx b/apps/fo/app/pages/processArea/modal/page.tsx new file mode 100644 index 000000000..06e26bac6 --- /dev/null +++ b/apps/fo/app/pages/processArea/modal/page.tsx @@ -0,0 +1,55 @@ +"use client" +import { useFoStore } from "@/app/store"; +import { UiTable } from "@common/business_components/ui"; +import ListDiv from "@common/business_components/ui/list_div"; +import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components/ui"; +import { useEffect } from "react"; + +export default function ProcessFacilityInfoModal(props: { fcltyName: string; facilityCd : string;}) { + // 그리드 관련 + const {ProcessAreaDetail, initModalData} = useFoStore((state) => state); + + useEffect(() => { + initModalData(props.facilityCd); + }, [initModalData, props.facilityCd]); + + const { + upHeadListFcltyImproveInfo, + gridListFcltyImproveInfo, + upHeadListProcessPopulation, + gridListProcessPopulation, + upHeadListProcessAreaPlan, + gridListProcessAreaPlan, + } = ProcessAreaDetail + + return ( + <> + <ListDiv> + <div className="card !m-[1.2rem] max-h-[40rem] overflow-auto !p-0r"> + <Table> + <TableHeader> + <TableRow> + <TableHead className="w-1/6">{'시설명'}</TableHead> + <TableCell className="w-1/3">{props.fcltyName}</TableCell> + <TableHead className="w-1/6">{'시설코드'}</TableHead> + <TableCell className="w-1/3">{props.facilityCd}</TableCell> + </TableRow> + </TableHeader> + </Table> + </div> + </ListDiv> + <ListDiv> + <UiTable headName={"시설증설 계획"} tableData={gridListFcltyImproveInfo} headlist={upHeadListFcltyImproveInfo} pageSize={0} total={0} cellClick={undefined} onPageChange={()=>{}}> + </UiTable> + </ListDiv> + <ListDiv> + <UiTable headName={"처리인구 계획"} tableData={gridListProcessPopulation} headlist={upHeadListProcessPopulation} pageSize={0} total={0} cellClick={undefined} onPageChange={()=>{}}> + </UiTable> + </ListDiv> + <ListDiv> + <UiTable headName={"처리구역 계획"} tableData={gridListProcessAreaPlan} headlist={upHeadListProcessAreaPlan} pageSize={0} total={0} cellClick={undefined} onPageChange={()=>{}}> + </UiTable> + </ListDiv> + </> + ); +} \ No newline at end of file diff --git a/apps/fo/app/pages/processArea/page.tsx b/apps/fo/app/pages/processArea/page.tsx new file mode 100644 index 000000000..92369cb45 --- /dev/null +++ b/apps/fo/app/pages/processArea/page.tsx @@ -0,0 +1,172 @@ +"use client" +import { useFoStore } from "@/app/store"; +import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; +import ListDiv from "@common/business_components/ui/list_div"; +import { useCallback, useEffect, useRef, useState } from "react"; +import ProcessFacilityInfoModal from "./modal/page"; + +export default function ProcessArea() { + const {Common, + ProcessArea, + ProcessAreaSearch, + initializeCommonData, + searchProcessAreaList + } = useFoStore((state) => state) || { + Common: { isInit: false, selectSidoData: [], selectSigunData: [], selectRegionData: [], selectUnitWaterShedData: [], selectMiddleRegionData: [], selectEnvPartData: [], selectSearchYear: [{ val: "0" }], btnClass: "" }, + ProcessArea: { processAreaList: [], processAreaCount: 0, excelList: [], upHeadList1: [] }, + ProcessAreaSearch: { processFacilityLabelArray: [] }, + initializeCommonData: () => {}, + searchProcessAreaList: () => {} + }; + + const { + isInit, + selectSidoData, + selectSigunData, + selectRegionData, + selectUnitWaterShedData, + selectMiddleRegionData, + selectEnvPartData, + selectSearchYear, + btnClass, + } = Common; + + const { + processFacilityLabelArray, + } = ProcessAreaSearch; + + const { + upHeadList1, + processAreaList, + processAreaCount, + excelList + } = ProcessArea; + + const mainList: Record<string, unknown>[] = processAreaList || []; + // 그리드 관련 + const searchInputRef = useRef<HTMLDivElement & HTMLInputElement>(null); + + // 모달 상태 추가 + const [selectedRow, setSelectedRow] = useState<number | null>(null); + const [fcltyNm, setFcltyNm] = useState<string>(""); + const [fcltyCd, setFcltyCd] = useState<string>(""); + + const [resetPage, setResetPage] = useState<boolean>(false); + const [searchYearValue, setSearchYearValue] = useState<string>(selectSearchYear[0]?.val || "0"); + + const ref = useRef<ReturnType>({ + partValue: '', + sidoValue: '', + sigunValue: '', + regionValue: '', + unitWaterShedValue: '', + middleRegionValue: '', + envPartValue: '', + sidoText: '', + sigunText: '' + }); + + const searchProcessArea = useCallback(({ sidoText, sigunText}: ReturnType, pageNo: number, pageCnt:number=20) => { + searchProcessAreaList(pageNo, pageCnt, sidoText, sigunText, searchYearValue); + }, [searchProcessAreaList, searchYearValue]); + + // 조회 + const chkVal = useCallback(() => { + setResetPage(true); + }, []); + + // 초기화 + const resetVal = () => { + if (searchInputRef.current) { + const input = searchInputRef.current.querySelector('input'); + if (input) { + input.value = ""; + } + } + setSearchYearValue(selectSearchYear[0]?.val || "0"); + searchProcessAreaList(1, 10, '', '', selectSearchYear[0]?.val); + } + + // 셀 클릭 시 모달 표시 + const cellClick = (index: number) => { + if (processAreaList[index]) { + setFcltyNm(processAreaList[index].swtrmtFcltNm); + setFcltyCd(processAreaList[index].swtrmtFcltCd); + setSelectedRow(index); // 클릭한 행의 index 저장 + } + }; + + // 모달 닫기 + const closeModal = () => { + setSelectedRow(null); + }; + + useEffect(() => { + if (!isInit) { + initializeCommonData(); + searchProcessAreaList(1, 10, '', '', selectSearchYear[0]?.val); + } + }, [isInit, initializeCommonData, searchProcessAreaList, selectSearchYear]); + + useEffect(() => { + if (excelList && excelList.length > 0) { + downloadExcel({ + data: excelList as unknown as JSON[], + fileName: '처리구역정보', + sheetNameArray: ['처리구역정보'], + headerRange: 'A1:W1' + }); + } + }, [excelList]); + + const excelDownload = useCallback(() => { + searchProcessArea(ref.current, 1, 10000); + }, [searchProcessArea]); + + const handlePageChange = useCallback((pageNo: number) => { + setResetPage(false); + searchProcessArea(ref.current, pageNo, 10); + }, [searchProcessArea]); + + const total = processAreaCount; + return ( + <MainDiv> + <Title title="처리구역정보" /> + <ContentsDiv> + <SearchBaseForm + leftChildren={<> + <SelectBox className="col-span-3" label={processFacilityLabelArray[6] as string} selectArray={selectSearchYear} onSelectValue={(val) => setSearchYearValue(val.toString())} value={searchYearValue} selectClass={undefined}/> + </>} + rightChildren={undefined} + selectSidoData={selectSidoData} + selectSigunData={selectSigunData} + selectRegionData={selectRegionData} + selectUnitWaterShedData={selectUnitWaterShedData} + selectMiddleRegionData={selectMiddleRegionData} + selectEnvPartData={selectEnvPartData} + btnClass={btnClass} + queryClick={chkVal} + excelClick={excelDownload} + initClick={resetVal} + ref={ref} + isAdmin={true} + /> + <ListDiv> + <div className="h-full w-full overflow-x-scroll"> + <div className="w-[180%]"> + <UiTable cellClick={(index: number | undefined) => index !== undefined && cellClick(index)} headName={"처리구역정보"} tableData={mainList} headlist={upHeadList1} pageSize={10} total={total} resetPageNo={resetPage} onPageChange={handlePageChange} /> + </div> + </div> + </ListDiv> + </ContentsDiv> + {selectedRow !== null && ( + <UiModal + body={<ProcessFacilityInfoModal fcltyName={fcltyNm} facilityCd={fcltyCd}></ProcessFacilityInfoModal>} + bodyClass="sm:max-w-[90vw] w-[1200px] h-[900px]" + open={selectedRow !== null} + onOpenChange={(open: boolean) => !open && closeModal()} + /> + )} + </MainDiv> + ); +} \ No newline at end of file diff --git a/apps/fo/app/pages/processFacility/modal/page.tsx b/apps/fo/app/pages/processFacility/modal/page.tsx new file mode 100644 index 000000000..3afed1166 --- /dev/null +++ b/apps/fo/app/pages/processFacility/modal/page.tsx @@ -0,0 +1,211 @@ +"use client" +import { useFoStore } from "@/app/store"; +import type { TableUpperProps } from "@/app/store/dashboard"; +import { UITab, UiTable } from "@common/business_components/ui"; +import ListDiv from "@common/business_components/ui/list_div"; +import { + TabsContent +} from "@common/components"; +import { Table, TableCell, TableHead, TableRow } from "@common/components/ui"; + +type ModalType = { + fcltyName: string; + facilityCd : string; + sido : string; + gugun : string; + daddr : string; + fctCpc : number | ''; + trtFrfNm : string; + rnDt : string; + cmplDt : string; + trstOprEtpNm : string; + bscFctClsNm : string; + mnstr : string; + efflWtrrgn : string; + lgbsn : string; + efflWqltStndrdNm : string; + rgnNm : string; + rvbsn : string; +} + +// 시설계획 table header 변경 +const headMakeList = (upHeadList: TableUpperProps[], gridNum:number) => { + if(gridNum===1){ + 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={6} style={{ whiteSpace: 'pre-line' }}>{'증설계획(m³/일)'}</TableHead>:null))} + </TableRow> + <TableRow> + {upHeadList.map((head: TableUpperProps) => + (head.upName === 'upChangeRe'?<TableHead className="w-150" key={head.id}>{head.title}</TableHead>:null))} + </TableRow> + </> ) + } else if(gridNum===2){ + return ( <> + <TableRow>{ + upHeadList.map((head: TableUpperProps) => + (!head.upSequnce?<TableHead rowSpan={2}key={head.id}>{head.title}</TableHead>: + head.upName === 'upChangeRe1'&& head.upSequnce=== 1?<TableHead key={head.id} rowSpan={1} colSpan={6} style={{ whiteSpace: 'pre-line' }}>{'계획유입수질(mg/L)'}</TableHead>: + head.upName === 'upChangeRe2'&& head.upSequnce=== 1?<TableHead key={head.id} rowSpan={1} colSpan={6} style={{ whiteSpace: 'pre-line' }}>{'설계유입수질(mg/L)'}</TableHead>:null))} + </TableRow> + <TableRow> + {upHeadList.map((head: TableUpperProps) => + (head.upName === 'upChangeRe1'||head.upName === 'upChangeRe2'?<TableHead style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>:null))} + </TableRow> + </> ) + } else if(gridNum===3){ + return ( <> + <TableRow>{ + upHeadList.map((head: TableUpperProps) => + (!head.upSequnce?<TableHead key={head.id}>{head.title}</TableHead>: + head.upName === 'upChangeRe1'&& head.upSequnce=== 1?<TableHead key={head.id} colSpan={2} style={{ whiteSpace: 'pre-line' }}>{'사업기간'}</TableHead>:null))} + </TableRow> + </> ) + } else if(gridNum===4){ + return ( <> + <TableRow>{ + upHeadList.map((head: TableUpperProps) => + (!head.upSequnce?<TableHead rowSpan={2}key={head.id}>{head.title}</TableHead>: + head.upName === 'upChangeRe1'&& head.upSequnce=== 1?<TableHead key={head.id} rowSpan={1} colSpan={6} style={{ whiteSpace: 'pre-line' }}>{'계획유입수질(mg/L)'}</TableHead>: + head.upName === 'upChangeRe2'&& head.upSequnce=== 1?<TableHead key={head.id} rowSpan={1} colSpan={6} style={{ whiteSpace: 'pre-line' }}>{'설계유입수질(mg/L)'}</TableHead>: + head.upName === 'upChangeRe3'&& head.upSequnce=== 1?<TableHead key={head.id} rowSpan={1} colSpan={6} style={{ whiteSpace: 'pre-line' }}>{'계획방류수질(mg/L)'}</TableHead>:null))} + </TableRow> + <TableRow> + {upHeadList.map((head: TableUpperProps) => + (head.upName === 'upChangeRe1'||head.upName === 'upChangeRe2'||head.upName === 'upChangeRe3'?<TableHead style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>:null))} + </TableRow> + </> ) + } +} + +export default function ProcessFacilityInfoModal(props: ModalType) { + // 그리드 관련 + const {ProcessFacilityDetail} = useFoStore((state) => state); + + const { + upHeadListPlanInfo, + gridListPlanInfo, + upHeadListFcltyAreaInfo, + gridListFcltyAreaInfo, + + upHeadListFcltyHistInfo, + gridListFcltyHistInfo, + upHeadListFcltyWaterQltyInfo, + gridListFcltyWaterQltyInfo, + + upHeadListLegalWaterQlty, + gridListLegalWaterQlty, + } = ProcessFacilityDetail; + + gridListFcltyAreaInfo.map((item) => { + item.efflWtrrgn = props.efflWtrrgn; + item.lgbsn = props.lgbsn; + item.efflWqltStndrdNm = props.efflWqltStndrdNm; + item.rgnNm = props.rgnNm; + item.rvbsn = props.rvbsn; + }) + + const tabList = [ + { value: 'common_info', label: '기본정보' }, + { value: 'history_info', label: '이력정보' }, + ]; + const fctCpc = props.fctCpc ? props.fctCpc.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : ''; + const upHeadListPlanInfoChange = headMakeList(upHeadListPlanInfo, 2); + const upHeadListFcltyHistInfoChange = headMakeList(upHeadListFcltyHistInfo, 3); + const upHeadListFcltyWaterQltyInfoChange = headMakeList(upHeadListFcltyWaterQltyInfo, 4); + return ( + <> + <ListDiv className="!h-[80px]"> + <div className="card !m-[1.2rem] max-h-[40rem] overflow-auto !p-0r"> + <Table> + <thead> + <TableRow className=""> + <TableHead className="w-1/6">{'시설명'}</TableHead> + <TableCell className="w-1/3">{props.fcltyName}</TableCell> + <TableHead className="w-1/6">{'시설코드'}</TableHead> + <TableCell className="w-1/3">{props.facilityCd}</TableCell> + </TableRow> + </thead> + </Table> + </div> + </ListDiv> + <div className="overflow-y-auto"> + <div className="!h-[600px]"> + <ListDiv className=""> + <UITab tabList={tabList} defaultValue="common_info"> + <TabsContent value="common_info"> + <div className="card-header"> + <h3 className="text-2xl font-medium text-label">시설 기본정보</h3> + </div> + <div className="card !m-[1.2rem] max-h-[40rem] overflow-auto !p-0r"> + <Table> + <thead> + <TableRow className=""> + <TableHead className="w-1/6">{'시설명'}</TableHead> + <TableCell className="w-1/6">{props.fcltyName}</TableCell> + <TableHead className="w-1/6">{'시설구분'}</TableHead> + <TableCell className="w-1/6">{props.bscFctClsNm}</TableCell> + <TableHead className="w-1/6">{'시설용량(m³/일)'}</TableHead> + <TableCell className="w-1/6">{fctCpc}</TableCell> + </TableRow> + </thead> + <tbody> + <TableRow className=""> + <TableHead className="w-1/6">{'시도'}</TableHead> + <TableCell className="w-1/6">{props.sido}</TableCell> + <TableHead className="w-1/6">{'시군구'}</TableHead> + <TableCell className="w-1/6">{props.gugun}</TableCell> + <TableHead className="w-1/6">{'가동개시일'}</TableHead> + <TableCell className="w-1/6">{props.rnDt}</TableCell> + </TableRow> + <TableRow className=""> + <TableHead className="w-1/6">{'주소'}</TableHead> + <TableCell className="w-1/6" colSpan={3}>{props.daddr}</TableCell> + <TableHead className="w-1/6">{'준공일'}</TableHead> + <TableCell className="w-1/6">{props.cmplDt}</TableCell> + </TableRow> + <TableRow className=""> + <TableHead className="w-1/6">{'공법'}</TableHead> + <TableCell className="w-1/6" colSpan={3}>{props.trtFrfNm}</TableCell> + <TableHead className="w-1/6">{'관리대행업자'}</TableHead> + <TableCell className="w-1/6">{props.trstOprEtpNm}</TableCell> + </TableRow> + </tbody> + </Table> + </div> + <UiTable headName={"시설 계획정보"} tableData={gridListPlanInfo} headlist={upHeadListPlanInfo} pageSize={0} total={0} cellClick={undefined} onPageChange={()=>{}}> + {upHeadListPlanInfoChange} + </UiTable> + <UiTable headName={"시설 법적방류수질"} tableData={gridListLegalWaterQlty} headlist={upHeadListLegalWaterQlty} pageSize={0} total={0} cellClick={undefined} onPageChange={()=>{}} /> + <UiTable headName={"시설 수역 및 구역정보"} tableData={gridListFcltyAreaInfo} headlist={upHeadListFcltyAreaInfo} pageSize={0} total={0} cellClick={undefined} onPageChange={()=>{}} /> + </TabsContent> + + <TabsContent value="history_info"> + <ListDiv> + <div className="h-[270px] w-full overflow-y-auto overflow-x-hidden"> + <div className="h-[130%]"> + <UiTable headName={"시설 이력관리"} tableData={gridListFcltyHistInfo} headlist={upHeadListFcltyHistInfo} pageSize={0} total={0} cellClick={undefined} onPageChange={()=>{}}> + {upHeadListFcltyHistInfoChange} + </UiTable> + </div> + </div> + </ListDiv> + <ListDiv> + <div className="h-[270px] w-full overflow-y-auto overflow-x-hidden"> + <div className="h-[130%]"> + <UiTable headName={"시설 수질정보"} tableData={gridListFcltyWaterQltyInfo} headlist={upHeadListFcltyWaterQltyInfo} pageSize={0} total={0} cellClick={undefined} onPageChange={()=>{}}> + {upHeadListFcltyWaterQltyInfoChange} + </UiTable> + </div> + </div> + </ListDiv> + </TabsContent> + </UITab> + </ListDiv> + </div> + </div> + </> + ); +} \ No newline at end of file diff --git a/apps/fo/app/pages/processFacility/page.tsx b/apps/fo/app/pages/processFacility/page.tsx new file mode 100644 index 000000000..ed58dea2b --- /dev/null +++ b/apps/fo/app/pages/processFacility/page.tsx @@ -0,0 +1,294 @@ +"use client" +import { HeadMakeColSpan } from "@/app/components/common/HeadMakeColSpan"; +import { useFoStore } from "@/app/store"; +import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; +import ListDiv from "@common/business_components/ui/list_div"; +import { useCallback, useEffect, useRef, useState } from "react"; +import ProcessFacilityInfoModal from "./modal/page"; + +export default function ProcessFacilitySearch() { + const {Common + ,ProcessFacility + ,ProcessFacilityList + ,initializeCommonData + //시설데이터 조회 + ,searchProcessFacility + //시설데이터 이력정보 조회 + ,searchProcessFacilityHist + //시설데이터 수질정보 조회 + ,searchProcessFacilityWaterQlty + } = useFoStore((state) => state) || { ProcessFacilityList: { processFacilityList: [], processFacilityCount: 0 }}; + + const { + isInit, + selectUpdownData, + selectOperationData, + selectFacilityPartData, + selectSidoData, + selectSigunData, + selectRegionData, + selectUnitWaterShedData, + selectMiddleRegionData, + selectEnvPartData, + btnClass, + } = Common; + + const ref = useRef<ReturnType>({ + partValue: '', + sidoValue: '', + sigunValue: '', + regionValue: '', + unitWaterShedValue: '', + middleRegionValue: '', + envPartValue: '', + sidoText: '', + sigunText: '' + }); + + useEffect(() => { + if(!isInit) { + initializeCommonData(); + searchProcessFacility(1, 10, 'A', '0', '0', '0', '0'); + } + }, [isInit + ,initializeCommonData + ,searchProcessFacility + ]); + + const { + processFacilityLabelArray, + } = ProcessFacility; + + const { + upHeadList1, + upHeadList2, + upHeadList3, + excelList + } = ProcessFacilityList; + + const processFacilityList = ProcessFacilityList?.processFacilityList || []; + + // 그리드 관련 + const inputRef = useRef<HTMLInputElement>(null); + const searchInputRef = useRef<HTMLDivElement & HTMLInputElement>(null); + + const [fcltCpct, setFcltCpct] = useState<string>("0"); + const [fcltyName, setFcltyName] = useState<string>(""); + const [operationValue, setOperationValue] = useState<string>("0"); + const [facilityPartValue, setFacilityPartValue] = useState<string>("0"); + + const [resetPage, setResetPage] = useState<boolean>(false); + + const [partValue, setPartValue] = useState<string>("A"); + const [widthSize, setWidthSize] = useState<string>("w-[120%]"); + const [upHeadList, setUpheadList] = useState(upHeadList1); + + /** + * 모달 상태 변수 + * selectedRow : 클릭한 행의 index + * modalFcltyNm : 시설명 + * modalFcltyCd : 시설코드 + * modalSido : 시도 + * modalGugun : 시군구 + * modalLaeNm : 주소 + * modalFctCpc : 시설용량 + * modalTrtFrfNm : 공법 + * modalRnDt : 가동개시일 + * modalCmplDt : 준공일 + * modalTrstOprEtpNm : 관리대행업자 + * modalBscFctClsNm : 시설구분 + */ + const [selectedRow, setSelectedRow] = useState<number | null>(null); + const [modalFcltyNm, setModalFcltyNm] = useState<string>(""); + const [modalFcltyCd, setModalFcltyCd] = useState<string>(""); + const [modalSido, setModalSido] = useState<string>(""); + const [modalGugun, setModalGugun] = useState<string>(""); + const [modalAddr, setModalAddr] = useState<string>(""); + const [modalFctCpc, setModalFctCpc] = useState<number | ''>(""); + const [modalTrtFrfNm, setModalTrtFrfNm] = useState<string>(""); + const [modalRnDt, setModalRnDt] = useState<string>(""); + const [modalCmplDt, setModalCmplDt] = useState<string>(""); + const [modalTrstOprEtpNm, setModalTrstOprEtpNm] = useState<string>(""); + const [modalBscFctClsNm, setModalBscFctClsNm] = useState<string>(""); + const [modalMnstr, setModalMnstr] = useState<string>(""); + const [modalEfflWtrrgn, setModalEfflWtrrgn] = useState<string>(""); + const [modalLgbsn, setModalLgbsn] = useState<string>(""); + const [modalEfflWqltStndrdNm, setModalEfflWqltStndrdNm] = useState<string>(""); + const [modalRgnNm, setModalRgnNm] = useState<string>(""); + const [modalRvbsn, setModalRvbsn] = useState<string>(""); + const [isSearch, setIsSearch] = useState<boolean>(false); + + // 표 레이아웃 상태 + const chkVal = useCallback((result:ReturnType) => { + setPartValue(result.partValue); + setResetPage(true); + setIsSearch(true); + }, []); + + useEffect(() => { + switch (partValue) { + case 'A': + setWidthSize("w-[130%]"); + setUpheadList(upHeadList1); + break; + case 'R': + setWidthSize("w-[140%]"); + setUpheadList(upHeadList2); + break; + case 'G': + setWidthSize("w-[150%]"); + setUpheadList(upHeadList3); + break; + } + }, [partValue, upHeadList1, upHeadList2, upHeadList3]); + + useEffect(() => { + if (excelList && excelList.length > 0) { + downloadExcel({ data: excelList as unknown as JSON[], fileName: '처리시설 시설정보', sheetNameArray: ['처리시설 시설정보'], headerRange: 'A1:P1' }); + } + }, [excelList]); + + const searchFcltyList = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=10) => { + let nameCd, partStr + switch (partValue) { + case 'R': + if (regionValue !== '0') { + if(unitWaterShedValue==='0') nameCd = regionValue; + else if (middleRegionValue==='0') nameCd = unitWaterShedValue; + else nameCd = middleRegionValue; + } + partStr = partValue + break; + default: + if(sidoValue !== '0') nameCd = sidoValue; + if(sigunValue !== '0') nameCd = sigunValue; + partStr = envPartValue !== '0' ? envPartValue:partValue; + break; + } + searchProcessFacility(pageNo, pageCnt, partStr, nameCd || '0', fcltCpct, operationValue, facilityPartValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined); + if(isSearch) { + setIsSearch(false); + } + }, [facilityPartValue, fcltCpct, fcltyName, isSearch, operationValue, searchProcessFacility]) + + const changePage = useCallback((page: number) => { + searchFcltyList(ref.current, page); + },[searchFcltyList]) + + const resetVal = () => { + // SelectBox 초기화 - 각 selectArray의 첫 번째 옵션 값 사용 + searchProcessFacility(1, 10, 'A', '0', '0', '0', '0', ''); + setFcltCpct("0"); + setOperationValue("0"); + setFacilityPartValue("0"); + setPartValue('A'); + setUpheadList(upHeadList1); + // SearchInputSelect 초기화 + setFcltyName(""); + + // ref를 사용하여 input 값 초기화 + if (inputRef.current) { + inputRef.current.value = ""; + } + + if (searchInputRef.current) { + const input = searchInputRef.current.querySelector('input'); + if (input) { + input.value = ""; + } + } + } + + // 셀 클릭 시 모달 표시 + const cellClick = (index: number) => { + const facility = processFacilityList[index]; + if (facility) { + setModalFcltyNm(facility.bscFctNm); + setModalFcltyCd(facility.bscFctCd); + setModalSido(facility.sido); + setModalGugun(facility.gugun); + setModalAddr(facility.daddr); + setModalFctCpc(Number(facility.fctCpc)); + setModalTrtFrfNm(facility.trtFrfNm); + setModalRnDt(facility.rnDt); + setModalCmplDt(facility.cmplDt); + setModalTrstOprEtpNm(facility.trstOprEtpNm); + setModalBscFctClsNm(facility.bscFctClsNm); + setModalMnstr(facility.mnstr); + setModalEfflWtrrgn(facility.efflWtrrgn); + setModalLgbsn(facility.lgbsn); + setModalEfflWqltStndrdNm(facility.efflWqltStndrdNm); + setModalRgnNm(facility.rgnNm); + setModalRvbsn(facility.rvbsn); + + searchProcessFacilityHist(facility.bscFctCd); + searchProcessFacilityWaterQlty(facility.bscFctCd); + setSelectedRow(index); // 클릭한 행의 index 저장 + } + } + + const excelDownload = useCallback(() => { + searchFcltyList(ref.current, 1, 10000); + }, [searchFcltyList]); + + // 모달 닫기 + const closeModal = () => { + setSelectedRow(null); + } + + const total = ProcessFacilityList?.processFacilityCount || 1; + + return ( + <MainDiv> + <Title title="처리시설 시설정보" /> + <ContentsDiv> + <SearchBaseForm + leftChildren={<> + <SelectBox className="col-span-3" label={processFacilityLabelArray[3] as string} selectArray={selectUpdownData} onSelectValue={val => setFcltCpct(val.toString())} value={fcltCpct} selectClass={undefined} /> + <SearchInput className="col-span-3" label={processFacilityLabelArray[4] as string} textValue={fcltyName} setText={setFcltyName} /> + <SelectBox className="col-span-3" label={processFacilityLabelArray[5] as string} selectArray={selectOperationData} onSelectValue={val => setOperationValue(val.toString())} value={operationValue} selectClass={undefined} /> + <SelectBox className="col-span-3" label={processFacilityLabelArray[6] as string} selectArray={selectFacilityPartData} onSelectValue={val => setFacilityPartValue(val.toString())} value={facilityPartValue} selectClass={undefined} /> + </>} + rightChildren={undefined} + selectSidoData={selectSidoData} + selectSigunData={selectSigunData} + selectRegionData={selectRegionData} + selectUnitWaterShedData={selectUnitWaterShedData} + selectMiddleRegionData={selectMiddleRegionData} + selectEnvPartData={selectEnvPartData} + btnClass={btnClass} + queryClick={chkVal} + excelClick={excelDownload} + initClick={resetVal} + ref={ref} isAdmin={false} /> + <ListDiv> + <div className="h-full w-full overflow-x-scroll"> + <div className={widthSize}> + <UiTable + cellClick={(index: number | undefined) => index !== undefined && cellClick(index)} + headName={"처리시설 시설정보"} + tableData={processFacilityList} + headlist={upHeadList} + pageSize={10} + total={total} + resetPageNo={resetPage} + onPageChange={changePage} + > + <HeadMakeColSpan upHeadList={upHeadList} layoutNum={partValue}/> + </UiTable> + </div> + </div> + </ListDiv> + <span className="text-2xl text-gray-500">※계획유입수질 및 설계유입수질은 해당 BOD를 나타냄</span> + </ContentsDiv> + {selectedRow !== null && ( + <UiModal + body={<ProcessFacilityInfoModal fcltyName={modalFcltyNm} facilityCd={modalFcltyCd} sido={modalSido} gugun={modalGugun} daddr={modalAddr} fctCpc={modalFctCpc} trtFrfNm={modalTrtFrfNm} rnDt={modalRnDt} cmplDt={modalCmplDt} trstOprEtpNm={modalTrstOprEtpNm} bscFctClsNm={modalBscFctClsNm} mnstr={modalMnstr} efflWtrrgn={modalEfflWtrrgn} lgbsn={modalLgbsn} efflWqltStndrdNm={modalEfflWqltStndrdNm} rgnNm={modalRgnNm} rvbsn={modalRvbsn}></ProcessFacilityInfoModal>} + bodyClass="sm:max-w-[70vw] sm:max-h-[95vh]" + open={selectedRow !== null} + onOpenChange={(open) => !open && closeModal()} + /> + )} + </MainDiv> + ); +} \ No newline at end of file diff --git a/apps/fo/app/pages/rdi/page.tsx b/apps/fo/app/pages/rdi/page.tsx new file mode 100644 index 000000000..d28c7241d --- /dev/null +++ b/apps/fo/app/pages/rdi/page.tsx @@ -0,0 +1,200 @@ +"use client" +import { useFoStore } from "@/app/store"; +import type { TableUpperProps } from "@/app/store/common"; +import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiTable, type ReturnType } from "@common/business_components"; +import ListDiv from "@common/business_components/ui/list_div"; +import { useCallback, useEffect, useRef, useState } from "react"; + +export default function Rdi() { + const {Common + ,initializeCommonData + ,Rdi + ,RdiSearch + ,searchRdiList + } = useFoStore((state) => state) || { processAreaList: { processAreaList: [] }}; + + const { + isInit, + selectUpdownData, + selectOperationData, + selectSidoData, + selectSigunData, + selectRegionData, + selectUnitWaterShedData, + selectMiddleRegionData, + selectEnvPartData, + selectSearchYear, + btnClass + } = Common; + + const { + labelArray, + } = RdiSearch; + + const { + upHeadList1, + upHeadList2, + upHeadList3, + rdiList, + rdiCount, + excelList, + } = Rdi; + + const ref = useRef<ReturnType>({ + partValue: '', + sidoValue: '', + sigunValue: '', + regionValue: '', + unitWaterShedValue: '', + middleRegionValue: '', + envPartValue: '', + sidoText: '', + sigunText: '' + }); + + // 그리드 관련 + const searchInputRef = useRef<HTMLDivElement & HTMLInputElement>(null); + + // 초기화 변수 + const [resetPage, setResetPage] = useState<boolean>(false); + // 표 너비 + const [widthSize, setWidthSize] = useState<string>("w-[120%]"); + // 구분별 그리드 헤더 + const [upheadList, setUpheadList] = useState<TableUpperProps[]>(upHeadList1); + // 용량별 + const [fcltCpct, setFcltCpct] = useState<string>("0"); + // 시설명 + const [fcltyName, setFcltyName] = useState<string>(""); + // 시설상태 + const [operationValue, setOperationValue] = useState<string>("0"); + // 조회기간 + const [searchYear, setSearchYear] = useState<string>(selectSearchYear[0]?.val || "0"); + + /** + * 페이지 함수 + */ + // 초기화 + const resetVal = () => { + setWidthSize("w-[120%]"); + + setUpheadList(upHeadList1) + // 상태값 설정 + setFcltCpct(selectUpdownData[0]?.val || "0"); + setOperationValue(selectOperationData[0]?.val || "0"); + setSearchYear(selectSearchYear[0]?.val || "0"); + // SearchInputSelect 초기화 + setFcltyName(""); + if (searchInputRef.current) { + const input = searchInputRef.current.querySelector('input'); + if (input) { + input.value = ""; + } + } + searchRdiList(1, 10, selectSearchYear[0]?.val || "0"); + + } + + /** + * useEffect + */ + useEffect(() => { + if (!isInit) { + initializeCommonData(); + searchRdiList(1, 10, selectSearchYear[0]?.val || "0"); + } + }, [isInit, initializeCommonData, searchRdiList, selectSearchYear]); + + useEffect(() => { + if (excelList && excelList.length > 0) { + downloadExcel({ + data: excelList as unknown as JSON[], + fileName: 'RDII추정', + sheetNameArray: ['RDII추정'], + headerRange: 'A1:O1' + }); + } + }, [excelList]); + + /** + * 콜백함수 + */ + // 조회 + const searchRdi = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => { + let nameCd, partStr + switch (partValue) { + case 'R': + if (regionValue !== '0') { + if(unitWaterShedValue==='0') nameCd = regionValue; + else if (middleRegionValue==='0') nameCd = unitWaterShedValue; + else nameCd = middleRegionValue; + } + partStr = partValue + break; + default: + if(sidoValue !== '0') nameCd = sidoValue; + if(sigunValue !== '0') nameCd = sigunValue; + partStr = envPartValue !== '0' ? envPartValue:partValue; + break; + } + if (partStr.length > 1 || fcltCpct !== '0' || operationValue!== '0' ||nameCd ||(fcltyName.trim() !== "" && fcltyName.trim() !== null)) { + searchRdiList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined); + } else { + searchRdiList(pageNo, pageCnt, searchYear); + } + }, [operationValue, fcltyName, searchRdiList, searchYear, fcltCpct]); + + const chkVal = useCallback((returntype:ReturnType) => { + setWidthSize(returntype.partValue=='A' ?"w-[120%]":"w-[130%]") + setUpheadList(returntype.partValue == 'R' ? upHeadList2 : returntype.partValue == 'G'?upHeadList3:upHeadList1); + setResetPage(true); + }, [upHeadList1, upHeadList2, upHeadList3]); + + // 엑셀다운로드 + const excelDownload = useCallback(() => { + searchRdi(ref.current, 1, 10000); + }, [searchRdi]); + + // 페이지 변경 + const handlePageChange = useCallback((page: number) => { + setResetPage(false); + searchRdi(ref.current, page, 10); + }, [searchRdi]); + + const mainList: Record<string, string | number | boolean>[] = rdiList || []; + const total = rdiCount || 0; + + return ( + <MainDiv> + <Title title="RDII추정" /> + <ContentsDiv> + <SearchBaseForm + leftChildren={<> + <SelectBox className="col-span-3" label={labelArray[3] as string} selectArray={selectUpdownData} onSelectValue={(val) => setFcltCpct(val.toString())} value={fcltCpct} selectClass={undefined} /> + <SearchInput ref={searchInputRef} className={"col-span-3"} label={labelArray[4] as string} textValue={fcltyName} setText={setFcltyName} /> + <SelectBox className="col-span-3" label={labelArray[5] as string} selectArray={selectSearchYear} onSelectValue={(val) => setSearchYear(val.toString())} value={searchYear} selectClass={undefined} /> + <SelectBox className="col-span-3" label={labelArray[6] as string} selectArray={selectOperationData} onSelectValue={(val) => setOperationValue(val.toString())} value={operationValue} selectClass={undefined} /> + </>} + rightChildren={undefined} + selectSidoData={selectSidoData} + selectSigunData={selectSigunData} + selectRegionData={selectRegionData} + selectUnitWaterShedData={selectUnitWaterShedData} + selectMiddleRegionData={selectMiddleRegionData} + selectEnvPartData={selectEnvPartData} + btnClass={btnClass} + queryClick={chkVal} + excelClick={excelDownload} + initClick={resetVal} + ref={ref} isAdmin={false} /> + <ListDiv> + <div className="h-full w-full overflow-x-scroll"> + <div className={widthSize}> + <UiTable cellClick={ undefined } headName={"RDII추정"} tableData={mainList} headlist={upheadList} pageSize={10} total={total} resetPageNo={resetPage} onPageChange={handlePageChange} /> + </div> + <span className="text-2xl text-gray-500">※주) 처리장 유입부에서 측정된 유량 및 수질조사 자료를 기준으로 산정된 결과임</span> + </div> + </ListDiv> + </ContentsDiv> + </MainDiv> + ); +} \ No newline at end of file diff --git a/apps/fo/app/pages/sewageOccur/modal/page.tsx b/apps/fo/app/pages/sewageOccur/modal/page.tsx new file mode 100644 index 000000000..23c890160 --- /dev/null +++ b/apps/fo/app/pages/sewageOccur/modal/page.tsx @@ -0,0 +1,92 @@ +"use client" +import { useFoStore } from "@/app/store"; +import type { modalTableType } from "@/app/store/sewageOccur"; +import { DateMonthPicker, UiTable } from "@common/business_components/ui"; +import ListDiv from "@common/business_components/ui/list_div"; +import { Table, TableCell, TableHead, TableRow } from "@common/components/ui"; +import { useEffect, useState } from "react"; + + +export default function SewageOccurModal(props: { fcltyName: string; facilityCd : string; searchYear : string}) { + // 그리드 관련 + const {SewageOccurDetail, searchSewageOccurDetailList} = useFoStore((state) => state); + + // searchYear 유효성 검사 및 기본값 설정 + const validSearchYear = props.searchYear && /^\d{4}$/.test(props.searchYear) ? props.searchYear : new Date().getFullYear().toString(); + + const [startYearMonth, setStartYearMonth] = useState(validSearchYear+'01'); + const [endYearMonth, setEndYearMonth] = useState(validSearchYear+'12'); + const [detailList, setDetailList] = useState<modalTableType[]>([]); + const { + upHeadListSewageOccurDetail, + gridListSewageOccurDetail + } = SewageOccurDetail; + + useEffect(() => { + searchSewageOccurDetailList(props.facilityCd, validSearchYear); + }, [searchSewageOccurDetailList, props.facilityCd, validSearchYear]); + + useEffect(() => { + setDetailList(gridListSewageOccurDetail); + }, [gridListSewageOccurDetail]); + + return ( + <> + <ListDiv className="!h-[80px] !mt-10"> + <div className="card !m-[1.2rem] max-h-[40rem] overflow-auto !p-0r"> + <Table> + <thead> + <TableRow className=""> + <TableHead className="w-1/6">{'시설명'}</TableHead> + <TableCell className="w-1/3">{props.fcltyName}</TableCell> + <TableHead className="w-1/6">{'년월'}</TableHead> + <TableCell className="w-1/3"> + <DateMonthPicker + field={{ + value: startYearMonth || (() => { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + return `${year}${month}`; + })(), + onChange: (value: string) => { + if (value && /^\d{6}$/.test(value)) { + setStartYearMonth(value); + } + } + }} + selectClass="ml-5 w-[100px]" + startYear={1900} + />~ + <DateMonthPicker + field={{ + value: endYearMonth || (() => { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + return `${year}${month}`; + })(), + onChange: (value: string) => { + if (value && /^\d{6}$/.test(value)) { + setEndYearMonth(value); + } + } + }} + selectClass="ml-5 w-[100px]" + startYear={1900} + /> + </TableCell> + </TableRow> + </thead> + </Table> + </div> + </ListDiv> + <ListDiv className="!h-[570px] overflow-y-auto"> + <div className="h-[120%]"> + <UiTable headName={"오수발생량 추정"} tableData={detailList} headlist={upHeadListSewageOccurDetail} pageSize={0} total={0} cellClick={undefined} onPageChange={() => {}} boldArray={[0, 1, 2]}> + </UiTable> + </div> + </ListDiv> + </> + ); +} \ No newline at end of file diff --git a/apps/fo/app/pages/sewageOccur/page.tsx b/apps/fo/app/pages/sewageOccur/page.tsx new file mode 100644 index 000000000..8b004e681 --- /dev/null +++ b/apps/fo/app/pages/sewageOccur/page.tsx @@ -0,0 +1,243 @@ +"use client" +import { useFoStore } from "@/app/store"; +import type { TableUpperProps } from "@/app/store/common"; +import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; +import ListDiv from "@common/business_components/ui/list_div"; +import { useCallback, useEffect, useRef, useState } from "react"; +import SewageOccurModal from "./modal/page"; +import type { SewageOccurProps } from "@/app/store/sewageOccur"; + +export default function PlanSewage() { + const {Common + ,SewageOccur + ,SewageOccurSearch + ,initializeCommonData + ,searchSewageOccurList + } = useFoStore((state) => state) || { + Common: { isInit: false, selectUpdownData: [], selectOperationData: [], selectSidoData: [], selectSigunData: [], selectRegionData: [], selectUnitWaterShedData: [], selectMiddleRegionData: [], selectEnvPartData: [], selectSearchYear: [{ val: "0" }], btnClass: "" }, + SewageOccur: { sewageOccurList: [], sewageOccurCount: 0, excelList: [], upHeadList1: [], upHeadList2: [], upHeadList3: [] }, + SewageOccurSearch: { labelArray: [] }, + initializeCommonData: () => {}, + searchSewageOccurList: () => {}, + calculateModalDataForAllFacilities: async () => [] + }; + + const { + isInit, + selectUpdownData, + selectOperationData, + selectSidoData, + selectSigunData, + selectRegionData, + selectUnitWaterShedData, + selectMiddleRegionData, + selectEnvPartData, + selectSearchYear, + btnClass + } = Common; + + const { + labelArray, + } = SewageOccurSearch; + + const { + upHeadList1, + upHeadList2, + upHeadList3, + sewageOccurList, + sewageOccurCount, + excelList + } = SewageOccur; + + const ref = useRef<ReturnType>({ + partValue: '', + sidoValue: '', + sigunValue: '', + regionValue: '', + unitWaterShedValue: '', + middleRegionValue: '', + envPartValue: '', + sidoText: '', + sigunText: '' + }); + + const sewageOccur = SewageOccur?.sewageOccurList || []; + + // 모달 상태 추가 + const [selectedRow, setSelectedRow] = useState<number | null>(null); + const [fcltyNm, setFcltyNm] = useState<string>(""); + const [fcltyCd, setFcltyCd] = useState<string>(""); + + // 그리드 관련 + const searchInputRef = useRef<HTMLDivElement & HTMLInputElement>(null); + + // 초기화 변수 + const [resetPage, setResetPage] = useState<boolean>(false); + // 표 너비 + const [widthSize, setWidthSize] = useState<string>("w-[120%]"); + // 구분별 그리드 헤더 + const [upheadList, setUpheadList] = useState<TableUpperProps[]>(upHeadList1); + // 용량별 + const [fcltCpct, setFcltCpct] = useState<string>("0"); + // 시설명 + const [fcltyName, setFcltyName] = useState<string>(""); + // 시설상태 + const [operationValue, setOperationValue] = useState<string>("0"); + // 조회기간 + // const [searchYear, setSearchYear] = useState<string>(selectSearchYear[0]?.val || "0"); + const [searchYear, setSearchYear] = useState<string>("2023"); + + /** + * 페이지 함수 + */ + // 초기화 + const resetVal = () => { + setWidthSize("w-[120%]"); + setUpheadList(upHeadList1) + // 상태값 설정 + setFcltCpct(selectUpdownData[0]?.val || "0"); + setOperationValue(selectOperationData[0]?.val || "0"); + setSearchYear(selectSearchYear[0]?.val || "0"); + // SearchInputSelect 초기화 + setFcltyName(""); + if (searchInputRef.current) { + const input = searchInputRef.current.querySelector('input'); + if (input) { + input.value = ""; + } + } + } + + // 모달 닫기 + const closeModal = () => { + setSelectedRow(null); + }; + + // 셀 클릭 시 모달 표시 + const cellClick = (index: number) => { + if (sewageOccur[index]) { + setFcltyNm(sewageOccur[index].bscFctNm); + setFcltyCd(sewageOccur[index].bscFctCd); + setSelectedRow(index); // 클릭한 행의 index 저장 + } + }; + + /** + * useEffect + */ + useEffect(() => { + if (!isInit) { + initializeCommonData(); + searchSewageOccurList(1, 10, searchYear || "0"); + } + }, [isInit, initializeCommonData, searchSewageOccurList, searchYear]); + + useEffect(() => { + if (excelList && excelList.length > 0) { + downloadExcel({ + data: excelList as unknown as JSON[], + fileName: '오수발생량 추정', + sheetNameArray: ['오수발생량 추정'], + headerRange: 'A1:O1' + }); + } + }, [excelList]); + + /** + * 콜백함수 + */ + // 조회 + const searchFacilityPrecipitation = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => { + let nameCd, partStr + switch (partValue) { + case 'R': + if (regionValue !== '0') { + if(unitWaterShedValue==='0') nameCd = regionValue; + else if (middleRegionValue==='0') nameCd = unitWaterShedValue; + else nameCd = middleRegionValue; + } + partStr = partValue + break; + default: + if(sidoValue !== '0') nameCd = sidoValue; + if(sigunValue !== '0') nameCd = sigunValue; + partStr = envPartValue !== '0' ? envPartValue:partValue; + break; + } + if (partStr.length > 1 || fcltCpct !== '0' || operationValue!== '0' ||nameCd ||(fcltyName.trim() !== "" && fcltyName.trim() !== null)) { + searchSewageOccurList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined); + } else { + searchSewageOccurList(pageNo, pageCnt, searchYear); + } + }, [fcltCpct, operationValue, fcltyName, searchSewageOccurList, searchYear]); + + const chkVal = useCallback((returntype:ReturnType) => { + setWidthSize(returntype.partValue=='A' ?"w-[120%]":"w-[130%]") + setUpheadList(returntype.partValue == 'R' ? upHeadList2 : returntype.partValue == 'G'?upHeadList3:upHeadList1); + setResetPage(true); + }, [upHeadList1, upHeadList2, upHeadList3]); + + // 엑셀다운로드 + const excelDownload = useCallback(() => { + searchFacilityPrecipitation(ref.current, 1, 10000); + }, [searchFacilityPrecipitation]); + + // 페이지 변경 + const handlePageChange = useCallback((page: number) => { + setResetPage(false); + searchFacilityPrecipitation(ref.current, page, 10); + }, [searchFacilityPrecipitation]); + + const mainList: SewageOccurProps[] = sewageOccurList || []; + const total = sewageOccurCount || 0; + + return ( + <MainDiv> + <Title title="오수발생량 추정" /> + <ContentsDiv> + <SearchBaseForm + leftChildren={<> + <SelectBox className="col-span-3" label={labelArray[3] as string} selectArray={selectUpdownData} onSelectValue={(val) => setFcltCpct(val.toString())} value={fcltCpct} selectClass={undefined} /> + <SearchInput className={"col-span-3"} label={labelArray[4] as string} textValue={fcltyName} setText={setFcltyName} /> + <SelectBox className="col-span-3" label={labelArray[5] as string} selectArray={selectSearchYear} onSelectValue={(val) => setSearchYear(val.toString())} value={searchYear} selectClass={undefined} /> + <SelectBox className="col-span-3" label={labelArray[6] as string} selectArray={selectOperationData} onSelectValue={(val) => setOperationValue(val.toString())} value={operationValue} selectClass={undefined} /> + </>} + rightChildren={undefined} + selectSidoData={selectSidoData} + selectSigunData={selectSigunData} + selectRegionData={selectRegionData} + selectUnitWaterShedData={selectUnitWaterShedData} + selectMiddleRegionData={selectMiddleRegionData} + selectEnvPartData={selectEnvPartData} + btnClass={btnClass} + queryClick={chkVal} + excelClick={excelDownload} + initClick={resetVal} + ref={ref} + isAdmin={false} + /> + <ListDiv> + <div className="h-full w-full overflow-x-scroll"> + <div className={widthSize}> + <UiTable cellClick={(index: number | undefined) => index !== undefined && cellClick(index)} headName={"오수발생량 추정"} tableData={mainList} headlist={upheadList} pageSize={10} total={total} resetPageNo={resetPage} onPageChange={handlePageChange} /> + </div> + </div> + </ListDiv> + </ContentsDiv> + {selectedRow !== null && ( + <UiModal + body={<SewageOccurModal + fcltyName={fcltyNm} + facilityCd={fcltyCd} + searchYear={searchYear} + />} + bodyClass="sm:max-w-[90vw] w-[1000px] h-[770px]" + open={selectedRow !== null} + onOpenChange={(open) => !open && closeModal()} + noHeaderYn="Y" + noFooterYn="Y" + /> + )} + </MainDiv> + ); +} \ No newline at end of file diff --git a/apps/fo/app/pages/underWaterInput/modal/page.tsx b/apps/fo/app/pages/underWaterInput/modal/page.tsx new file mode 100644 index 000000000..145612431 --- /dev/null +++ b/apps/fo/app/pages/underWaterInput/modal/page.tsx @@ -0,0 +1,320 @@ +"use client" +import { useFoStore } from "@/app/store"; +import type { UnderWaterInputModalProps } from "@/app/store/underWaterInput/types"; +import { DateMonthPicker, UITab, UiTable } from "@common/business_components/ui"; +import ListDiv from "@common/business_components/ui/list_div"; +import { Table, TableCell, TableHead, TableRow, TabsContent } from "@common/components/ui"; +import { useEffect, useState } from "react"; + +type ModalProps ={ + fcltyName: string; + searchYear : string; +} + +const tabList1 = [ + { value: 'tab_term_year', label: '연간' }, + { value: 'tab_term_summer', label: '하절기' }, + { value: 'tab_term_winter', label: '동절기' }, +]; + +export default function UnderWaterInputModal({fcltyName='', searchYear=new Date().getFullYear().toString()} : ModalProps) { + // 그리드 관련 + const {UnderWaterDetail, unserWaterModal} = useFoStore((state) => state); + + const [startYmDate, setStartYmDate] = useState<string>(searchYear+'01'); + const [endYmDate, setEndYmDate] = useState<string>(searchYear+'12'); + const [tabTerm, setTabTerm] = useState<string>('tab_term_year'); + + // 이전 값들을 저장하기 위한 상태 + const [prevStartYmDate, setPrevStartYmDate] = useState<string>(searchYear+'01'); + const [prevEndYmDate, setPrevEndYmDate] = useState<string>(searchYear+'12'); + + const { + upHeadListUnderWaterDetail, + gridListUnderWaterDetail, + } = UnderWaterDetail; + + // 필터링 된 후 최대, 최소, 평균 값 계산 + const setFilterAfterData = (filteredData: UnderWaterInputModalProps[]) => { + let maxRealSewageInput = Number(filteredData[0]?.realSewageInput ?? 0); + let maxSewageOccur = Number(filteredData[0]?.sewageOccur ?? 0); + let maxUnderWaterInput = Number(filteredData[0]?.underWaterInput ?? 0); + + let minRealSewageInput = Number(filteredData[0]?.realSewageInput ?? 0); + let minSewageOccur = Number(filteredData[0]?.sewageOccur ?? 0); + let minUnderWaterInput = Number(filteredData[0]?.underWaterInput ?? 0); + + let avgRealSewageInput = 0; + let avgSewageOccur = 0; + let avgUnderWaterInput = 0; + + filteredData.forEach((item: UnderWaterInputModalProps) => { + maxRealSewageInput = Math.max(Number(maxRealSewageInput), Number(item.realSewageInput)); + maxSewageOccur = Math.max(Number(maxSewageOccur), Number(item.sewageOccur)); + maxUnderWaterInput = Math.max(Number(maxUnderWaterInput), Number(item.underWaterInput)); + + minRealSewageInput = Math.min(Number(minRealSewageInput) || 0, Number(item.realSewageInput)); + minSewageOccur = Math.min(Number(minSewageOccur) || 0, Number(item.sewageOccur)); + minUnderWaterInput = Math.min(Number(minUnderWaterInput) || 0, Number(item.underWaterInput)); + + avgRealSewageInput += Number(item.realSewageInput ?? 0); + avgSewageOccur += Number(item.sewageOccur ?? 0); + avgUnderWaterInput += Number(item.underWaterInput ?? 0); + }) + + const newFilteredData = [ + { + msrDt: '평균', + realSewageInput: filteredData.length==0?0:Math.round(Number(avgRealSewageInput) / filteredData.length * 10) / 10, + sewageOccur: filteredData.length==0?0:Math.round(Number(avgSewageOccur) / filteredData.length * 1000) / 1000 , + underWaterInput: filteredData.length==0?0:Math.round(Number(avgUnderWaterInput) / filteredData.length * 1000) / 1000, + }, + { + msrDt : '최대', + realSewageInput : Math.round(maxRealSewageInput * 10) / 10, + sewageOccur : Math.round(maxSewageOccur * 1000) / 1000 , + underWaterInput : Math.round(maxUnderWaterInput * 1000) / 1000, + }, + { + msrDt : '최소', + realSewageInput : Math.round(minRealSewageInput * 10) / 10, + sewageOccur : Math.round(minSewageOccur * 1000) / 1000 , + underWaterInput : Math.round(minUnderWaterInput * 1000) / 1000, + }, + ...filteredData + ]; + return newFilteredData; + } + + // 날짜 필터링 관련 상태 + const [copyYearList, setCopyYearList] = useState<UnderWaterInputModalProps[]>([]); + + useEffect(() => { + const filteredYearData = setFilterAfterData(gridListUnderWaterDetail); + setCopyYearList(filteredYearData as UnderWaterInputModalProps[]); + }, [gridListUnderWaterDetail]); + + useEffect(() => { + switch (tabTerm) { + case 'tab_term_summer': + setStartYmDate(searchYear+'06'); + setEndYmDate(searchYear+'08'); + setPrevStartYmDate(searchYear+'06'); + setPrevEndYmDate(searchYear+'08'); + break; + case 'tab_term_winter': + setStartYmDate(searchYear+'01'); + setEndYmDate(searchYear+'02'); + setPrevStartYmDate(searchYear+'01'); + setPrevEndYmDate(searchYear+'02'); + break; + default: + setStartYmDate(searchYear+'01'); + setEndYmDate(searchYear+'12'); + setPrevStartYmDate(searchYear+'01'); + setPrevEndYmDate(searchYear+'12'); + } + }, [searchYear, tabTerm]); + useEffect(()=>{ + console.log(fcltyName, searchYear); + unserWaterModal(fcltyName, searchYear); + }, []) + useEffect(() => { + if(endYmDate < startYmDate) { + alert('종료일이 시작일보다 작습니다.') + // 이전 값으로 되돌리기 + setStartYmDate(prevStartYmDate); + setEndYmDate(prevEndYmDate); + return; + } + + // 유효한 경우 현재 값을 이전 값으로 저장 + setPrevStartYmDate(startYmDate); + setPrevEndYmDate(endYmDate); + + const filteredData = gridListUnderWaterDetail.filter((item: UnderWaterInputModalProps) => { + // msrDt가 "2024-01" 형태이므로 "202401" 형태로 변환 + const itemDate = item.msrDt.replace('-', ''); + return itemDate >= startYmDate && itemDate <= endYmDate; + }); + const filteredYearData = setFilterAfterData(filteredData); + setCopyYearList(filteredYearData as UnderWaterInputModalProps[]); + }, [startYmDate, endYmDate, gridListUnderWaterDetail, prevStartYmDate, prevEndYmDate]); + + return ( + <> + <UITab tabList={tabList1} defaultValue="tab_term_year" onValueChange={(val) => {setTabTerm(val)}}> + <TabsContent value="tab_term_year"> + <ListDiv className="!h-[100px]"> + <div className="card !m-[1.2rem] max-h-[40rem] overflow-auto !p-0r"> + <Table> + <thead> + <TableRow> + <TableHead className="w-[120px]">{'시설명'}</TableHead> + <TableCell className="w-[300px]">{fcltyName}</TableCell> + <TableHead className="w-[120px]">{'년/월'}</TableHead> + <TableCell className=""> + <div className="flex"> + <DateMonthPicker + field={{ + value: startYmDate, + onChange: (value: string) => { + setStartYmDate(value); + } + }} + selectClass="ml-5 w-[100px]" + startYear={Number(searchYear)} // 시작 연도 설정 + /> + <h3 className="justify-center px-5">~</h3> + <DateMonthPicker + field={{ + value: endYmDate, + onChange: (value: string) => { + setEndYmDate(value); + } + }} + selectClass="ml-5 w-[100px]" + startYear={Number(searchYear)} // 시작 연도 설정 + /> + </div> + </TableCell> + </TableRow> + </thead> + </Table> + </div> + </ListDiv> + <div className="overflow-y-auto h-[500px]"> + <UiTable + headName={undefined} + tableData={copyYearList} + headlist={upHeadListUnderWaterDetail} + pageSize={0} + total={0} + cellClick={undefined} + onPageChange={() => {}} + className="h-[200px]" + boldArray={[0,1,2]} + /> + </div> + </TabsContent> + + <TabsContent value="tab_term_summer"> + {/* <FacilityTermModalHeader<UnderWaterInputModalProps> + searchList={gridListUnderWaterDetail} + fcltyCd={facilityCd} + fcltyName={fcltyName} + tabName="tab_term_summer" + searchYear={searchYear} + ref={ref} + queryClick={filterData} + /> */} + <ListDiv className="!h-[100px]"> + <div className="card !m-[1.2rem] max-h-[40rem] overflow-auto !p-0r"> + <Table> + <thead> + <TableRow> + <TableHead className="w-[120px]">{'시설명'}</TableHead> + <TableCell className="w-[300px]">{fcltyName}</TableCell> + <TableHead className="w-[120px]">{'년/월'}</TableHead> + <TableCell className=""> + <div className="flex"> + <DateMonthPicker + field={{ + value: startYmDate, + onChange: (value: string) => { + setStartYmDate(value); + } + }} + selectClass="ml-5 w-[100px]" + startYear={Number(searchYear)} // 시작 연도 설정 + /> + <h3 className="justify-center px-5">~</h3> + <DateMonthPicker + field={{ + value: endYmDate, + onChange: (value: string) => { + setEndYmDate(value); + } + }} + selectClass="ml-5 w-[100px]" + startYear={Number(searchYear)} // 시작 연도 설정 + /> + </div> + </TableCell> + </TableRow> + </thead> + </Table> + </div> + </ListDiv> + <div className="overflow-y-auto h-[500px]"> + <UiTable + headName={undefined} + tableData={copyYearList} + headlist={upHeadListUnderWaterDetail} + pageSize={0} + total={0} + cellClick={undefined} + onPageChange={() => { } } + className="h-[500px]" + boldArray={[0,1,2]} + /> + </div> + </TabsContent> + + <TabsContent value="tab_term_winter"> + <ListDiv className="!h-[100px]"> + <div className="card !m-[1.2rem] max-h-[40rem] overflow-auto !p-0r"> + <Table> + <thead> + <TableRow> + <TableHead className="w-[120px]">{'시설명'}</TableHead> + <TableCell className="w-[300px]">{fcltyName}</TableCell> + <TableHead className="w-[120px]">{'년/월'}</TableHead> + <TableCell className=""> + <div className="flex"> + <DateMonthPicker + field={{ + value: startYmDate, + onChange: (value: string) => { + setStartYmDate(value); + } + }} + selectClass="ml-5 w-[100px]" + startYear={Number(searchYear)} // 시작 연도 설정 + /> + <h3 className="justify-center px-5">~</h3> + <DateMonthPicker + field={{ + value: endYmDate, + onChange: (value: string) => { + setEndYmDate(value); + } + }} + selectClass="ml-5 w-[100px]" + startYear={Number(searchYear)} // 시작 연도 설정 + /> + </div> + </TableCell> + </TableRow> + </thead> + </Table> + </div> + </ListDiv> + <div className="overflow-y-auto h-[500px]"> + <UiTable + headName={undefined} + tableData={copyYearList} + headlist={upHeadListUnderWaterDetail} + pageSize={0} + total={0} + cellClick={undefined} + onPageChange={() => {}} + className="h-[500px]" + boldArray={[0,1,2]} + /> + </div> + </TabsContent> + </UITab> + </> + ); +} \ No newline at end of file diff --git a/apps/fo/app/pages/underWaterInput/page.tsx b/apps/fo/app/pages/underWaterInput/page.tsx new file mode 100644 index 000000000..d57c894dd --- /dev/null +++ b/apps/fo/app/pages/underWaterInput/page.tsx @@ -0,0 +1,238 @@ +"use client" +import { useFoStore } from "@/app/store"; +import type { TableUpperProps } from "@/app/store/common"; +import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; +import ListDiv from "@common/business_components/ui/list_div"; +import { useCallback, useEffect, useRef, useState } from "react"; +import UnderWaterInputModal from "./modal/page"; +import type { UnderWaterProps } from "@/app/store/underWaterInput/types"; + +export default function UnderWaterInput() { + const {Common + ,UnderWater + ,UnderWaterSearch + ,initializeCommonData + ,searchUnderWaterList + } = useFoStore((state) => state) || { + Common: { isInit: false, selectUpdownData: [], selectOperationData: [], selectSidoData: [], selectSigunData: [], selectRegionData: [], selectUnitWaterShedData: [], selectMiddleRegionData: [], selectEnvPartData: [], selectSearchYear: [{ val: "0" }], btnClass: "" }, + UnderWater: { underWaterList: [], underWaterCount: 0, excelList: [], upHeadList1: [], upHeadList2: [], upHeadList3: [] }, + UnderWaterSearch: { labelArray: [] }, + initializeCommonData: () => {}, + searchUnderWaterList: () => {} + }; + + const { + isInit, + selectUpdownData, + selectOperationData, + selectSidoData, + selectSigunData, + selectRegionData, + selectUnitWaterShedData, + selectMiddleRegionData, + selectEnvPartData, + selectSearchYear, + btnClass + } = Common; + + const { + labelArray, + } = UnderWaterSearch; + + const { + upHeadList1, + upHeadList2, + upHeadList3, + underWaterList, + underWaterCount, + excelList, + } = UnderWater; + + const ref = useRef<ReturnType>({ + partValue: '', + sidoValue: '', + sigunValue: '', + regionValue: '', + unitWaterShedValue: '', + middleRegionValue: '', + envPartValue: '', + sidoText: '', + sigunText: '' + }); + + const underWater = UnderWater?.underWaterList || []; + + // 모달 상태 추가 + const [selectedRow, setSelectedRow] = useState<number | null>(null); + const [fcltyNm, setFcltyNm] = useState<string>(""); + + // 그리드 관련 + const searchInputRef = useRef<HTMLDivElement & HTMLInputElement>(null); + + // 초기화 변수 + const [resetPage, setResetPage] = useState<boolean>(false); + // 표 너비 + const [widthSize, setWidthSize] = useState<string>("w-[120%]"); + // 구분별 그리드 헤더 + const [upheadList, setUpheadList] = useState<TableUpperProps[]>(upHeadList1); + // 용량별 + const [fcltCpct, setFcltCpct] = useState<string>("0"); + // 시설명 + const [fcltyName, setFcltyName] = useState<string>(""); + // 시설상태 + const [operationValue, setOperationValue] = useState<string>("0"); + // 조회기간 + //const [searchYear, setSearchYear] = useState<string>(selectSearchYear[0]?.val || "0"); + const [searchYear, setSearchYear] = useState<string>("2023"); + + /** + * 페이지 함수 + */ + // 초기화 + const resetVal = () => { + setWidthSize("w-[120%]"); + setUpheadList(upHeadList1) + // 상태값 설정 + setFcltCpct(selectUpdownData[0]?.val || "0"); + setOperationValue(selectOperationData[0]?.val || "0"); + setSearchYear(selectSearchYear[0]?.val || "0"); + // SearchInputSelect 초기화 + setFcltyName(""); + if (searchInputRef.current) { + const input = searchInputRef.current.querySelector('input'); + if (input) { + input.value = ""; + } + } + searchUnderWaterList(1, 10, selectSearchYear[0]?.val || "0"); + } + + // 모달 닫기 + const closeModal = () => { + setSelectedRow(null); + }; + + // 셀 클릭 시 모달 표시 + const cellClick = (index: number) => { + if (underWater[index]) { + setFcltyNm(underWater[index].bscFctCd); + setSelectedRow(index); // 클릭한 행의 index 저장 + } + }; + + /** + * useEffect + */ + useEffect(() => { + if (!isInit) { + initializeCommonData(); + searchUnderWaterList(1, 10, selectSearchYear[0]?.val || "0"); + } + }, [isInit, initializeCommonData, searchUnderWaterList, selectSearchYear]); + + useEffect(() => { + if (excelList && excelList.length > 0) { + downloadExcel({ + data: excelList as unknown as JSON[], + fileName: '지하수유입량 추정', + sheetNameArray: ['지하수유입량 추정'], + headerRange: 'A1:V1' + }); + } + }, [excelList]); + + /** + * 콜백함수 + */ + // 조회 + const searchFacilityPrecipitation = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => { + let nameCd, partStr + switch (partValue) { + case 'R': + if (regionValue !== '0') { + if(unitWaterShedValue==='0') nameCd = regionValue; + else if (middleRegionValue==='0') nameCd = unitWaterShedValue; + else nameCd = middleRegionValue; + } + partStr = partValue + break; + default: + if(sidoValue !== '0') nameCd = sidoValue; + if(sigunValue !== '0') nameCd = sigunValue; + partStr = envPartValue !== '0' ? envPartValue:partValue; + break; + } + if (partStr.length > 1 || fcltCpct !== '0' || operationValue!== '0' ||nameCd ||(fcltyName.trim() !== "" && fcltyName.trim() !== null)) { + searchUnderWaterList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined); + } else { + searchUnderWaterList(pageNo, pageCnt, searchYear); + } + }, [fcltCpct, operationValue, fcltyName, searchUnderWaterList, searchYear]); + + const chkVal = useCallback((returntype:ReturnType) => { + setWidthSize(returntype.partValue=='A' ?"w-[120%]":"w-[130%]") + setUpheadList(returntype.partValue == 'R' ? upHeadList2 : returntype.partValue == 'G'?upHeadList3:upHeadList1); + setResetPage(true); + }, [upHeadList1, upHeadList2, upHeadList3]); + + // 엑셀다운로드 + const excelDownload = useCallback(() => { + searchFacilityPrecipitation(ref.current, 1, 10000); + }, [searchFacilityPrecipitation]); + + // 페이지 변경 + const handlePageChange = useCallback((page: number) => { + setResetPage(false); + searchFacilityPrecipitation(ref.current, page, 10); + }, [searchFacilityPrecipitation]); + + const mainList: UnderWaterProps[] = underWaterList || []; + const total = underWaterCount || 0; + + return ( + <MainDiv> + <Title title="지하수유입량 추정" /> + <ContentsDiv> + <SearchBaseForm + leftChildren={<> + <SelectBox className="col-span-3" label={labelArray[3] as string} selectArray={selectUpdownData} onSelectValue={(val) => setFcltCpct(val.toString())} value={fcltCpct} selectClass={undefined} /> + <SearchInput className={"col-span-3"} label={labelArray[4] as string} textValue={fcltyName} setText={setFcltyName} /> + <SelectBox className="col-span-3" label={labelArray[5] as string} selectArray={selectSearchYear} onSelectValue={(val) => setSearchYear(val.toString())} value={searchYear} selectClass={undefined} /> + <SelectBox className="col-span-3" label={labelArray[6] as string} selectArray={selectOperationData} onSelectValue={(val) => setOperationValue(val.toString())} value={operationValue} selectClass={undefined} /> + </>} + rightChildren={undefined} + selectSidoData={selectSidoData} + selectSigunData={selectSigunData} + selectRegionData={selectRegionData} + selectUnitWaterShedData={selectUnitWaterShedData} + selectMiddleRegionData={selectMiddleRegionData} + selectEnvPartData={selectEnvPartData} + btnClass={btnClass} + queryClick={chkVal} + excelClick={excelDownload} + initClick={resetVal} + ref={ref} + isAdmin={false} + /> + <ListDiv> + <div className="h-full w-full overflow-x-scroll"> + <div className={widthSize}> + <UiTable cellClick={(index: number | undefined) => index !== undefined && cellClick(index)} headName={"지하수유입량 추정"} tableData={mainList} headlist={upheadList} pageSize={10} total={total} resetPageNo={resetPage} onPageChange={handlePageChange} /> + </div> + </div> + <span className="text-2xl text-gray-500">※지하수 유입량 = 하수처리장 청천시 실제유입량 - (물사용량 × 오수전환율)</span> + </ListDiv> + </ContentsDiv> + {selectedRow !== null && ( + <UiModal + body={<UnderWaterInputModal fcltyName={fcltyNm} searchYear={searchYear}></UnderWaterInputModal>} + bodyClass="sm:max-w-[90vw] w-[1000px] h-[770px]" + open={selectedRow !== null} + onOpenChange={(open) => !open && closeModal()} + noHeaderYn="Y" + noFooterYn="Y" + /> + )} + </MainDiv> + ); +} \ No newline at end of file diff --git a/apps/fo/app/store/common/index.tsx b/apps/fo/app/store/common/index.tsx new file mode 100644 index 000000000..3e15e2162 --- /dev/null +++ b/apps/fo/app/store/common/index.tsx @@ -0,0 +1,432 @@ +/**==================================== + * 보편적으로 쓰는 store 공간 + ====================================**/ + +import { getFetch } from "@/app/components/api"; + +/** + * STORE - TYPE + */ + +// selectBox +export type SelectDataType = { + text: string; + val: string; +} +// grid header +export type TableUpperProps = { + id: string; + title: string; + upName?: string; + upSequnce?: number; +} + +// 시도 데이터 +export type sidoDataType = { + adstDstrCd: number, + sido: string, +} + +// 시군구 데이터 +export type sigunDataType = { + adstDstrCd: number, + gugun: string +} + +// 권역 데이터 +export type regionDataType = { + rvbsnCd: string, + rvbsn: string, +} + +// 단위유역 데이터 +export type unitWaterShedDataType = { + rvbsnCd: string, + lgbsn: string, +} + +// 중권역 데이터 +export type middleRegionDataType = { + rvbsnCd: string, + mdmbsn: string, +} + +// 환경청 데이터 +export type envPartDataType = { + lclgvrnmntCd: string, + lclgvrnmntNm: string, + adstDliStrCd: string, + adstDstrCd: string, +} + +export type envPartSelectDataType = SelectDataType & { + adstDliStrCd: string, +} + +export interface MenuItem { + menuCd: number; + menuKornNm: string; + menuEngNm: string; + parentId: number | null; + sortSeq: number; + iconNm?: string; + children: MenuItem[] | null; + levelNo: number; + urlPath: string; + menuLnkgTypeCd: string; + menuLnkgSn: number; + menuPath: string; + qrcd: string; + useYn: string; + otptYn: string; + newWndwYn: string; + requiredUrlPath: boolean; +} + +/** + * STORE - DATA + */ + +// 시도 데이터 +let selectSidoData: SelectDataType[] = [ + { text: "전체", val: '0' } +]; + +// 시군구 데이터 +let selectSigunData: SelectDataType[] = [ + { text: "전체", val: '0' } +]; + +// 권역 데이터 +const selectRegionData: SelectDataType[] = [ + { text: "전체", val: '0' } +]; + +// 단위유역 데이터 +const selectUnitWaterShedData: SelectDataType[] = [ +]; + +// 환경청 데이터 +let selectEnvPartData: envPartSelectDataType[] = [ + { text: "전체", val: '0', adstDliStrCd: '' } +]; + +// 환경청 내 시도 데이터 +const selectEnvPartSidoData: SelectDataType[] = [ + { text: "전체", val: '0' } +]; + +// 환경청 내 시군구 데이터 +const selectEnvPartSigunData: SelectDataType[] = [ + { text: "전체", val: '0' } +]; + +// 구분 데이터 +// const selectPartData:SelectDataType[] = [ +// { text: "행정별", val: '0' }, +// { text: "유역별", val: '01' }, +// { text: "환경청별", val: '02' }, +// ]; + +// 상향 하향 데이터 +const selectUpdownData:SelectDataType[] = [ + { text: "전체", val: '0' }, + { text: "500이하", val: '400' }, + { text: "500이상", val: '500' }, +]; + +// 운영 상태 데이터 +const selectOperationData:SelectDataType[] = [ + { text: "전체", val: '0' }, + { text: "가동중", val: '1' }, + { text: "재가동", val: '2' }, + { text: "가동중지", val: '3' }, + { text: "시설폐쇄", val: '4' }, +]; + +// 시설 구분 데이터 +const selectFacilityPartData:SelectDataType[] = [ + { text: "전체", val: '0' }, + { text: "공공하수처리시설", val: 'SW' }, + { text: "오수처리장", val: 'SS' }, + { text: "개인하수처리시설", val: 'PB' }, + { text: "분뇨처리장", val: 'NS' }, + { text: "폐수처리장", val: 'LW' }, + { text: "침출수처리장", val: 'LC' }, + { text: "기타기초시설", val: 'ET' }, +]; + +// 년도 데이터 +const getYearData = () => { + const year = new Date().getFullYear(); + const year1 = year - 1; + const year2 = year - 2; + const year3 = year - 3; + const year4 = year - 4; + const year5 = year - 5; + return [ + { text: year1 + "년", val: year1.toString() }, + { text: year2 + "년", val: year2.toString() }, + { text: year3 + "년", val: year3.toString() }, + { text: year4 + "년", val: year4.toString() }, + { text: year5 + "년", val: year5.toString() }, + ] +} + +const selectSearchYear:SelectDataType[] = getYearData(); + +// 유량 기간 구분 +const selectFlowTerm : SelectDataType[] = [ + { text: "연간", val: '0' }, + { text: "하절기", val: '01' }, + { text: "동절기", val: '02' }, + { text: "청천시", val: '03' }, + { text: "강우시", val: '04' }, +]; + +// 유량 기간 구분 +const selectFlowPanel : SelectDataType[] = [ + { text: "전체", val: '0'}, + { text: "3~10mm미만", val: '2'}, + { text: "10~20mm미만", val: '3'}, + { text: "20~30mm미만", val: '4'}, + { text: "30~40mm미만", val: '5'}, + { text: "40~50mm미만", val: '6'}, + { text: "50~60mm미만", val: '7'}, + { text: "60mm이상", val: '8'}, +]; + +// 중권역 데이터 +const selectMiddleRegionData: SelectDataType[] = [ + { text: "전체", val: '0' } +]; + +/** + * STORE - API + */ + +// 시도 API 호출 함수 추가 +const fetchSidoData = async (): Promise<SelectDataType[]> => { + try { + const res = await getFetch('fo', '/addr/city'); + const data = res.data; + selectSidoData = [ + { text: "전체", val: '0'}, + ...data.map((item: sidoDataType) => ({ + text: item.sido || '', + val: String(item.adstDstrCd) || '' + })) + ]; + return selectSidoData; + } catch (error) { + console.error('시도 데이터를 가져오는데 실패했습니다:', error); + return selectSidoData; + } +}; + +// 시군구 API 호출 함수 추가 +const fetchSigunData = async (): Promise<SelectDataType[]> => { + try { + const res = await getFetch('fo', '/addr/sgg'); + const data = res.data; + selectSigunData = [ + { text: "전체", val: '0' }, + ...data + .map((item: sigunDataType) => ({ + text: item.gugun || '', + val: String(item.adstDstrCd) || '' + })) + ]; + return selectSigunData; + } catch (error) { + console.error('시군구 데이터를 가져오는데 실패했습니다:', error); + return selectSigunData; + } +}; + +// 권역 API 호출 함수 추가 +const fetchRegionData = async (): Promise<SelectDataType[]> => { + try { + const res = await getFetch('fo', '/addr/sarea'); + const data = res.data; + return [ + { text: "전체", val: '0'}, + ...data.map((item: regionDataType) => ({ + text: item.rvbsn || '', + val: String(item.rvbsnCd) || '' + })) + ]; + } catch (error) { + console.error('권역 데이터를 가져오는데 실패했습니다:', error); + return selectRegionData; + } +}; + +// 중권역 API 호출 함수 추가 +const fetchMiddleRegionData = async (): Promise<SelectDataType[]> => { + try { + const res = await getFetch('fo', '/addr/midArea'); + const data = res.data; + return [ + { text: "전체", val: '0' }, + ...data + .map((item: middleRegionDataType) => ({ + text: item.mdmbsn || '', + val: String(item.rvbsnCd) || '' + })) + ]; + } catch (error) { + console.error('중권역 데이터를 가져오는데 실패했습니다:', error); + return selectMiddleRegionData; + } +}; + +// 단위유역 API 호출 함수 추가 +const fetchUnitWaterShedData = async (): Promise<SelectDataType[]> => { + try { + const res = await getFetch('fo', '/addr/largeArea'); + const data = res.data; + return [ + { text: "전체", val: '0' }, + ...data + .map((item: unitWaterShedDataType) => ({ + text: item.lgbsn || '', + val: String(item.rvbsnCd) || '' + })) + ]; + } catch (error) { + console.error('단위유역 데이터를 가져오는데 실패했습니다:', error); + return selectUnitWaterShedData; + } +}; + +// 환경청 데이터 +const fetchEnvPartData = async (): Promise<envPartSelectDataType[]> => { + try { + const res = await getFetch('fo', '/addr/govArea'); + const data = res.data; + selectEnvPartData = [ + { text: "전체", val: '0', adstDliStrCd: '' }, + ...data + .map((item: envPartDataType) => ({ + text: item.lclgvrnmntNm || '', + val: String(item.lclgvrnmntCd) || '', + adstDliStrCd: item.adstDliStrCd || '' + })) + ]; + return selectEnvPartData; + } catch (error) { + console.error('환경청 데이터를 가져오는데 실패했습니다:', error); + return selectEnvPartData; + } +}; + +/**==================================== + * Store - expaort + ====================================*/ +export type CommonAction = { + getMenuAuth : (menuCd : number) => Promise<void>; + initializeCommonData: () => Promise<void>; +} + +export type CommonType = { + Common: { + isInit: boolean; + selectSidoData: SelectDataType[]; + selectSigunData: SelectDataType[]; + selectUpdownData: SelectDataType[]; + selectOperationData: SelectDataType[]; + selectFacilityPartData: SelectDataType[]; + selectSearchYear: SelectDataType[]; + selectUnitWaterShedData: SelectDataType[]; + selectRegionData: SelectDataType[]; + selectMiddleRegionData: SelectDataType[]; + selectEnvPartSidoData: SelectDataType[]; + selectEnvPartSigunData: SelectDataType[]; + selectFlowTerm: SelectDataType[]; + selectFlowPanel: SelectDataType[]; + selectEnvPartData: envPartSelectDataType[]; + btnClass: string; + }; + + auth: { + mngAuthrtYn: string; + inqAuthrtYn: string; + regAuthrtYn: string; + mdfcnAuthrtYn: string; + delAuthrtYn: string; + }; +}; + +export const commonState: CommonType = { + Common: { + isInit: false, + selectSidoData: selectSidoData, + selectSigunData: selectSigunData, + selectUpdownData: selectUpdownData, + selectOperationData: selectOperationData, + selectFacilityPartData: selectFacilityPartData, + selectSearchYear: selectSearchYear, + selectRegionData: selectRegionData, + selectUnitWaterShedData: selectUnitWaterShedData, + selectMiddleRegionData: selectMiddleRegionData, + selectEnvPartSidoData: selectEnvPartSidoData, + selectEnvPartSigunData: selectEnvPartSigunData, + selectFlowTerm: selectFlowTerm, + selectFlowPanel: selectFlowPanel, + selectEnvPartData: selectEnvPartData, + btnClass: "p-4 m-2 text-white rounded-sm text-lg font-medium", + }, + + auth: { + mngAuthrtYn: "N", + inqAuthrtYn: "N", + regAuthrtYn: "N", + mdfcnAuthrtYn: "N", + delAuthrtYn: "N", + }, +}; + +export const CommonReducer:(set:(fn: (state: CommonType) => CommonType) => void)=>CommonAction=(set: (fn: (state: CommonType) => CommonType) => void) => { + return { + // 메뉴 권한 확인 + getMenuAuth : async (menuCd : number) =>{ + const response = await getFetch( + "local", + `/cms-service/api/v1/menu-authrts/pageinfo/${menuCd}` + ); + set((state: CommonType) => { + return { + ...state, + auth: { + mngAuthrtYn: response[0].mngAuthrtYn, + inqAuthrtYn: response[0].mngAuthrtYn, + regAuthrtYn: response[0].mngAuthrtYn, + mdfcnAuthrtYn: response[0].mngAuthrtYn, + delAuthrtYn: response[0].mngAuthrtYn, + }, + }; + }); + }, + + // 시도 변경 이벤트 + initializeCommonData: async () => { + const promiseSet = await Promise.all([fetchSidoData(), fetchSigunData(), fetchRegionData(), fetchUnitWaterShedData(), fetchMiddleRegionData(), fetchEnvPartData()]); + const [sidoData, sggData, regionData, unitData, midData, envData] = promiseSet; + set((state: CommonType) => ({ + ...state, + Common: { + ...state.Common, + selectSidoData: sidoData, + selectSigunData: sggData, + selectRegionData: regionData, + selectMiddleRegionData: midData, + selectUnitWaterShedData: unitData, + selectEnvPartData: envData + } + })) + }, + } +} + +export type CommonStore = CommonType & CommonAction; \ No newline at end of file diff --git a/apps/fo/app/store/dashboard/index.tsx b/apps/fo/app/store/dashboard/index.tsx new file mode 100644 index 000000000..c0720b9fa --- /dev/null +++ b/apps/fo/app/store/dashboard/index.tsx @@ -0,0 +1,1687 @@ +/** + * Store - 타입 + */ + +import { getFetch, getFetchAreaList } from "@/app/components/api"; +import DataForm from "@/app/components/common/DataForm"; +import type { FacilityWaterQltyProps } from "../facilityWaterQlty"; + +// selectBox +export type SelectDataType = { + text: string; + val: string; +} + +// grid header +export type TableUpperProps = { + id: string; + title: string; + upName?: string; + upSequnce?: number; + align? : string; + row? : number; + rowSpan?: number; + colSpan?: number; +} + +// ===TAB1 +// main fclty +export type mainFcltyProps = { + fcltyNm : string, + facilityCapacity : string, + publicMethod : string, + location : string +} + +// 시설계획 +export type planFcltyListProps = { + item : string, + provisionalApproval : string | number | '', + now : string | number | '', + stepOne : string | number | '', + stepTwo : string | number | '', + stepThree : string | number | '', + stepFour : string | number | '', +} + +// 운영실태 +export type OperationalStatusListProps = { + flowRate : string, + waterQlty : string, + yearAvg : number | string | '', + yearMax : number | string | '', + yearMin : number | string | '', + summerAll : number | string | '', + summerThree : number | string | '', + summerRain : number | string | '', + winterAll : number | string | '', + winterSunnyAvg : number | string | '', + winterSunnyMax : number | string | '', + winterSunnyMin : number | string | '', + groundWaterAvg : number | string | '', + groundWaterSummer : number | string | '', + groundWaterWinter : number | string | '', + rdi : number | string | '', + rdiRain : number | string | '', +} + +export interface operationalStatusListProps { + flowRate : string, + waterQlty : string, + yearAvg : number | '', + yearMax : number | '', + yearMin : number | '', + summerAll : number | '', + summerThree : number | '', + summerRain : number | '', + winterAll : number | '', + winterSunnyAvg : number | '', + winterSunnyMax : number | '', + winterSunnyMin : number | '', + groundWaterAvg : number | string | '', + groundWaterSummer : number | string | '', + groundWaterWinter : number | string | '', + rdi : number | '', +} + +// 차트 +export type lineBarChartDataProps = { + label : string, + lineVal : number + barVal : number + barVal0 : number + barVal1 : number + lineVal0 : number + lineVal1 : number +} + +export type lineChartDataProps = { + label : string, + val : number +} + +export interface RainManageListProps { + rainItem : string, + rainRate : string, + rainDay : number, + bscFctCd?: string, + bscFctNm?: string, + msrDt?: string + sidoNm?: string, + sggNm?: string, + addr?: string, + fctCpc?: number, + inflAssctAftWqltBod: number | '', + inflAssctAftWqltToc: number | '', + inflAssctAftWqltSs: number | '', + inflAssctAftWqltTn: number | '', + inflAssctAftWqltTp: number | '', + efflWqltBod: number | '', + efflWqltCod: number | '', + efflWqltSs: number | '', + efflWqltTn: number | '', + efflWqltTp: number | '', + rgnNm?:string|'', + rvbsn?:string|'', + lgbsn?:string|'', + mdmbsn?:string|'', + inflAmnt: number | '', + lclgvrnmntNm?: string, + infoRnDay?: number | '', + tpInflWqltTmprtr?: number | '', + tpInflWqltPh?: number | '', + tpInflWqltBod?: number | '', + tpInflWqltCod?: number | '', + tpInflWqltSs?: number | '', + tpInflWqltTn?: number | '', + tpInflWqltTp?: number | '', + tpInflWqltEclgyTox?: number | '', + tpInflWqltClfgrpcnt?: number | '', + dsInflWqltBod: number | '', + dsInflWqltCod: number | '', + dsInflWqltSs: number | '', + dsInflWqltTn: number | '', + dsInflWqltTp: number | '', + extnsSeqno?: number | '', +} + +export interface RainManageExcelType { + 강우량: string, + 강우조건비율: number | '', + 일수: number | '', + 유입유량: number | '', + 시설용량대비: number | '', + 설계유입수질BOD: number | '', + 설계유입수질TOC: number | '', + 설계유입수질SS: number | '', + 설계유입수질TN: string | number | '', + 설계유입수질TP: string | number | '', + 유입수질BOD: number | '', + 유입수질TOC: number | '', + 유입수질SS: number | '', + 유입수질TN: string | number | '', + 유입수질TP: string | number | '', + 방류수질BOD: number | '', + 방류수질TOC: number | '', + 방류수질SS: number | '', + 방류수질TN: string | number | '', + 방류수질TP: string | number | '', +} + +export interface DrainageEquipmentListProps { + part1 : string | '', + part2 : string | '', + nowSum : number | '', + nowJoin : number | '', + nowSep : number | '', + stepOneSum : number | '', + stepOneJoin : number | '', + stepOneSep : number | '', + stepTwoSum : number | '', + stepTwoJoin : number | '', + stepTwoSep : number | '', + stepThreeSum : number | '', + stepThreeJoin : number | '', + stepThreeSep : number | '', + stepFourSum : number | '', + stepFourJoin : number | '', + stepFourSep : number | '', + etc : string | '', +} + +export interface DrainageEquipmentExcelType { + partOne : string | '', + partTwo : string | '', + nowSum : number | '', + nowJoin : number | '', + nowSep : number | '', + stepOneSum : number | '', + stepOneJoin : number | '', + stepOneSep : number | '', + stepTwoSum : number | '', + stepTwoJoin : number | '', + stepTwoSep : number | '', + stepThreeSum : number | '', + stepThreeJoin : number | '', + stepThreeSep : number | '', + stepFourSum : number | '', + stepFourJoin : number | '', + stepFourSep : number | '', +} + +/** + * Store - data + */ +// Main +const DashBoardTopLabelArray:string[] = ['구분', '시도', '시군구', '년도', '시설명']; + +// === Tab1 +// grid header +const upHeadListFclty:TableUpperProps[] = [ + { id : "fcltyNm", title : "시설명"}, + { id : "facilityCapacity", title : "시설용량(m³/일)"}, + { id : "publicMethod", title : "공법"}, + { id : "location", title : "지역구"}, +] + +const fcltyList : mainFcltyProps[] = [ +] + +let planYearList : string[] = [ + "2021", + "2022", + "2023", + "2024", + "2025", + "2026" +] + +const upHeadListPlanFclty : TableUpperProps[] = [ + { id: "item", title: "", upName:"upChangeRe", upSequnce:1 }, + { id: "provisionalApproval", title: "기승인\n4단계", upName:"upChangeRe", upSequnce:2 }, + { id: "now", title: "현재", upName:"upChangeRe", upSequnce:3 }, + { id: "stepOne", title: "1단계", upName:"upChangeRe", upSequnce:4 }, + { id: "stepTwo", title: "2단계", upName:"upChangeRe", upSequnce:5 }, + { id: "stepThree", title: "3단계", upName:"upChangeRe", upSequnce:6 }, + { id: "stepFour", title: "4단계", upName:"upChangeRe", upSequnce:7 }, +] + +const planFcltyList:planFcltyListProps[] = [ + { + item : "1.증설계획\n(㎥/일)", + provisionalApproval: "", + now: "", + stepOne: "", + stepTwo: "", + stepThree: "", + stepFour : "", + }, + { + item : "2.처리인구(인)", + provisionalApproval: "", + now: "", + stepOne: "", + stepTwo: "", + stepThree: "", + stepFour : "", + }, + { + item : "3.처리구역\n(km²)", + provisionalApproval: "", + now: "", + stepOne: "", + stepTwo: "", + stepThree: "", + stepFour : "", + }, + { + item : "4.분류식화율\n(%)", + provisionalApproval: "", + now: "", + stepOne: "", + stepTwo: "", + stepThree: "", + stepFour : "", + }, + { + item : "5.물사용량", + provisionalApproval: "", + now: "", + stepOne: "", + stepTwo: "", + stepThree: "", + stepFour : "", + }, + { + item : "년도\n(최근 3개년)", + provisionalApproval: "물사용량\n(㎥/일)", + now: "", + stepOne: "", + stepTwo: "비고", + stepThree: "", + stepFour : "", + }, + { + item : "2021", + provisionalApproval: "", + now: "", + stepOne: "", + stepTwo: "", + stepThree: "", + stepFour : "", + }, + { + item : "2022", + provisionalApproval: "", + now: "", + stepOne: "", + stepTwo: "", + stepThree: "", + stepFour : "", + }, + { + item : "2023", + provisionalApproval: "", + now: "", + stepOne: "", + stepTwo: "", + stepThree: "", + stepFour : "", + }, +] + +const upHeadListOperationStatus : TableUpperProps[] = [ + { id: "flowRate", title: "유량 및 수질", upName:"upChangeRe1", upSequnce:1 }, + { id: "waterQlty", title: "유량 및 수질", upName:"upChangeRe1", upSequnce:2 }, + { id: "yearAvg", title: "평균", upName:"upChangeRe2", upSequnce:1 }, + { id: "yearMax", title: "최대", upName:"upChangeRe2", upSequnce:2 }, + { id: "yearMin", title: "최소", upName:"upChangeRe2", upSequnce:3 }, + { id: "summerAll", title: "전기간", upName:"upChangeRe3", upSequnce:1 }, + { id: "summerThree", title: "강우시\n(3mm이상)", upName:"upChangeRe3", upSequnce:2 }, + { id: "summerRain", title: "청천시", upName:"upChangeRe3", upSequnce:3 }, + { id: "winterAll", title: "전기간", upName:"upChangeRe4", upSequnce:1 }, + { id: "winterSunnyAvg", title: "청천시\n평균", upName:"upChangeRe4", upSequnce:2 }, + { id: "winterSunnyMax", title: "청천시\n최대", upName:"upChangeRe4", upSequnce:3 }, + { id: "winterSunnyMin", title: "청천시\n최소", upName:"upChangeRe4", upSequnce:4 }, + { id: "groundWaterAvg", title: "연평균", upName:"upChangeRe5", upSequnce:1 }, + { id: "groundWaterSummer", title: "하절기\n평균", upName:"upChangeRe5", upSequnce:2 }, + { id: "groundWaterWinter", title: "동절기\n평균", upName:"upChangeRe5", upSequnce:3 }, + { id: "rdi", title: "RDII 추정" }, + { id: "rdiRain", title: "RDII 추정시\n일강수량(mm)" }, +] + +const upHeadListRainManage : TableUpperProps[] = [ + { id: "rainItem", title: "강우량", row:0, rowSpan:2 }, + { id: "rainRate", title: "강우조건\n비율(%)", row:0, rowSpan:2 }, + { id: "rainDay", title: "일수", row:0, rowSpan:2 }, + { id: "A", title: "유입(㎥/일)", row:0, colSpan:2 }, + { id: "inflAmnt", title: "유입유량", row:1 }, + { id: "fctCpc", title: "시설용량대비", row:1 }, + { id: "B", title: "설계유입수질(㎎/L)", row:0, colSpan:5 }, + { id: "dsInflWqltBod", title: "BOD", row:1 }, + { id: "dsInflWqltCod", title: "TOC\n(COD)", row:1 }, + { id: "dsInflWqltSs", title: "SS", row:1 }, + { id: "dsInflWqltTn", title: "T-N", row:1 }, + { id: "dsInflWqltTp", title: "T-P", row:1 }, + { id: "C", title: "유입수질(㎎/L)", row:0, colSpan:5 }, + { id: "inflAssctAftWqltBod", title: "BOD", row:1 }, + { id: "inflAssctAftWqltToc", title: "TOC\n(COD)", row:1 }, + { id: "inflAssctAftWqltSs", title: "SS", row:1 }, + { id: "inflAssctAftWqltTn", title: "T-N", row:1 }, + { id: "inflAssctAftWqltTp", title: "T-P", row:1 }, + { id: "D", title: "방류수질(㎎/L)", row:0, colSpan:5 }, + { id: "efflWqltBod", title: "BOD", row:1 }, + { id: "efflWqltCod", title: "TOC\n(COD)", row:1 }, + { id: "efflWqltSs", title: "SS", row:1 }, + { id: "efflWqltTn", title: "T-N", row:1 }, + { id: "efflWqltTp", title: "T-P", row:1 }, +] + +const upHeadListDrainageEquipment : TableUpperProps[] = [ + { id: "part1", title: "구분", row:0, upName:"upChangeRe1", upSequnce:1 }, + { id: "part2", title: "구분", row:0, upName:"upChangeRe1", upSequnce:2 }, + { id: "nowSum", title: "계", row:2, upName:"upChangeRe2", upSequnce:1 }, + { id: "nowJoin", title: "합류식\n가구수", row:2, upName:"upChangeRe2", upSequnce:2 }, + { id: "nowSep", title: "분리식\n가구수", row:2, upName:"upChangeRe2", upSequnce:3 }, + { id: "stepOneSum", title: "계", row:2, upName:"upChangeRe3", upSequnce:1 }, + { id: "stepOneJoin", title: "합류식\n가구수", row:2, upName:"upChangeRe3", upSequnce:2 }, + { id: "stepOneSep", title: "분리식\n가구수", row:2, upName:"upChangeRe3", upSequnce:3 }, + { id: "stepTwoSum", title: "계", row:2, upName:"upChangeRe4", upSequnce:1 }, + { id: "stepTwoJoin", title: "합류식\n가구수", row:2, upName:"upChangeRe4", upSequnce:2 }, + { id: "stepTwoSep", title: "분리식\n가구수", row:2, upName:"upChangeRe4", upSequnce:3 }, + { id: "stepThreeSum", title: "계", row:2, upName:"upChangeRe5", upSequnce:1 }, + { id: "stepThreeJoin", title: "합류식\n가구수", row:2, upName:"upChangeRe5", upSequnce:2 }, + { id: "stepThreeSep", title: "분리식\n가구수", row:2, upName:"upChangeRe5", upSequnce:3 }, + { id: "stepFourSum", title: "계", row:2, upName:"upChangeRe6", upSequnce:1 }, + { id: "stepFourJoin", title: "합류식\n가구수", row:2, upName:"upChangeRe6", upSequnce:2 }, + { id: "stepFourSep", title: "분리식\n가구수", row:2, upName:"upChangeRe6", upSequnce:3 }, + { id: "etc", title: "비고", row:0, upName:"upChangeRe7", upSequnce:1 }, +] + +const operationalStatusList : OperationalStatusListProps[] = [ + { + flowRate : '유량\n(㎥/일)', + waterQlty : '유입', + yearAvg : '', + yearMax : '', + yearMin : '', + summerAll : '', + summerThree : '', + summerRain : '', + winterAll : '', + winterSunnyAvg : '', + winterSunnyMax : '', + winterSunnyMin : '', + groundWaterAvg : '', + groundWaterSummer : '', + groundWaterWinter : '', + rdi : '', + rdiRain : '', + }, + { + flowRate : '유량\n(㎥/일)', + waterQlty : '순수하수', + yearAvg : '', + yearMax : '', + yearMin : '', + summerAll : '', + summerThree : '', + summerRain : '', + winterAll : '', + winterSunnyAvg : '', + winterSunnyMax : '', + winterSunnyMin : '', + groundWaterAvg : '', + groundWaterSummer : '', + groundWaterWinter : '', + rdi : '', + rdiRain : '', + }, + { + flowRate : '유량\n(㎥/일)', + waterQlty : '연계처리수', + yearAvg : '', + yearMax : '', + yearMin : '', + summerAll : '', + summerThree : '', + summerRain : '', + winterAll : '', + winterSunnyAvg : '', + winterSunnyMax : '', + winterSunnyMin : '', + groundWaterAvg : '', + groundWaterSummer : '', + groundWaterWinter : '', + rdi : '', + rdiRain : '', + }, + { + flowRate : '유량\n(㎥/일)', + waterQlty : '총인설비\n반류수', + yearAvg : '', + yearMax : '', + yearMin : '', + summerAll : '', + summerThree : '', + summerRain : '', + winterAll : '', + winterSunnyAvg : '', + winterSunnyMax : '', + winterSunnyMin : '', + groundWaterAvg : '', + groundWaterSummer : '', + groundWaterWinter : '', + rdi : '', + rdiRain : '', + }, + { + flowRate : '유량\n(㎥/일)', + waterQlty : '방류', + yearAvg : '', + yearMax : '', + yearMin : '', + summerAll : '', + summerThree : '', + summerRain : '', + winterAll : '', + winterSunnyAvg : '', + winterSunnyMax : '', + winterSunnyMin : '', + groundWaterAvg : '', + groundWaterSummer : '', + groundWaterWinter : '', + rdi : '', + rdiRain : '', + }, + { + flowRate : '수질\n(mg/L)', + waterQlty : '유입\n(BOD)', + yearAvg : '', + yearMax : '', + yearMin : '', + summerAll : '', + summerThree : '', + summerRain : '', + winterAll : '', + winterSunnyAvg : '', + winterSunnyMax : '', + winterSunnyMin : '', + groundWaterAvg : '', + groundWaterSummer : '', + groundWaterWinter : '', + rdi : '', + rdiRain : '', + }, + { + flowRate : '수질\n(mg/L)', + waterQlty : '방류\n(BOD)', + yearAvg : '', + yearMax : '', + yearMin : '', + summerAll : '', + summerThree : '', + summerRain : '', + winterAll : '', + winterSunnyAvg : '', + winterSunnyMax : '', + winterSunnyMin : '', + groundWaterAvg : '', + groundWaterSummer : '', + groundWaterWinter : '', + rdi : '', + rdiRain : '', + }, + { + flowRate : '수질\n(mg/L)', + waterQlty : '유입\n(T-N)', + yearAvg : '', + yearMax : '', + yearMin : '', + summerAll : '', + summerThree : '', + summerRain : '', + winterAll : '', + winterSunnyAvg : '', + winterSunnyMax : '', + winterSunnyMin : '', + groundWaterAvg : '', + groundWaterSummer : '', + groundWaterWinter : '', + rdi : '', + rdiRain : '', + }, + { + flowRate : '수질\n(mg/L)', + waterQlty : '방류\n(T-N)', + yearAvg : '', + yearMax : '', + yearMin : '', + summerAll : '', + summerThree : '', + summerRain : '', + winterAll : '', + winterSunnyAvg : '', + winterSunnyMax : '', + winterSunnyMin : '', + groundWaterAvg : '', + groundWaterSummer : '', + groundWaterWinter : '', + rdi : '', + rdiRain : '', + }, + { + flowRate : '수질\n(mg/L)', + waterQlty : '유입\n(T-P)', + yearAvg : '', + yearMax : '', + yearMin : '', + summerAll : '', + summerThree : '', + summerRain : '', + winterAll : '', + winterSunnyAvg : '', + winterSunnyMax : '', + winterSunnyMin : '', + groundWaterAvg : '', + groundWaterSummer : '', + groundWaterWinter : '', + rdi : '', + rdiRain : '', + }, + { + flowRate : '수질\n(mg/L)', + waterQlty : '방류\n(T-P)', + yearAvg : '', + yearMax : '', + yearMin : '', + summerAll : '', + summerThree : '', + summerRain : '', + winterAll : '', + winterSunnyAvg : '', + winterSunnyMax : '', + winterSunnyMin : '', + groundWaterAvg : '', + groundWaterSummer : '', + groundWaterWinter : '', + rdi : '', + rdiRain : '', + }, + { + flowRate : '총인\n시설\n수질\n(mg/L)', + waterQlty : '총인설비\n유입(T-P)', + yearAvg : '', + yearMax : '', + yearMin : '', + summerAll : '', + summerThree : '', + summerRain : '', + winterAll : '', + winterSunnyAvg : '', + winterSunnyMax : '', + winterSunnyMin : '', + groundWaterAvg : '', + groundWaterSummer : '', + groundWaterWinter : '', + rdi : '', + rdiRain : '', + }, + { + flowRate : '총인\n시설\n수질\n(mg/L)', + waterQlty : '총인설비\n방류(T-P)', + yearAvg : '', + yearMax : '', + yearMin : '', + summerAll : '', + summerThree : '', + summerRain : '', + winterAll : '', + winterSunnyAvg : '', + winterSunnyMax : '', + winterSunnyMin : '', + groundWaterAvg : '', + groundWaterSummer : '', + groundWaterWinter : '', + rdi : '', + rdiRain : '', + }, +] + +const chart_inflowRain : lineBarChartDataProps[] = []; +const chart_bodRain : lineBarChartDataProps[] = []; +const chart_inflTp : lineChartDataProps[] = []; +const chart_chongInTp : lineChartDataProps[] = []; +const chart_efflTp : lineChartDataProps[] = []; +const chart_inflTn : lineChartDataProps[] = []; +const chart_efflTn : lineChartDataProps[] = []; +const chart_inflBod : lineChartDataProps[] = []; +const chart_efflBod : lineChartDataProps[] = []; + +const rainManageList : RainManageListProps[] = []; + +const drainageEquipmentList : DrainageEquipmentListProps[] = [ + { + part1 : '청천시청', + part2 : '청천시청', + nowSum : 101, + nowJoin : 102, + nowSep : 103, + stepOneSum : 104, + stepOneJoin : 105, + stepOneSep : 106, + stepTwoSum : 107, + stepTwoJoin : 108, + stepTwoSep : 109, + stepThreeSum : 110, + stepThreeJoin : 111, + stepThreeSep : 112, + stepFourSum : 113, + stepFourJoin : 114, + stepFourSep : 115, + etc : '1050', + } +]; + +/**==================================== +* Store - api (데이터 호출) +====================================*/ +/* 시설정보 및 운영현황 - 계획 */ +const fetchFacilityPlan = async (targetYear : string | undefined, fcltyCd : string | undefined) => { + try { + targetYear = targetYear ? targetYear : "2023"; + const baseUrl = 'https://apigateway.rsonesoft.com/dc-service/api/v1/nowDmgMtncGnsvy'; + const queryParams = new URLSearchParams(); + + if (fcltyCd) queryParams.append('swtrmtFcltCd', fcltyCd); + //if (targetYear) queryParams.append('targetYear',targetYear); + const queryString = queryParams.toString(); + const url = queryString ? `${baseUrl}?${queryString}` : baseUrl; + const res = await getFetchAreaList(url); + return res + } catch (error) { + console.error('시설계획정보를 가져오는데 실패했습니다:', error); + return {data: [], count: 0}; + } +}; + +/* 시설정보 및 운영현황 - 운영실태 */ +const fetchOperationStatus = async (searchYear: string|undefined, fcltyCd: string|undefined) => { + try{ + const url = '/dashBoard/mngInfo/' + (searchYear?searchYear:'2023') + '/' + (fcltyCd?fcltyCd:''); + const res = await getFetch('fo', url); + return { data: res.data, count: res.count } + } catch (error) { + console.error('운영실태 데이터를 가져오는데 실패했습니다:', error); + return { data: [], count: 0 }; + } +} + +// 주요그래프 데이터 +const fetchMainGraph = async (searchYear: string|undefined, fcltyCd: string|undefined) => { + try { + const url = '/fclt/qlty/hist/' + (searchYear?searchYear:'2023') + '/' + (fcltyCd?fcltyCd:'11680PB001R'); + const res = await getFetch('fo', url); + return { data: res.data, count: res.count } + } catch (error) { + console.error('주요그래프 데이터를 가져오는데 실패했습니다:', error); + return { data: [], count: 0 }; + } +} + +// 배수설비 정비현황 +const fetchDrainageEquipment = async (searchYear: string|undefined, fcltyCd: string|undefined) => { + try{ + /* const url = '/fclt/qlty/hist/' + (searchYear?searchYear:'2024') + '/' + (fcltyCd?fcltyCd:'11680PB001R'); + const res = await getFetch('fo', url); + return { data: res.data, count: res.count } */ + console.log(searchYear, fcltyCd); + return { data : drainageEquipmentList, count : drainageEquipmentList.length } + } catch (error) { + console.error('배수설비 정비현황 데이터를 가져오는데 실패했습니다:', error); + return { data: [], count: 0 }; + } +} + +/**==================================== +* Store - action +====================================*/ +export type DashBoardActions = { + initializeDashBoardData(): Promise<void>; + /** 시설정보 및 운영현황 - 계획정보 */ + searchPlanInfo(targetYear: string, fcltyCd: string): Promise<void>; + /** 시설정보 및 운영현황 - 운영실태 */ + searchOperationStatus(isExcel: boolean, searchYear: string, fcltyCd: string): Promise<void>; + /** 주요그래프 데이터 */ + searchMainGraph(searchYear: string, fcltyCd: string): Promise<void>; + /**강우일 운영현황 */ + searchRainManage(searchYear: string, fcltyCd: string, isExcel: boolean): Promise<void>; + /**배수설비 정비현황 */ + searchDrainageEquipment(searchYear: string, fcltyCd: string, isExcel: boolean): Promise<void>; +} + +export type DashBoardType = { + DashboardMain: { + isInit: boolean; + topLabelArray:string[]; + }, + + FcltyMain : { + isInit : boolean; + upHeadListFclty : TableUpperProps[]; + fcltyList : mainFcltyProps[]; + }, + + PlanFclty : { + isInit : boolean; + planYearList : string[]; + upHeadListPlanFclty : TableUpperProps[]; + planFcltyList : planFcltyListProps[]; + }, + + OperationStatus : { + isInit : boolean; + upHeadListOperationStatus : TableUpperProps[]; + operationalStatusList : OperationalStatusListProps[]; + } + + MainGraph : { + isInit : boolean; + chart_inflowRain : lineBarChartDataProps[]; + chart_bodRain : lineBarChartDataProps[]; + chart_inflTp : lineChartDataProps[]; + chart_chongInTp : lineChartDataProps[]; + chart_efflTp : lineChartDataProps[]; + chart_inflTn : lineChartDataProps[]; + chart_efflTn : lineChartDataProps[]; + chart_inflBod : lineChartDataProps[]; + chart_efflBod : lineChartDataProps[]; + }, + + RainManage : { + isInit : boolean; + upHeadListRainManage : TableUpperProps[]; + rainManageList : RainManageListProps[]; + excelList : RainManageExcelType[]; + }, + + DrainageEquipment : { + isInit : boolean; + upHeadListDrainageEquipment : TableUpperProps[]; + drainageEquipmentList : DrainageEquipmentListProps[]; + drainageEquipmentExcelList : DrainageEquipmentExcelType[]; + } +} + +export type DashBoardStore = DashBoardType & DashBoardActions; + +export const dashBoardInitState:DashBoardType = { + DashboardMain: { + isInit: false, + topLabelArray: DashBoardTopLabelArray, + }, + + FcltyMain : { + isInit : false, + upHeadListFclty : upHeadListFclty, + fcltyList : fcltyList, + }, + + PlanFclty : { + isInit : true, + planYearList : planYearList, + upHeadListPlanFclty : upHeadListPlanFclty, + planFcltyList : planFcltyList + }, + + OperationStatus : { + isInit : false, + upHeadListOperationStatus : upHeadListOperationStatus, + operationalStatusList : operationalStatusList + }, + + MainGraph : { + isInit : false, + chart_inflowRain : chart_inflowRain, + chart_bodRain : chart_bodRain, + chart_inflTp : chart_inflTp, + chart_chongInTp : chart_chongInTp, + chart_efflTp : chart_efflTp, + chart_inflTn : chart_inflTn, + chart_efflTn : chart_efflTn, + chart_inflBod : chart_inflBod, + chart_efflBod : chart_efflBod + }, + + RainManage : { + isInit : false, + upHeadListRainManage : upHeadListRainManage, + rainManageList : rainManageList, + excelList : [] + }, + + DrainageEquipment : { + isInit : false, + upHeadListDrainageEquipment : upHeadListDrainageEquipment, + drainageEquipmentList : drainageEquipmentList, + drainageEquipmentExcelList : [] + } +} + +/** + * 데이터 폼 포맷설정 + */ +export const dataFormSettingPlan=(item : planFcltyListProps) => { + DataForm({ type: 'float2', column: 'provisionalApproval', item: item as unknown as Record<string, unknown> }); + DataForm({ type: 'float2', column: 'now', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float2', column: 'stepOne', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float2', column: 'stepTwo', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float2', column: 'stepThree', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float2', column: 'stepFour', item: item as unknown as Record<string, unknown>}); +} + +export const dataFormSetting=(item : OperationalStatusListProps)=> { + DataForm({ type: 'float2', column: 'yearAvg', item: item as unknown as Record<string, unknown> }); + DataForm({ type: 'float2', column: 'yearMax', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float2', column: 'yearMin', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float2', column: 'summerAll', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float2', column: 'summerThree', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float2', column: 'summerRain', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float2', column: 'winterAll', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float2', column: 'winterSunnyAvg', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float2', column: 'winterSunnyMax', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float2', column: 'winterSunnyMin', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float2', column: 'groundWaterAvg', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float2', column: 'groundWaterSummer', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float2', column: 'groundWaterWinter', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float2', column: 'rainRate', item: item as unknown as Record<string, unknown>}); +}; + +const changeValue=(operationalStatus, data, keyTxt = 'INFL_AMNT')=>{ + operationalStatus.yearAvg = data.yearData ? (keyTxt.includes('_TP') || keyTxt.includes("_TN"))? data.yearData?Number(Math.round(data.yearData['AVG_'+keyTxt] * 1000) / 1000).toFixed(3).toString():'': data.yearData['AVG_'+keyTxt] : ''; + operationalStatus.yearMax = data.yearData ? (keyTxt.includes('_TP') || keyTxt.includes("_TN"))? data.yearData?Number(Math.round(data.yearData['MAX_'+keyTxt] * 1000) / 1000).toFixed(3).toString():'': data.yearData['MAX_'+keyTxt] : ''; + operationalStatus.yearMin = data.yearData ? (keyTxt.includes('_TP') || keyTxt.includes("_TN"))? data.yearData?Number(Math.round(data.yearData['MIN_'+keyTxt] * 1000) / 1000).toFixed(3).toString():'': data.yearData['MIN_'+keyTxt] : ''; + // 여름 + operationalStatus.summerAll = data.summer ? (keyTxt.includes('_TP') || keyTxt.includes("_TN"))? data.summer?Number(Math.round(data.summer['AVG_'+keyTxt] * 1000) / 1000).toFixed(3).toString():'': data.summer['AVG_'+keyTxt] : ''; + operationalStatus.summerThree = data.summerRainy ? (keyTxt.includes('_TP') || keyTxt.includes("_TN"))? data.summerRainy?Number(Math.round(data.summerRainy["AVG_" + keyTxt] * 1000) / 1000).toFixed(3).toString():'': data.summerRainy["AVG_" + keyTxt] : ''; + operationalStatus.summerRain = data.summerSunny ? (keyTxt.includes('_TP') || keyTxt.includes("_TN"))? data.summerSunny?Number(Math.round(data.summerSunny["AVG_" + keyTxt] * 1000) / 1000).toFixed(3).toString():'': data.summerSunny["AVG_" + keyTxt] : ''; + // 겨울 + operationalStatus.winterAll = data.winter ? (keyTxt.includes('_TP') || keyTxt.includes("_TN"))? data.winter?Number(Math.round(data.winter["AVG_" + keyTxt] * 1000) / 1000).toFixed(3).toString():'': data.winter["AVG_" + keyTxt] : ''; + operationalStatus.winterSunnyMin = data.winterSunny ? (keyTxt.includes('_TP') || keyTxt.includes("_TN"))? data.winterSunny?Number(Math.round(data.winterSunny["MIN_" + keyTxt] * 1000) / 1000).toFixed(3).toString():'': data.winterSunny["MIN_" + keyTxt] : ''; + operationalStatus.winterSunnyAvg = data.winterSunny ? (keyTxt.includes('_TP') || keyTxt.includes("_TN"))? data.winterSunny?Number(Math.round(data.winterSunny["AVG_" + keyTxt] * 1000) / 1000).toFixed(3).toString():'': data.winterSunny["AVG_" + keyTxt] : ''; + operationalStatus.winterSunnyMax = data.winterSunny ? (keyTxt.includes('_TP') || keyTxt.includes("_TN"))? data.winterSunny?Number(Math.round(data.winterSunny["MAX_" + keyTxt] * 1000) / 1000).toFixed(3).toString():'': data.winterSunny["MAX_" + keyTxt] : ''; + // 지하수 + operationalStatus.groundWaterAvg = 10; + operationalStatus.groundWaterSummer = 11; + operationalStatus.groundWaterWinter = 12; + // 오염물질 + // RDII 추정 = 하절기(6~8월) 중 최저 유입 BOD일때 유입유량 x ((동절기 청천시 유입BOD - 하절기(6~8월) 중 최저 유입BOD) / (동절기 청천시 유입BOD)) + const summerLowFlow = data.summerBod && data.summerBod.INFL_AMNT && data.summerBod.INFL_AMNT != 'null' ? Number(data.summerBod.INFL_AMNT) : 0; + const winterAvgBod = data.winterSunny && data.winterSunny.AVG_INFL_ASSCT_BFR_WQLT_BOD && data.winterSunny.AVG_INFL_ASSCT_BFR_WQLT_BOD != 'null' ? Number(data.winterSunny.AVG_INFL_ASSCT_BFR_WQLT_BOD) : 0; + const summerLowBod = data.summerBod && data.summerBod.MIN_INFL_ASSCT_BFR_WQLT_BOD && data.summerBod.MIN_INFL_ASSCT_BFR_WQLT_BOD != 'null' ? Number(data.summerBod.MIN_INFL_ASSCT_BFR_WQLT_BOD) : 0; + const summerLowRain = data.summerBod && data.summerBod.INFO_RN_DAY && data.summerBod.INFO_RN_DAY != 'null' ? Number(data.summerBod.INFO_RN_DAY) : 0; + operationalStatus.rdi = winterAvgBod === 0 ? 0 : summerLowFlow * ((winterAvgBod - summerLowBod) / winterAvgBod); + operationalStatus.rdiRain = summerLowRain; +} + +const calcValue = (operationalStatus, data, keyTxt='INFL_ASSCT_BFR_WQLT_TP') => { + // 연간 + operationalStatus.yearAvg = data.yearData ? Number(Math.round(data.yearData['AVG_'+keyTxt] * 1000) / 1000).toFixed(3).toString() : ''; + operationalStatus.yearMax = data.yearData ? Number(Math.round(data.yearData.MAX_INFL_ASSCT_BFR_WQLT_TP * 1000) / 1000).toFixed(3).toString() : ''; + operationalStatus.yearMin = data.yearData ? Number(Math.round(data.yearData.MIN_INFL_ASSCT_BFR_WQLT_TP * 1000) / 1000).toFixed(3).toString() : ''; + // 여름 + operationalStatus.summerAll = data.summer ? Number(Math.round(data.summer.AVG_INFL_ASSCT_BFR_WQLT_TP * 1000) / 1000).toFixed(3).toString() : ''; + operationalStatus.summerThree = data.summerRainy ? Number(Math.round(data.summerRainy.AVG_INFL_ASSCT_BFR_WQLT_TP * 1000) / 1000).toFixed(3).toString() : ''; + operationalStatus.summerRain = data.summerSunny ? Number(Math.round(data.summerSunny.AVG_INFL_ASSCT_BFR_WQLT_TP * 1000) / 1000).toFixed(3).toString() : ''; + // 겨울 + operationalStatus.winterAll = data.winter ? Number(Math.round(data.winter.AVG_INFL_ASSCT_BFR_WQLT_TP * 1000) / 1000).toFixed(3).toString() : ''; + operationalStatus.winterSunnyMin = data.winterSunny ? Number(Math.round(data.winterSunny.MIN_INFL_ASSCT_BFR_WQLT_TP * 1000) / 1000).toFixed(3).toString() : ''; + operationalStatus.winterSunnyAvg = data.winterSunny ? Number(Math.round(data.winterSunny.AVG_INFL_ASSCT_BFR_WQLT_TP * 1000) / 1000).toFixed(3).toString() : ''; + operationalStatus.winterSunnyMax = data.winterSunny ? Number(Math.round(data.winterSunny.MAX_INFL_ASSCT_BFR_WQLT_TP * 1000) / 1000).toFixed(3).toString() : ''; +} + +export const dashBoardReducer:(set:(fn: (state: DashBoardType) => DashBoardType) => void)=>DashBoardActions=(set: (fn: (state: DashBoardType) => DashBoardType) => void) => { + return { + initializeDashBoardData: async () => { + set((state: DashBoardType) => ({ + ...state, + DashboardMain: { + ...state.DashboardMain, + isInit: true, + } + })); + }, + + /** 시설정보 및 운영현황 - 계획정보 */ + searchPlanInfo : async (targetYear: string, fcltyCd: string) => { + const res = await fetchFacilityPlan(targetYear, fcltyCd); + let minDiff=9999999; + let minIndex; + res.forEach((el:any, idx) => { + if(minDiff>Math.abs(Number(targetYear) - Number(el.yr))){ + minDiff=Math.abs(Number(targetYear) - Number(el.yr)); + minIndex=idx; + } + }); + + const rtnPlanData = res[minIndex]; + + planFcltyList.map((el, index) => { + planYearList = []; + planYearList.push(rtnPlanData?rtnPlanData.yrDt4.toString():""); + planYearList.push(rtnPlanData?rtnPlanData.yrDc1.toString():""); + planYearList.push(rtnPlanData?rtnPlanData.yrDc2.toString():""); + planYearList.push(rtnPlanData?rtnPlanData.yrDc3.toString():""); + planYearList.push(rtnPlanData?rtnPlanData.yrDc4.toString():""); + + if(index === 0){ + el.provisionalApproval = rtnPlanData?rtnPlanData.planSwageQntSumDt4:''; + el.now = rtnPlanData?rtnPlanData.planSwageQntSum:''; + el.stepOne = rtnPlanData?rtnPlanData.planSwageQntSumDc1:''; + el.stepTwo = rtnPlanData?rtnPlanData.planSwageQntSumDc2:''; + el.stepThree = rtnPlanData?rtnPlanData.planSwageQntSumDc3:''; + el.stepFour = rtnPlanData?rtnPlanData.planSwageQntSumDc4:''; + } + if(index === 1){ + el.provisionalApproval = rtnPlanData?rtnPlanData.swtrmtPpltnCntDt4:''; + el.now = rtnPlanData?rtnPlanData.swtrmtPpltnCnt:''; + el.stepOne = rtnPlanData?rtnPlanData.swtrmtPpltnCntDc1:''; + el.stepTwo = rtnPlanData?rtnPlanData.swtrmtPpltnCntDc2:''; + el.stepThree = rtnPlanData?rtnPlanData.swtrmtPpltnCntDc3:''; + el.stepFour = rtnPlanData?rtnPlanData.swtrmtPpltnCntDc4:''; + } + if(index === 2){ + el.provisionalApproval = rtnPlanData?rtnPlanData.swtrmtZoneAreaDt4:''; + el.now = rtnPlanData?rtnPlanData.swtrmtZoneArea:''; + el.stepOne = rtnPlanData?rtnPlanData.swtrmtZoneAreaDc1:''; + el.stepTwo = rtnPlanData?rtnPlanData.swtrmtZoneAreaDc2:''; + el.stepThree = rtnPlanData?rtnPlanData.swtrmtZoneAreaDc3:''; + el.stepFour = rtnPlanData?rtnPlanData.swtrmtZoneAreaDc4:''; + } + if(index === 4){ + el.provisionalApproval = rtnPlanData?rtnPlanData.ugwtUseSum:''; + } + + // 임시시 + dataFormSettingPlan(el); + }); + + set((state: DashBoardType) => ({ + ...state, + PlanFclty : { + ...state.PlanFclty, + planFcltyList : planFcltyList + } + })); + }, + + /** 시설정보 및 운영현황 - 운영실태 */ + searchOperationStatus: async (isExcel: boolean, searchYear: string, fcltyCd: string) => { + const res = await fetchOperationStatus(searchYear, fcltyCd); + const data = res.data; + if(data && operationalStatusList){ + // 유량 - 유입 + if (operationalStatusList?.[0]) { + // 연간 + const yearAvg = data.yearData===null?'':(data.yearData.AVG_INFL_AMNT?data.yearData.AVG_INFL_AMNT:0)+(data.yearData.AVG_SUM_FLOW?data.yearData.AVG_SUM_FLOW:0)+(data.yearData.AVG_RETURN_AMNT?data.yearData.AVG_RETURN_AMNT:0); + const yearMax = data.yearData===null?'':(data.yearData.MAX_INFL_AMNT?data.yearData.MAX_INFL_AMNT:0)+(data.yearData.MAX_SUM_FLOW?data.yearData.MAX_SUM_FLOW:0)+(data.yearData.MAX_RETURN_AMNT?data.yearData.MAX_RETURN_AMNT:0); + const yearMin = data.yearData===null?'':(data.yearData.MIN_INFL_AMNT?data.yearData.MIN_INFL_AMNT:0)+(data.yearData.MIN_SUM_FLOW?data.yearData.MIN_SUM_FLOW:0)+(data.yearData.MIN_RETURN_AMNT?data.yearData.MIN_RETURN_AMNT:0); + operationalStatusList[0].yearAvg = yearAvg; + operationalStatusList[0].yearMax = yearMax; + operationalStatusList[0].yearMin = yearMin; + // 여름 + operationalStatusList[0].summerAll = data.summer === null ? '' : ( + (data.summer.AVG_INFL_AMNT ? data.summer.AVG_INFL_AMNT : 0) + + (data.summer.AVG_SUM_FLOW ? data.summer.AVG_SUM_FLOW : 0) + + (data.summer.AVG_TP_INFL_WQLT_TP ? data.summer.AVG_TP_INFL_WQLT_TP : 0) + ); + operationalStatusList[0].summerThree = data.summerRainy === null ? '' : ( + (data.summerRainy.AVG_INFL_AMNT ? data.summerRainy.AVG_INFL_AMNT : 0) + + (data.summerRainy.AVG_SUM_FLOW ? data.summerRainy.AVG_SUM_FLOW : 0) + + (data.summerRainy.AVG_TP_INFL_WQLT_TP ? data.summerRainy.AVG_TP_INFL_WQLT_TP : 0) + ); + operationalStatusList[0].summerRain = data.summerSunny === null ? '' : ( + (data.summerSunny.AVG_INFL_AMNT ? data.summerSunny.AVG_INFL_AMNT : 0) + + (data.summerSunny.AVG_SUM_FLOW ? data.summerSunny.AVG_SUM_FLOW : 0) + + (data.summerSunny.AVG_TP_INFL_WQLT_TP ? data.summerSunny.AVG_TP_INFL_WQLT_TP : 0) + ); + // 겨울 + operationalStatusList[0].winterAll = data.winter === null ? '' : (data.winter.AVG_INFL_AMNT ? data.winter.AVG_INFL_AMNT : 0) + (data.winter.AVG_SUM_FLOW ? data.winter.AVG_SUM_FLOW : 0) + (data.winter.AVG_TP_INFL_WQLT_TP ? data.winter.AVG_TP_INFL_WQLT_TP : 0); + operationalStatusList[0].winterSunnyMin = data.winterSunny === null ? '' : (data.winterSunny.MIN_INFL_AMNT ? data.winterSunny.MIN_INFL_AMNT : 0) + (data.winterSunny.MIN_SUM_FLOW ? data.winterSunny.MIN_SUM_FLOW : 0) + (data.winterSunny.MIN_TP_INFL_WQLT_TP ? data.winterSunny.MIN_TP_INFL_WQLT_TP : 0); + operationalStatusList[0].winterSunnyAvg = data.winterSunny === null ? '' : (data.winterSunny.AVG_INFL_AMNT ? data.winterSunny.AVG_INFL_AMNT : 0) + (data.winterSunny.AVG_SUM_FLOW ? data.winterSunny.AVG_SUM_FLOW : 0) + (data.winterSunny.AVG_TP_INFL_WQLT_TP ? data.winterSunny.AVG_TP_INFL_WQLT_TP : 0); + operationalStatusList[0].winterSunnyMax = data.winterSunny === null ? '' : (data.winterSunny.MAX_INFL_AMNT ? data.winterSunny.MAX_INFL_AMNT : 0) + (data.winterSunny.MAX_SUM_FLOW ? data.winterSunny.MAX_SUM_FLOW : 0) + (data.winterSunny.MAX_TP_INFL_WQLT_TP ? data.winterSunny.MAX_TP_INFL_WQLT_TP : 0); + // 지하수 + operationalStatusList[0].groundWaterAvg = data.yearData.AVG_INFL_AMNT === null ? '' : Number(data.yearData.AVG_INFL_AMNT) - (Number(data.yearData.AVG_INFL_AMNT)*0.65*0.8); + operationalStatusList[0].groundWaterSummer = data.summer.AVG_INFL_AMNT === null ? '' : Number(data.summer.AVG_INFL_AMNT) - (Number(data.summer.AVG_INFL_AMNT)*0.65*0.8); + operationalStatusList[0].groundWaterWinter = data.winter.AVG_INFL_AMNT === null ? '' : Number(data.winter.AVG_INFL_AMNT) - (Number(data.winter.AVG_INFL_AMNT)*0.65*0.8); + + // 오염물질 + // RDII 추정 = 하절기(6~8월) 중 최저 유입 BOD일때 유입유량 x ((동절기 청천시 유입BOD - 하절기(6~8월) 중 최저 유입BOD) / (동절기 청천시 유입BOD)) + const summerLowFlow = data.summerBod&&data.summerBod.INFL_AMNT&&data.summerBod.INFL_AMNT!='null'?Number(data.summerBod.INFL_AMNT):0; + const winterAvgBod = data.winterSunny&&data.winterSunny.AVG_INFL_ASSCT_BFR_WQLT_BOD&&data.winterSunny.AVG_INFL_ASSCT_BFR_WQLT_BOD!='null'?Number(data.winterSunny.AVG_INFL_ASSCT_BFR_WQLT_BOD):0; + const summerLowBod = data.summerBod&&data.summerBod.MIN_INFL_ASSCT_BFR_WQLT_BOD&&data.summerBod.MIN_INFL_ASSCT_BFR_WQLT_BOD!='null'?Number(data.summerBod.MIN_INFL_ASSCT_BFR_WQLT_BOD):0; + const summerLowRain = data.summerBod&&data.summerBod.INFO_RN_DAY&&data.summerBod.INFO_RN_DAY!='null'?Number(data.summerBod.INFO_RN_DAY):0; + operationalStatusList[0].rdi = winterAvgBod===0?0:summerLowFlow * ((winterAvgBod - summerLowBod) / winterAvgBod); + operationalStatusList[0].rdiRain = summerLowRain; + dataFormSetting(operationalStatusList[0]); + } + + if(operationalStatusList[0] && operationalStatusList[7]){ + console.log(Number(operationalStatusList[0].groundWaterAvg)+' '+Number(data.yearData.AVG_INFL_AMNT)); + console.log(Number(operationalStatusList[0].groundWaterSummer)+' '+Number(data.summer.AVG_INFL_AMNT)); + console.log(Number(operationalStatusList[0].groundWaterWinter)+' '+Number(data.winter.AVG_INFL_AMNT)); + operationalStatusList[7].groundWaterAvg = Number(operationalStatusList[0].groundWaterAvg) / Number(data.yearData.AVG_INFL_AMNT) * 100; + operationalStatusList[7].groundWaterSummer = Number(operationalStatusList[0].groundWaterSummer) / Number(data.summer.AVG_INFL_AMNT) * 100; + operationalStatusList[7].groundWaterWinter = Number(operationalStatusList[0].groundWaterWinter) / Number(data.winter.AVG_INFL_AMNT) * 100; + /* operationalStatusList[7].groundWaterAvg = operationalStatusList[0].groundWaterAvg === null ? '' : Number(Number(operationalStatusList[0].groundWaterAvg) / Number(data.yearData.AVG_INFL_AMNT)) * 100; + operationalStatusList[7].groundWaterSummer = operationalStatusList[0].groundWaterSummer === null ? '' : Number(Number(operationalStatusList[0].groundWaterSummer) / Number(data.summer.AVG_INFL_AMNT)) * 100; + operationalStatusList[7].groundWaterWinter = operationalStatusList[0].groundWaterWinter === null ? '' : Number(Number(operationalStatusList[0].groundWaterWinter) / Number(data.winter.AVG_INFL_AMNT)) * 100;*/ + dataFormSetting(operationalStatusList[0]); + } + + for(let i=1; i<operationalStatusList.length; i++){ + const txtArray = ['1', 'INFL_AMNT', 'SUM_FLOW', 'RETURN_AMNT', 'EFFL_AMNT', 'INFL_ASSCT_BFR_WQLT_BOD', 'EFFL_WQLT_BOD', 'INFL_ASSCT_BFR_WQLT_TN', 'EFFL_WQLT_TN', 'INFL_ASSCT_BFR_WQLT_TP', 'EFFL_WQLT_TP', 'INFL_ASSCT_BFR_WQLT_TP', 'EFFL_WQLT_TP']; + if(operationalStatusList[i]){ + if(i < 7){ + changeValue(operationalStatusList[i], data, txtArray[i]); + dataFormSetting(operationalStatusList[i]!); + }else{ + calcValue(operationalStatusList[i], data, txtArray[i]); + } + } + } + } + //const excelDownList: RainManageExcelType[] = []; + /* if(isExcel){ + res.data.forEach(item => { + dataFormSetting(item) + item.fctCpc = parseFloat(item.fctCpc.toString().replace(/,/g, '')); + item.sidoNm = item.lclgvrnmntNm.split(' ')[0]?.toString() || ''; + item.sggNm = item.lclgvrnmntNm.split(' ')[1]?.toString() || ''; + excelDownList.push({ + 시설코드: item.bscFctCd, + 시설이름: item.bscFctNm, + 측정일자: item.msrDt, + 시도: item.sidoNm, + 시군구: item.sggNm, + 주소: item.addr, + 용량: item.fctCpc, + 유입BOD: item.inflAssctAftWqltBod, + 유입TN: item.inflAssctAftWqltTn, + 유입TP: item.inflAssctBfrWqltTp, + 유출BOD: item.efflWqltBod, + 유출TN: item.efflWqltTn, + 유출TP: item.efflWqltTp, + 환경청: item.rgnNm, + 유역: item.rvbsn, + 대유역: item.lgbsn, + 중유역: item.mdmbsn, + }); + }); + }else{ */ + /* data.forEach((item: OperationalStatusListProps) => { + }); */ + /* } */ + set((state: DashBoardType) => ({ + ...state, + OperationStatus: { + ...state.OperationStatus, + operationalStatusList: operationalStatusList, + operationalStatusCount: res.count + } + })) + }, + + searchMainGraph: async (searchYear: string, fcltyCd: string) => { + const res = await fetchMainGraph(searchYear, fcltyCd); + if(res == null || res.data == null || res.data.length === 0) { + set((state: DashBoardType) => ({ + ...state, + MainGraph: { + ...state.MainGraph, + chart_inflowRain: [], + chart_bodRain: [], + chart_inflTp: [], + chart_chongInTp: [], + chart_efflTp: [], + chart_inflTn: [], + chart_efflTn: [], + chart_inflBod: [], + chart_efflBod: [], + } + })); + return; + } + + const chart_inflowRain = res.data.wqltObjs.map((item: FacilityWaterQltyProps) => ({ + label: item.msrDt.substring(0, 4)+"-"+item.msrDt.substring(4, 6)+"-"+item.msrDt.substring(6, 8), + barVal0: item.infoRnDay ?? 0, + lineVal0: item.fctCpc ?? 0, + lineVal1: item.inflAmnt ?? 0, + })); + + const chart_bodRain = res.data.wqltObjs.map((item: FacilityWaterQltyProps) => ({ + label: item.msrDt.substring(0, 4)+"-"+item.msrDt.substring(4, 6)+"-"+item.msrDt.substring(6, 8), + barVal0: item.infoRnDay ?? 0, + lineVal0: item.inflAssctAftWqltBod ?? 0, + })); + + const chart_inflTp = res.data.wqltObjs.map((item: FacilityWaterQltyProps) => ({ + label: item.msrDt.substring(0, 4)+"-"+item.msrDt.substring(4, 6)+"-"+item.msrDt.substring(6, 8), + val : item.inflAssctBfrWqltTp ?? 0 + })); + + const chart_chongInTp = res.data.wqltObjs.map((item: FacilityWaterQltyProps) => ({ + label: item.msrDt.substring(0, 4)+"-"+item.msrDt.substring(4, 6)+"-"+item.msrDt.substring(6, 8), + val : item.tpInflWqltTp ?? 0 + })); + + const chart_efflTp = res.data.wqltObjs.map((item: FacilityWaterQltyProps) => ({ + label: item.msrDt.substring(0, 4)+"-"+item.msrDt.substring(4, 6)+"-"+item.msrDt.substring(6, 8), + val : item.efflWqltTp ?? 0 + })); + + const chart_inflTn = res.data.wqltObjs.map((item: FacilityWaterQltyProps) => ({ + label: item.msrDt.substring(0, 4)+"-"+item.msrDt.substring(4, 6)+"-"+item.msrDt.substring(6, 8), + val : item.inflAssctAftWqltTn ?? 0 + })); + + const chart_efflTn = res.data.wqltObjs.map((item: FacilityWaterQltyProps) => ({ + label: item.msrDt.substring(0, 4)+"-"+item.msrDt.substring(4, 6)+"-"+item.msrDt.substring(6, 8), + val : item.efflWqltTn ?? 0 + })); + + const chart_inflBod = res.data.wqltObjs.map((item: FacilityWaterQltyProps) => ({ + label: item.msrDt.substring(0, 4)+"-"+item.msrDt.substring(4, 6)+"-"+item.msrDt.substring(6, 8), + val : item.inflAssctAftWqltBod ?? 0 + })); + + const chart_efflBod = res.data.wqltObjs.map((item: FacilityWaterQltyProps) => ({ + label: item.msrDt.substring(0, 4)+"-"+item.msrDt.substring(4, 6)+"-"+item.msrDt.substring(6, 8), + val : item.efflWqltBod ?? 0 + })); + + set((state: DashBoardType) => ({ + ...state, + MainGraph: { + ...state.MainGraph, + chart_inflowRain: chart_inflowRain, + chart_bodRain: chart_bodRain, + chart_inflTp: chart_inflTp, + chart_chongInTp : chart_chongInTp, + chart_efflTp: chart_efflTp, + chart_inflTn: chart_inflTn, + chart_efflTn: chart_efflTn, + chart_inflBod: chart_inflBod, + chart_efflBod: chart_efflBod + } + })) + }, + + searchRainManage: async (searchYear: string, fcltyCd: string, isExcel : boolean) => { + const res = await fetchMainGraph(searchYear, fcltyCd); + if(res == null || res.data.wqltObjs == null || res.data.wqltObjs.length === 0) { + set((state: DashBoardType) => ({ + ...state, + RainManage: { + ...state.RainManage, + rainManageList: [], + } + })); + return; + } + const fcltyCpcValue = res.data.wqltObjs[0].fctCpc; + + const itemList = ['계', '3~10mm 미만', '10~20mm 미만', '20~30mm 미만', '30~40mm 미만', '40~50mm 미만', '50mm~60mm 미만', '60mm 이상']; + const rainRate = ['0', '0', '0', '0', '0', '0', '0', '0']; + const rainDate = [0, 0, 0, 0, 0, 0, 0, 0]; + const inflAmtCnt = [0, 0, 0, 0, 0, 0, 0, 0]; + const inflAmnt = [0, 0, 0, 0, 0, 0, 0, 0]; + const fcltyCpct = [0, 0, 0, 0, 0, 0, 0, 0]; + const dsInflWqltBod: number[] = [0, 0, 0, 0, 0, 0, 0, 0]; + const dsInflWqltCod: number[] = [0, 0, 0, 0, 0, 0, 0, 0]; + const dsInflWqltSs: number[] = [0, 0, 0, 0, 0, 0, 0, 0]; + const dsInflWqltTn: number[] = [0, 0, 0, 0, 0, 0, 0, 0]; + const dsInflWqltTp: number[] = [0, 0, 0, 0, 0, 0, 0, 0]; + const inflAssctAftWqltBodCnt = [0, 0, 0, 0, 0, 0, 0, 0]; + const inflAssctAftWqltBod = [0, 0, 0, 0, 0, 0, 0, 0]; + const inflAssctAftWqltTocCnt = [0, 0, 0, 0, 0, 0, 0, 0]; + const inflAssctAftWqltToc = [0, 0, 0, 0, 0, 0, 0, 0]; + const inflAssctAftWqltSsCnt = [0, 0, 0, 0, 0, 0, 0, 0]; + const inflAssctAftWqltSs = [0, 0, 0, 0, 0, 0, 0, 0]; + const inflAssctAftWqltTnCnt = [0, 0, 0, 0, 0, 0, 0, 0]; + const inflAssctAftWqltTn = [0, 0, 0, 0, 0, 0, 0, 0]; + const inflAssctAftWqltTpCnt = [0, 0, 0, 0, 0, 0, 0, 0]; + const inflAssctAftWqltTp = [0, 0, 0, 0, 0, 0, 0, 0]; + const efflWqltBodCnt = [0, 0, 0, 0, 0, 0, 0, 0]; + const efflWqltBod = [0, 0, 0, 0, 0, 0, 0, 0]; + const efflWqltCodCnt = [0, 0, 0, 0, 0, 0, 0, 0]; + const efflWqltCod = [0, 0, 0, 0, 0, 0, 0, 0]; + const efflWqltSsCnt = [0, 0, 0, 0, 0, 0, 0, 0]; + const efflWqltSs = [0, 0, 0, 0, 0, 0, 0, 0]; + const efflWqltTnCnt = [0, 0, 0, 0, 0, 0, 0, 0]; + const efflWqltTn = [0, 0, 0, 0, 0, 0, 0, 0]; + const efflWqltTpCnt = [0, 0, 0, 0, 0, 0, 0, 0]; + const efflWqltTp = [0, 0, 0, 0, 0, 0, 0, 0]; + // Process the data and create rain management list + res.data.wqltObjs.forEach((item: RainManageListProps) => { + + // 강우일 + /*if(Number(item.infoRnDay) < 3){ + rainDate[1] = Number(rainDate[1] || 0) + 1; + inflAmtCnt[1] = Number(inflAmtCnt[1] || 0) + 1; + inflAmnt[1] = Number(inflAmnt[1] || 0) + Number(item.inflAmnt || 0); + fcltyCpct[1] = 0; + inflAssctAftWqltBodCnt[1] = Number(inflAssctAftWqltBodCnt[1] || 0) + 1; + inflAssctAftWqltBod[1] = Number(inflAssctAftWqltBod[1] || 0) + Number(item.inflAssctAftWqltBod || 0); + inflAssctAftWqltTocCnt[1] = Number(inflAssctAftWqltTocCnt[1] || 0) + 1; + inflAssctAftWqltToc[1] = Number(inflAssctAftWqltToc[1] || 0) + Number(item.inflAssctAftWqltToc || 0); + inflAssctAftWqltSsCnt[1] = Number(inflAssctAftWqltSsCnt[1] || 0) + 1; + inflAssctAftWqltSs[1] = Number(inflAssctAftWqltSs[1] || 0) + Number(item.inflAssctAftWqltSs || 0); + inflAssctAftWqltTnCnt[1] = Number(inflAssctAftWqltTnCnt[1] || 0) + 1; + inflAssctAftWqltTn[1] = Number(inflAssctAftWqltTn[1] || 0) + Number(item.inflAssctAftWqltTn || 0); + inflAssctAftWqltTpCnt[1] = Number(inflAssctAftWqltTpCnt[1] || 0) + 1; + inflAssctAftWqltTp[1] = Number(inflAssctAftWqltTp[1] || 0) + Number(item.inflAssctAftWqltTp || 0); + efflWqltBodCnt[1] = Number(efflWqltBodCnt[1] || 0) + 1; + efflWqltBod[1] = Number(efflWqltBod[1] || 0) + Number(item.efflWqltBod || 0); + efflWqltCodCnt[1] = Number(efflWqltCodCnt[1] || 0) + 1; + efflWqltCod[1] = Number(efflWqltCod[1] || 0) + Number(item.efflWqltCod || 0); + efflWqltSsCnt[1] = Number(efflWqltSsCnt[1] || 0) + 1; + efflWqltSs[1] = Number(efflWqltSs[1] || 0) + Number(item.efflWqltSs || 0); + efflWqltTnCnt[1] = Number(efflWqltTnCnt[1] || 0) + 1; + efflWqltTn[1] = Number(efflWqltTn[1] || 0) + Number(item.efflWqltTn || 0); + efflWqltTpCnt[1] = Number(efflWqltTpCnt[1] || 0) + 1; + efflWqltTp[1] = Number(efflWqltTp[1] || 0) + Number(item.efflWqltTp || 0); + }*/ + if(Number(item.infoRnDay) >= 3 && Number(item.infoRnDay) < 10){ + rainDate[1] = Number(rainDate[1] || 0) + 1; + inflAmtCnt[1] = Number(inflAmtCnt[1] || 0) + 1; + inflAmnt[1] = Number(inflAmnt[1] || 0) + Number(item.inflAmnt || 0); + fcltyCpct[1] = 0; + inflAssctAftWqltBodCnt[1] = Number(inflAssctAftWqltBodCnt[1] || 0) + 1; + inflAssctAftWqltBod[1] = Number(inflAssctAftWqltBod[1] || 0) + Number(item.inflAssctAftWqltBod || 0); + inflAssctAftWqltTocCnt[1] = Number(inflAssctAftWqltTocCnt[1] || 0) + 1; + inflAssctAftWqltToc[1] = Number(inflAssctAftWqltToc[1] || 0) + Number(item.inflAssctAftWqltToc || 0); + inflAssctAftWqltSsCnt[1] = Number(inflAssctAftWqltSsCnt[1] || 0) + 1; + inflAssctAftWqltSs[1] = Number(inflAssctAftWqltSs[1] || 0) + Number(item.inflAssctAftWqltSs || 0); + inflAssctAftWqltTnCnt[1] = Number(inflAssctAftWqltTnCnt[1] || 0) + 1; + inflAssctAftWqltTn[1] = Number(inflAssctAftWqltTn[1] || 0) + Number(item.inflAssctAftWqltTn || 0); + inflAssctAftWqltTpCnt[1] = Number(inflAssctAftWqltTpCnt[1] || 0) + 1; + inflAssctAftWqltTp[1] = Number(inflAssctAftWqltTp[1] || 0) + Number(item.inflAssctAftWqltTp || 0); + efflWqltBodCnt[1] = Number(efflWqltBodCnt[1] || 0) + 1; + efflWqltBod[1] = Number(efflWqltBod[1] || 0) + Number(item.efflWqltBod || 0); + efflWqltCodCnt[1] = Number(efflWqltCodCnt[1] || 0) + 1; + efflWqltCod[1] = Number(efflWqltCod[1] || 0) + Number(item.efflWqltCod || 0); + efflWqltSsCnt[1] = Number(efflWqltSsCnt[1] || 0) + 1; + efflWqltSs[1] = Number(efflWqltSs[1] || 0) + Number(item.efflWqltSs || 0); + efflWqltTnCnt[1] = Number(efflWqltTnCnt[1] || 0) + 1; + efflWqltTn[1] = Number(efflWqltTn[1] || 0) + Number(item.efflWqltTn || 0); + efflWqltTpCnt[1] = Number(efflWqltTpCnt[1] || 0) + 1; + efflWqltTp[1] = Number(efflWqltTp[1] || 0) + Number(item.efflWqltTp || 0); + }else if(Number(item.infoRnDay) >= 10 && Number(item.infoRnDay) < 20){ + rainDate[2] = Number(rainDate[2] || 0) + 1; + inflAmtCnt[2] = Number(inflAmtCnt[2] || 0) + 1; + inflAmnt[2] = Number(inflAmnt[2] || 0) + Number(item.inflAmnt || 0); + fcltyCpct[2] = 0; + inflAssctAftWqltBodCnt[2] = Number(inflAssctAftWqltBodCnt[2] || 0) + 1; + inflAssctAftWqltBod[2] = Number(inflAssctAftWqltBod[2] || 0) + Number(item.inflAssctAftWqltBod || 0); + inflAssctAftWqltTocCnt[2] = Number(inflAssctAftWqltTocCnt[2] || 0) + 1; + inflAssctAftWqltToc[2] = Number(inflAssctAftWqltToc[2] || 0) + Number(item.inflAssctAftWqltToc || 0); + inflAssctAftWqltSsCnt[2] = Number(inflAssctAftWqltSsCnt[2] || 0) + 1; + inflAssctAftWqltSs[2] = Number(inflAssctAftWqltSs[2] || 0) + Number(item.inflAssctAftWqltSs || 0); + inflAssctAftWqltTnCnt[2] = Number(inflAssctAftWqltTnCnt[2] || 0) + 1; + inflAssctAftWqltTn[2] = Number(inflAssctAftWqltTn[2] || 0) + Number(item.inflAssctAftWqltTn || 0); + inflAssctAftWqltTpCnt[2] = Number(inflAssctAftWqltTpCnt[2] || 0) + 1; + inflAssctAftWqltTp[2] = Number(inflAssctAftWqltTp[2] || 0) + Number(item.inflAssctAftWqltTp || 0); + efflWqltBodCnt[2] = Number(efflWqltBodCnt[2] || 0) + 1; + efflWqltBod[2] = Number(efflWqltBod[2] || 0) + Number(item.efflWqltBod || 0); + efflWqltCodCnt[2] = Number(efflWqltCodCnt[2] || 0) + 1; + efflWqltCod[2] = Number(efflWqltCod[2] || 0) + Number(item.efflWqltCod || 0); + efflWqltSsCnt[2] = Number(efflWqltSsCnt[2] || 0) + 1; + efflWqltSs[2] = Number(efflWqltSs[2] || 0) + Number(item.efflWqltSs || 0); + efflWqltTnCnt[2] = Number(efflWqltTnCnt[2] || 0) + 1; + efflWqltTn[2] = Number(efflWqltTn[2] || 0) + Number(item.efflWqltTn || 0); + efflWqltTpCnt[2] = Number(efflWqltTpCnt[2] || 0) + 1; + efflWqltTp[2] = Number(efflWqltTp[2] || 0) + Number(item.efflWqltTp || 0); + }else if(Number(item.infoRnDay) >= 20 && Number(item.infoRnDay) < 30){ + rainDate[3] = Number(rainDate[3] || 0) + 1; + inflAmtCnt[3] = Number(inflAmtCnt[3] || 0) + 1; + inflAmnt[3] = Number(inflAmnt[3] || 0) + Number(item.inflAmnt || 0); + fcltyCpct[3] = 0; + inflAssctAftWqltBodCnt[3] = Number(inflAssctAftWqltBodCnt[3] || 0) + 1; + inflAssctAftWqltBod[3] = Number(inflAssctAftWqltBod[3] || 0) + Number(item.inflAssctAftWqltBod || 0); + inflAssctAftWqltTocCnt[3] = Number(inflAssctAftWqltTocCnt[3] || 0) + 1; + inflAssctAftWqltToc[3] = Number(inflAssctAftWqltToc[3] || 0) + Number(item.inflAssctAftWqltToc || 0); + inflAssctAftWqltSsCnt[3] = Number(inflAssctAftWqltSsCnt[3] || 0) + 1; + inflAssctAftWqltSs[3] = Number(inflAssctAftWqltSs[3] || 0) + Number(item.inflAssctAftWqltSs || 0); + inflAssctAftWqltTnCnt[3] = Number(inflAssctAftWqltTnCnt[3] || 0) + 1; + inflAssctAftWqltTn[3] = Number(inflAssctAftWqltTn[3] || 0) + Number(item.inflAssctAftWqltTn || 0); + inflAssctAftWqltTpCnt[3] = Number(inflAssctAftWqltTpCnt[3] || 0) + 1; + inflAssctAftWqltTp[3] = Number(inflAssctAftWqltTp[3] || 0) + Number(item.inflAssctAftWqltTp || 0); + efflWqltBodCnt[3] = Number(efflWqltBodCnt[3] || 0) + 1; + efflWqltBod[3] = Number(efflWqltBod[3] || 0) + Number(item.efflWqltBod || 0); + efflWqltCodCnt[3] = Number(efflWqltCodCnt[3] || 0) + 1; + efflWqltCod[3] = Number(efflWqltCod[3] || 0) + Number(item.efflWqltCod || 0); + efflWqltSsCnt[3] = Number(efflWqltSsCnt[3] || 0) + 1; + efflWqltSs[3] = Number(efflWqltSs[3] || 0) + Number(item.efflWqltSs || 0); + efflWqltTnCnt[3] = Number(efflWqltTnCnt[3] || 0) + 1; + efflWqltTn[3] = Number(efflWqltTn[3] || 0) + Number(item.efflWqltTn || 0); + efflWqltTpCnt[3] = Number(efflWqltTpCnt[3] || 0) + 1; + efflWqltTp[3] = Number(efflWqltTp[3] || 0) + Number(item.efflWqltTp || 0); + }else if(Number(item.infoRnDay) >= 30 && Number(item.infoRnDay) < 40){ + rainDate[4] = Number(rainDate[4] || 0) + 1; + inflAmtCnt[4] = Number(inflAmtCnt[4] || 0) + 1; + inflAmnt[4] = Number(inflAmnt[4] || 0) + Number(item.inflAmnt || 0); + fcltyCpct[4] = item.inflAmnt&&Number(item.inflAmnt) !== 0 ? Number(Math.round(Number(item.inflAmnt) / Number(item.fctCpc) * 10) / 10) : 0; + inflAssctAftWqltBodCnt[4] = Number(inflAssctAftWqltBodCnt[4] || 0) + 1; + inflAssctAftWqltBod[4] = Number(inflAssctAftWqltBod[4] || 0) + Number(item.inflAssctAftWqltBod || 0); + inflAssctAftWqltTocCnt[4] = Number(inflAssctAftWqltTocCnt[4] || 0) + 1; + inflAssctAftWqltToc[4] = Number(inflAssctAftWqltToc[4] || 0) + Number(item.inflAssctAftWqltToc || 0); + inflAssctAftWqltSsCnt[4] = Number(inflAssctAftWqltSsCnt[4] || 0) + 1; + inflAssctAftWqltSs[4] = Number(inflAssctAftWqltSs[4] || 0) + Number(item.inflAssctAftWqltSs || 0); + inflAssctAftWqltTnCnt[4] = Number(inflAssctAftWqltTnCnt[4] || 0) + 1; + inflAssctAftWqltTn[4] = Number(inflAssctAftWqltTn[5] || 0) + Number(item.inflAssctAftWqltTn || 0); + inflAssctAftWqltTpCnt[4] = Number(inflAssctAftWqltTpCnt[4] || 0) + 1; + inflAssctAftWqltTp[4] = Number(inflAssctAftWqltTp[4] || 0) + Number(item.inflAssctAftWqltTp || 0); + efflWqltBodCnt[4] = Number(efflWqltBodCnt[4] || 0) + 1; + efflWqltBod[4] = Number(efflWqltBod[4] || 0) + Number(item.efflWqltBod || 0); + efflWqltCodCnt[4] = Number(efflWqltCodCnt[4] || 0) + 1; + efflWqltCod[4] = Number(efflWqltCod[4] || 0) + Number(item.efflWqltCod || 0); + efflWqltSsCnt[4] = Number(efflWqltSsCnt[4] || 0) + 1; + efflWqltSs[4] = Number(efflWqltSs[4] || 0) + Number(item.efflWqltSs || 0); + efflWqltTnCnt[4] = Number(efflWqltTnCnt[5] || 0) + 1; + efflWqltTn[4] = Number(efflWqltTn[4] || 0) + Number(item.efflWqltTn || 0); + efflWqltTpCnt[4] = Number(efflWqltTpCnt[5] || 0) + 1; + efflWqltTp[4] = Number(efflWqltTp[4] || 0) + Number(item.efflWqltTp || 0); + }else if(Number(item.infoRnDay) >= 40 && Number(item.infoRnDay) < 50){ + rainDate[5] = Number(rainDate[5] || 0) + 1; + inflAmtCnt[5] = Number(inflAmtCnt[5] || 0) + 1; + inflAmnt[5] = Number(inflAmnt[5] || 0) + Number(item.inflAmnt || 0); + fcltyCpct[5] = 0; + inflAssctAftWqltBodCnt[5] = Number(inflAssctAftWqltBodCnt[5] || 0) + 1; + inflAssctAftWqltBod[5] = Number(inflAssctAftWqltBod[5] || 0) + Number(item.inflAssctAftWqltBod || 0); + inflAssctAftWqltTocCnt[5] = Number(inflAssctAftWqltTocCnt[5] || 0) + 1; + inflAssctAftWqltToc[5] = Number(inflAssctAftWqltToc[5] || 0) + Number(item.inflAssctAftWqltToc || 0); + inflAssctAftWqltSsCnt[5] = Number(inflAssctAftWqltSsCnt[5] || 0) + 1; + inflAssctAftWqltSs[5] = Number(inflAssctAftWqltSs[5] || 0) + Number(item.inflAssctAftWqltSs || 0); + inflAssctAftWqltTnCnt[5] = Number(inflAssctAftWqltTnCnt[5] || 0) + 1; + inflAssctAftWqltTn[5] = Number(inflAssctAftWqltTn[5] || 0) + Number(item.inflAssctAftWqltTn || 0); + inflAssctAftWqltTpCnt[5] = Number(inflAssctAftWqltTpCnt[5] || 0) + 1; + inflAssctAftWqltTp[5] = Number(inflAssctAftWqltTp[5] || 0) + Number(item.inflAssctAftWqltTp || 0); + efflWqltBodCnt[5] = Number(efflWqltBodCnt[5] || 0) + 1; + efflWqltBod[5] = Number(efflWqltBod[5] || 0) + Number(item.efflWqltBod || 0); + efflWqltCodCnt[5] = Number(efflWqltCodCnt[5] || 0) + 1; + efflWqltCod[5] = Number(efflWqltCod[5] || 0) + Number(item.efflWqltCod || 0); + efflWqltSsCnt[5] = Number(efflWqltSsCnt[5] || 0) + 1; + efflWqltSs[5] = Number(efflWqltSs[5] || 0) + Number(item.efflWqltSs || 0); + efflWqltTnCnt[5] = Number(efflWqltTnCnt[5] || 0) + 1; + efflWqltTn[5] = Number(efflWqltTn[5] || 0) + Number(item.efflWqltTn || 0); + efflWqltTpCnt[5] = Number(efflWqltTpCnt[5] || 0) + 1; + efflWqltTp[5] = Number(efflWqltTp[5] || 0) + Number(item.efflWqltTp || 0); + }else if(Number(item.infoRnDay) >= 50 && Number(item.infoRnDay) < 60){ + rainDate[6] = Number(rainDate[6] || 0) + 1; + inflAmtCnt[6] = Number(inflAmtCnt[6] || 0) + 1; + inflAmnt[6] = Number(inflAmnt[6] || 0) + Number(item.inflAmnt || 0); + fcltyCpct[6] = 0; + inflAssctAftWqltBodCnt[6] = Number(inflAssctAftWqltBodCnt[6] || 0) + 1; + inflAssctAftWqltBod[6] = Number(inflAssctAftWqltBod[6] || 0) + Number(item.inflAssctAftWqltBod || 0); + inflAssctAftWqltTocCnt[6] = Number(inflAssctAftWqltTocCnt[6] || 0) + 1; + inflAssctAftWqltToc[6] = Number(inflAssctAftWqltToc[6] || 0) + Number(item.inflAssctAftWqltToc || 0); + inflAssctAftWqltSsCnt[6] = Number(inflAssctAftWqltSsCnt[6] || 0) + 1; + inflAssctAftWqltSs[6] = Number(inflAssctAftWqltSs[6] || 0) + Number(item.inflAssctAftWqltSs || 0); + inflAssctAftWqltTnCnt[6] = Number(inflAssctAftWqltTnCnt[6] || 0) + 1; + inflAssctAftWqltTn[6] = Number(inflAssctAftWqltTn[6] || 0) + Number(item.inflAssctAftWqltTn || 0); + inflAssctAftWqltTpCnt[6] = Number(inflAssctAftWqltTpCnt[6] || 0) + 1; + inflAssctAftWqltTp[6] = Number(inflAssctAftWqltTp[6] || 0) + Number(item.inflAssctAftWqltTp || 0); + efflWqltBodCnt[6] = Number(efflWqltBodCnt[6] || 0) + 1; + efflWqltBod[6] = Number(efflWqltBod[6] || 0) + Number(item.efflWqltBod || 0); + efflWqltCodCnt[6] = Number(efflWqltCodCnt[6] || 0) + 1; + efflWqltCod[6] = Number(efflWqltCod[6] || 0) + Number(item.efflWqltCod || 0); + efflWqltSsCnt[6] = Number(efflWqltSsCnt[6] || 0) + 1; + efflWqltSs[6] = Number(efflWqltSs[6] || 0) + Number(item.efflWqltSs || 0); + efflWqltTnCnt[6] = Number(efflWqltTnCnt[6] || 0) + 1; + efflWqltTn[6] = Number(efflWqltTn[6] || 0) + Number(item.efflWqltTn || 0); + efflWqltTpCnt[6] = Number(efflWqltTpCnt[6] || 0) + 1; + efflWqltTp[6] = Number(efflWqltTp[6] || 0) + Number(item.efflWqltTp || 0); + }else if(Number(item.infoRnDay) >= 60){ + rainDate[7] = Number(rainDate[7] || 0) + 1; + inflAmtCnt[7] = Number(inflAmtCnt[7] || 0) + 1; + inflAmnt[7] = Number(inflAmnt[7] || 0) + Number(item.inflAmnt || 0); + fcltyCpct[7] = 0; + inflAssctAftWqltBodCnt[7] = Number(inflAssctAftWqltBodCnt[7] || 0) + 1; + inflAssctAftWqltBod[7] = Number(inflAssctAftWqltBod[7] || 0) + Number(item.inflAssctAftWqltBod || 0); + inflAssctAftWqltTocCnt[7] = Number(inflAssctAftWqltTocCnt[7] || 0) + 1; + inflAssctAftWqltToc[7] = Number(inflAssctAftWqltToc[7] || 0) + Number(item.inflAssctAftWqltToc || 0); + inflAssctAftWqltSsCnt[7] = Number(inflAssctAftWqltSsCnt[7] || 0) + 1; + inflAssctAftWqltSs[7] = Number(inflAssctAftWqltSs[7] || 0) + Number(item.inflAssctAftWqltSs || 0); + inflAssctAftWqltTnCnt[7] = Number(inflAssctAftWqltTnCnt[7] || 0) + 1; + inflAssctAftWqltTn[7] = Number(inflAssctAftWqltTn[7] || 0) + Number(item.inflAssctAftWqltTn || 0); + inflAssctAftWqltTpCnt[7] = Number(inflAssctAftWqltTpCnt[7] || 0) + 1; + inflAssctAftWqltTp[7] = Number(inflAssctAftWqltTp[7] || 0) + Number(item.inflAssctAftWqltTp || 0); + efflWqltBodCnt[7] = Number(efflWqltBodCnt[7] || 0) + 1; + efflWqltBod[7] = Number(efflWqltBod[7] || 0) + Number(item.efflWqltBod || 0); + efflWqltCodCnt[7] = Number(efflWqltCodCnt[7] || 0) + 1; + efflWqltCod[7] = Number(efflWqltCod[7] || 0) + Number(item.efflWqltCod || 0); + efflWqltSsCnt[7] = Number(efflWqltSsCnt[7] || 0) + 1; + efflWqltSs[7] = Number(efflWqltSs[7] || 0) + Number(item.efflWqltSs || 0); + efflWqltTnCnt[7] = Number(efflWqltTnCnt[7] || 0) + 1; + efflWqltTn[7] = Number(efflWqltTn[7] || 0) + Number(item.efflWqltTn || 0); + efflWqltTpCnt[7] = Number(efflWqltTpCnt[7] || 0) + 1; + efflWqltTp[7] = Number(efflWqltTp[7] || 0) + Number(item.efflWqltTp || 0); + } + }); + + // Process the data and create rain management list + if(res.data&&res.data.dsWqltObjs&&res.data.dsWqltObjs.length > 0){ + // 설계 유입수질 데이터 가져오기 - 0값이 있으면 이전 차수에서 가져오기 + const getDesignWaterQualityData = (stepIndex: number, maxPreviousSteps: number = 3) => { + for (let i = 0; i <= maxPreviousSteps && stepIndex - i >= 0; i++) { + const currentStep = res.data.dsWqltObjs[stepIndex - i]; + if (currentStep && + currentStep.dsInflWqltBod > 0 && + currentStep.dsInflWqltCod > 0 && + currentStep.dsInflWqltSs > 0 && + currentStep.dsInflWqltTn > 0 && + currentStep.dsInflWqltTp > 0) { + return { + dsInflWqltBod: currentStep.dsInflWqltBod, + dsInflWqltCod: currentStep.dsInflWqltCod, + dsInflWqltSs: currentStep.dsInflWqltSs, + dsInflWqltTn: currentStep.dsInflWqltTn, + dsInflWqltTp: currentStep.dsInflWqltTp + }; + } + } + // 모든 차수에서 유효한 데이터를 찾지 못한 경우 기본값 반환 + return { + dsInflWqltBod: 0, + dsInflWqltCod: 0, + dsInflWqltSs: 0, + dsInflWqltTn: 0, + dsInflWqltTp: 0 + }; + }; + + const lastStepIndex = res.data.dsWqltObjs.length - 1; + const designData = getDesignWaterQualityData(lastStepIndex); + + dsInflWqltBod[0] = designData.dsInflWqltBod; + dsInflWqltCod[0] = designData.dsInflWqltCod; + dsInflWqltSs[0] = designData.dsInflWqltSs; + dsInflWqltTn[0] = designData.dsInflWqltTn; + dsInflWqltTp[0] = designData.dsInflWqltTp; + } + + rainDate[0] = (Number(rainDate[1]) || 0) + (Number(rainDate[2]) || 0) + (Number(rainDate[3]) || 0) + + (Number(rainDate[4]) || 0) + (Number(rainDate[5]) || 0) + (Number(rainDate[6]) || 0) + + (Number(rainDate[7]) || 0); + + // 강수비율 + for(let idx=0; idx<rainDate.length; idx++){ + if(Number(rainDate[idx]) > 0){ + rainRate[0] = '100'; + rainRate[idx] = (Math.round(Number(rainDate[idx])/Number(rainDate[0])*100*100)/100).toString(); + } + } + + // 평균 및 합계 계산 + const calculateAverage = (value: number, count: number) => { + return count > 0 ? Number((value / count).toFixed(1)) : 0; + }; + + // 데이터 있는 항목만 계산 + let dataIsCount = 0; + for(let idx=0; idx<inflAmnt.length; idx++){ + if(Number(inflAmnt[idx]) > 0){ + dataIsCount++; + } + } + + for(let idx=0; idx<inflAmnt.length; idx++){ + inflAmnt[idx] = calculateAverage(Number(inflAmnt[idx]), Number(inflAmtCnt[idx])); + inflAssctAftWqltBod[idx] = calculateAverage(Number(inflAssctAftWqltBod[idx]), Number(inflAssctAftWqltBodCnt[idx])); + inflAssctAftWqltToc[idx] = calculateAverage(Number(inflAssctAftWqltToc[idx]), Number(inflAssctAftWqltTocCnt[idx])); + inflAssctAftWqltSs[idx] = calculateAverage(Number(inflAssctAftWqltSs[idx]), Number(inflAssctAftWqltSsCnt[idx])); + inflAssctAftWqltTn[idx] = calculateAverage(Number(inflAssctAftWqltTn[idx]), Number(inflAssctAftWqltTnCnt[idx])); + inflAssctAftWqltTp[idx] = calculateAverage(Number(inflAssctAftWqltTp[idx]), Number(inflAssctAftWqltTpCnt[idx])); + efflWqltBod[idx] = calculateAverage(Number(efflWqltBod[idx]), Number(efflWqltBodCnt[idx])); + efflWqltCod[idx] = calculateAverage(Number(efflWqltCod[idx]), Number(efflWqltCodCnt[idx])); + efflWqltSs[idx] = calculateAverage(Number(efflWqltSs[idx]), Number(efflWqltSsCnt[idx])); + efflWqltTn[idx] = calculateAverage(Number(efflWqltTn[idx]), Number(efflWqltTnCnt[idx])); + efflWqltTp[idx] = calculateAverage(Number(efflWqltTp[idx]), Number(efflWqltTpCnt[idx])); + } + + // 합계 계산 (index 0) + const calculateTotal = (arr: number[]) => { + return Number((arr.slice(1).reduce((sum, val) => sum + Number(val), 0)).toFixed(1)); + }; + + inflAmnt[0] = calculateAverage(calculateTotal(inflAmnt), dataIsCount); + fcltyCpct[0] = calculateTotal(fcltyCpct); + inflAssctAftWqltBod[0] = Number((calculateTotal(inflAssctAftWqltBod)/7).toFixed(1)); + inflAssctAftWqltToc[0] = Number((calculateTotal(inflAssctAftWqltToc)/7).toFixed(1)); + inflAssctAftWqltSs[0] = Number((calculateTotal(inflAssctAftWqltSs)/7).toFixed(1)); + inflAssctAftWqltTn[0] = Number((calculateTotal(inflAssctAftWqltTn)/7).toFixed(3)); + inflAssctAftWqltTp[0] = Number((calculateTotal(inflAssctAftWqltTp)/7).toFixed(3)); + efflWqltBod[0] = Number((calculateTotal(efflWqltBod)/7).toFixed(1)); + efflWqltCod[0] = Number((calculateTotal(efflWqltCod)/7).toFixed(1)); + efflWqltSs[0] = Number((calculateTotal(efflWqltSs as number[])/7).toFixed(1)); + efflWqltTn[0] = Number((calculateTotal(efflWqltTn as number[])/7).toFixed(3)); + efflWqltTp[0] = Number((calculateTotal(efflWqltTp as number[])/7).toFixed(3)); + + // 최종 리스트 + const filteredData = itemList.map((item, index) => ({ + rainItem: item, + rainRate: rainRate[index] || '0', + rainDay: rainDate[index] ? Number(rainDate[index].toFixed(1)) : '', + inflAmnt: inflAmnt[index] ? Number(inflAmnt[index].toFixed(1)) : '', + fctCpc: inflAmnt[index] ? Number((inflAmnt[index] / fcltyCpcValue).toFixed(1)) : '', + dsInflWqltBod: dsInflWqltBod[index] ? Number(dsInflWqltBod[index].toFixed(1)) : '', + dsInflWqltCod: dsInflWqltCod[index] ? Number(dsInflWqltCod[index].toFixed(1)) : '', + dsInflWqltSs: dsInflWqltSs[index] ? Number(dsInflWqltSs[index].toFixed(1)) : '', + dsInflWqltTn: dsInflWqltTn[index] ? dsInflWqltTn[index].toFixed(3) : '', + dsInflWqltTp: dsInflWqltTp[index] ? dsInflWqltTp[index].toFixed(3) : '', + inflAssctAftWqltBod: inflAssctAftWqltBod[index] ? Number(inflAssctAftWqltBod[index].toFixed(1)) : '', + inflAssctAftWqltToc: inflAssctAftWqltToc[index] ? Number(inflAssctAftWqltToc[index].toFixed(1)) : '', + inflAssctAftWqltSs: inflAssctAftWqltSs[index] ? Number(inflAssctAftWqltSs[index].toFixed(1)) : '', + inflAssctAftWqltTn: inflAssctAftWqltTn[index] ? inflAssctAftWqltTn[index].toFixed(3) : '', + inflAssctAftWqltTp: inflAssctAftWqltTp[index] ? inflAssctAftWqltTp[index].toFixed(3) : '', + efflWqltBod: efflWqltBod[index] ? Number(efflWqltBod[index].toFixed(1)) : '', + efflWqltCod: efflWqltCod[index] ? Number(efflWqltCod[index].toFixed(1)) : '', + efflWqltSs: efflWqltSs[index] ? Number(efflWqltSs[index].toFixed(1)) : '', + efflWqltTn: efflWqltTn[index] ? efflWqltTn[index].toFixed(3) : '', + efflWqltTp: efflWqltTp[index] ? efflWqltTp[index].toFixed(3) : '' + })); + + const excelList : RainManageExcelType[] = []; + if(isExcel){ + filteredData.forEach((item) => { + excelList.push({ + 강우량: item.rainItem, + 강우조건비율: item.rainRate ? Number(Number(item.rainRate).toFixed(1)) : '', + 일수: item.rainDay ? Number(Number(item.rainDay).toFixed(1)) : '', + 유입유량: item.inflAmnt ? Number(Number(item.inflAmnt).toFixed(1)) : '', + 시설용량대비: item.fctCpc ? Number(Number(item.fctCpc).toFixed(1)) : '', + 설계유입수질BOD: item.dsInflWqltBod ? Number(Number(item.dsInflWqltBod).toFixed(1)) : '', + 설계유입수질TOC: item.dsInflWqltCod ? Number(Number(item.dsInflWqltCod).toFixed(1)) : '', + 설계유입수질SS: item.dsInflWqltSs ? Number(Number(item.dsInflWqltSs).toFixed(1)) : '', + 설계유입수질TN: item.dsInflWqltTn ? Number(item.dsInflWqltTn).toFixed(3) : '', + 설계유입수질TP: item.dsInflWqltTp ? Number(item.dsInflWqltTp).toFixed(3) : '', + 유입수질BOD: item.inflAssctAftWqltBod ? Number(Number(item.inflAssctAftWqltBod).toFixed(1)) : '', + 유입수질TOC: item.inflAssctAftWqltToc ? Number(Number(item.inflAssctAftWqltToc).toFixed(1)) : '', + 유입수질SS: item.inflAssctAftWqltSs ? Number(Number(item.inflAssctAftWqltSs).toFixed(1)) : '', + 유입수질TN: item.inflAssctAftWqltTn ? Number(item.inflAssctAftWqltTn).toFixed(3) : '', + 유입수질TP: item.inflAssctAftWqltTp ? Number(item.inflAssctAftWqltTp).toFixed(3) : '', + 방류수질BOD: item.efflWqltBod ? Number(Number(item.efflWqltBod).toFixed(1)) : '', + 방류수질TOC: item.efflWqltCod ? Number(Number(item.efflWqltCod).toFixed(1)) : '', + 방류수질SS: item.efflWqltSs ? Number(Number(item.efflWqltSs).toFixed(1)) : '', + 방류수질TN: item.efflWqltTn ? Number(item.efflWqltTn).toFixed(3) : '', + 방류수질TP: item.efflWqltTp ? Number(item.efflWqltTp).toFixed(3) : '', + }); + }); + } + + set((state: DashBoardType) => ({ + ...state, + RainManage: isExcel ? { + ...state.RainManage, + rainManageList: filteredData as RainManageListProps[], + excelList: excelList, + } : { + ...state.RainManage, + rainManageList: filteredData as RainManageListProps[], + } + })); + }, + + searchDrainageEquipment: async (searchYear: string, fcltyCd: string, isExcel : boolean) => { + await fetchDrainageEquipment(searchYear, fcltyCd); + /* if(res == null || res.data.wqltObjs == null || res.data.wqltObjs.length === 0) { + set((state: DashBoardType) => ({ + ...state.RainManage, + rainManageList: [], + })); + return; + } */ + + /* if(isExcel){ + res.data.wqltObjs.forEach((item) => { + excelList.push({ + 강우량: item.rainItem, + 강우조건비율: item.rainRate || 0, + 일수: item.rainDay || 0, + 유입유량: item.inflAmnt || 0, + 시설용량대비: item.fctCpc || 0, + 설계유입수질BOD: item.dsInflWqltBod || 0, + 설계유입수질TOC: item.dsInflWqltCod || 0, + 설계유입수질SS: item.dsInflWqltSs || 0, + 설계유입수질TN: item.dsInflWqltTn || 0, + 설계유입수질TP: item.dsInflWqltTp || 0, + 유입수질BOD: item.inflAssctAftWqltBod || 0, + 유입수질COD: item.inflAssctAftWqltToc || 0, + 유입수질SS: item.inflAssctAftWqltSs || 0, + 유입수질TN: item.inflAssctAftWqltTn || 0, + 유입수질TP: item.inflAssctAftWqltTp || 0, + 방류수질BOD: item.efflWqltBod || 0, + 방류수질COD: item.efflWqltCod || 0, + 방류수질SS: item.efflWqltSs || 0, + 방류수질TN: item.efflWqltTn || 0, + 방류수질TP: item.efflWqltTp || 0, + }); + }); + } */ + + set((state: DashBoardType) => ({ + ...state, + DrainageEquipment: isExcel ? { + ...state.DrainageEquipment, + drainageEquipmentList: drainageEquipmentList, + drainageEquipmentExcelList: [], + } : { + ...state.DrainageEquipment, + drainageEquipmentList: drainageEquipmentList, + } + })); + } + } +} \ No newline at end of file diff --git a/apps/fo/app/store/facilityPrecipitation/index.tsx b/apps/fo/app/store/facilityPrecipitation/index.tsx new file mode 100644 index 000000000..0c94cf553 --- /dev/null +++ b/apps/fo/app/store/facilityPrecipitation/index.tsx @@ -0,0 +1,307 @@ +import DataForm from "@/app/components/common/DataForm"; +import type { TableUpperProps } from "../dashboard"; +import { getFetch } from "@/app/components/api"; +export type { TableUpperProps }; +/**==================================== + * Store - 타입 + ====================================*/ + +/** + * (1) 메인페이지 + */ +// 시설별 강수현황 +type FacilityPrecipitationProps = { + bscFctCd : string, + sido : string, + gugun : string, + bscFctNm : string, + addr : string, + fctCpc : number, + stnKo : string, + infoRnDay : number, + rainEffectDay : number, + totalYearRain : number, + rvbsn : string, + lgbsn : string, + mdmbsn : string, + rgnNm : string, +} + +/* +* (2) 강수현황 팝업 +*/ +// 강수현황 +export type PrecipitationPropsDetailProps = { + bscFctCd : string, + rainDt : string, + infoRnDay : number, + rainEffectDay : number, + rainDay : number +} + +export interface ExcelType { + 시설코드: string, + 권역:string|'', + 단위유역:string|'', + 중권역:string|'', + 환경청:string|'', + 시도: string, + 시군구: string, + 시설이름: string, + 주소: string, + 시설용량: number, + 관측소정보 : string, + 강우일 : number, + 강우영향일 : number, + 연간총강수량 : number, +} + +/**==================================== + * Store - data + ====================================*/ + +/** + * (1) 메인페이지 + */ +// 검색단 +const LabelArray:string[] = ['구분', '시도', '시군구','용량별', '시설명', '시설상태', '조회기간', '권역', '단위유역', '중권역', '환경청별']; + +// 리스트 헤더 +const upHeadList1:TableUpperProps[] = [ + { id: "bscFctCd", title: "시설코드", row: 0, rowSpan: 2}, + { id: "A", title: "행정구역", row: 0, colSpan: 2 }, + { id: "sido", title: "시도", upName:"upChangeRe", upSequnce:1, row: 1 }, + { id: "gugun", title: "시군구", upName:"upChangeRe",upSequnce:2, row: 1 }, + { id: "bscFctNm", title: "시설명", row: 0, rowSpan: 2}, + { id: "addr", title: "주소", row: 0, rowSpan: 2}, + { id: "fctCpc", title: "시설용량(m³/일)", row: 0, rowSpan: 2}, + { id: "stnKo", title: "관측소 정보", row: 0, rowSpan: 2}, + { id: "infoRnDay", title: "강우일\n(일)", row: 0, rowSpan: 2}, + { id: "rainEffectDay", title: "강우영향일\n(일)", row: 0, rowSpan: 2}, + { id: "totalYearRain", title: "연간 총 총강수량\n(mm)", row: 0, rowSpan: 2} +]; + +const upHeadList2:TableUpperProps[] = [ + { id: "bscFctCd", title: "시설코드", row: 0, rowSpan: 2}, + { id: "A", title: "유역", row: 0, colSpan: 3} , + { id: "rvbsn", title: "권역", upName:"upChangeRe", upSequnce:1, row: 1} , + { id: "lgbsn", title: "단위유역", upName:"upChangeRe", upSequnce:2, row: 1}, + { id: "mdmbsn", title: "중권역", upName:"upChangeRe", upSequnce:3, row: 1}, + { id: "bscFctNm", title: "시설명", row: 0, rowSpan: 2}, + { id: "addr", title: "주소", row: 0, rowSpan: 2}, + { id: "fctCpc", title: "시설용량(m³/일)", row: 0, rowSpan: 2}, + { id: "stnKo", title: "관측소 정보", row: 0, rowSpan: 2}, + { id: "infoRnDay", title: "강우일\n(일)", row: 0, rowSpan: 2}, + { id: "rainEffectDay", title: "강우영향일\n(일)", row: 0, rowSpan: 2}, + { id: "totalYearRain", title: "연간 총 총강수량\n(mm)", row: 0, rowSpan: 2} +]; + +const upHeadList3:TableUpperProps[] = [ + { id: "bscFctCd", title: "시설코드", row: 0, rowSpan: 2}, + { id: "A", title: "유역", row: 0, colSpan: 3} , + { id: "rvbsn", title: "권역", upName:"upChangeRe1", upSequnce:1, row: 1}, + { id: "lgbsn", title: "단위유역", upName:"upChangeRe1", upSequnce:2, row: 1}, + { id: "mdmbsn", title: "중권역", upName:"upChangeRe1", upSequnce:3, row: 1}, + { id: "rgnNm", title: "환경청", row: 0, rowSpan: 2}, + { id: "B", title: "행정구역", upName:"upChangeRe2", row: 0, colSpan: 2}, + { id: "sido", title: "시도", upName:"upChangeRe2", upSequnce:1, row: 1}, + { id: "gugun", title: "시군구", upName:"upChangeRe2",upSequnce:2, row: 1}, + { id: "bscFctNm", title: "시설명", row: 0, rowSpan: 2}, + { id: "addr", title: "주소", row: 0, rowSpan: 2}, + { id: "fctCpc", title: "시설용량(m³/일)", row: 0, rowSpan: 2}, + { id: "stnKo", title: "관측소 정보", row: 0, rowSpan: 2}, + { id: "infoRnDay", title: "강우일\n(일)", row: 0, rowSpan: 2}, + { id: "rainEffectDay", title: "강우영향일\n(일)", row: 0, rowSpan: 2}, + { id: "totalYearRain", title: "연간 총 총강수량\n(mm)", row: 0, rowSpan: 2} +]; + +const facilityPrecipitation:FacilityPrecipitationProps[] = [ +] + +/** + * 강수현황 상세팝업 + */ +// 강수현황 (헤더) +const upHeadListPrecipitationDetail:TableUpperProps[] = [ + { id: "rainDt", title: "날짜"}, + { id: "infoRnDay", title: "강수량(mm/일)"}, + { id: "rainDay", title: "강우일(일)"}, + { id: "rainEffectDay", title: "강우영향일(일)"} +] +// 강수현황 (리스트) +const gridListPrecipitationDetail:PrecipitationPropsDetailProps[] = [ +] + +/**==================================== + * Store - api + ====================================*/ + +const fetchFacilityPrecipitationList = async (pageNo: number, pageSize: number, searchYear: string, part?: string, nameCd?: string, fcltCpct?: string, operationValue?: string, fcltyNm?: string) => { + const tailStr = '/'+pageNo + '/' + pageSize + '/' + searchYear + (part?'/' + part:'' ) + (nameCd?'/' + nameCd:'' ) + (fcltCpct?'/' + fcltCpct:'' ) + (operationValue?'/' + operationValue:'' ) + (fcltyNm && fcltyNm !== '' ? '/' + fcltyNm : '') ; + console.log('tailStr', tailStr); + const url = '/fclt/rain' + tailStr; + const res = await getFetch('fo', url); + const tmpData:PrecipitationPropsDetailProps[] = []; + if(res!= null && res.data.length > 0){ + for(let idx=0; idx<res.data.length; idx++){ + for(let idx2=0; idx2<res.data[idx].rainyDayObjList.length; idx2++){ + const bscFctCd = res.data[idx].rainyDayObjList[idx2].bscFctCd; + const rainDt = res.data[idx].rainyDayObjList[idx2].rainDt; + const infoRnDay = res.data[idx].rainyDayObjList[idx2].infoRnDay; + const rainDay = res.data[idx].rainyDayObjList[idx2].infoRnDay>=3?1:0; + let rainEffectDay = 0; + if(idx2 < res.data[idx].rainyDayObjList.length-2){ + if(rainDay==1){ + rainEffectDay = 1; + } else { + if(idx2 >= 2){ + if(res.data[idx].rainyDayObjList[idx2-1].infoRnDay>=3 || res.data[idx].rainyDayObjList[idx2-2].infoRnDay>=3){ + rainEffectDay = 1; + } + } + } + } else if(idx2 < res.data[idx].rainyDayObjList.length-1){ + if(rainDay==1 || res.data[idx].rainyDayObjList[idx2+1].infoRnDay>=3){ + rainEffectDay = 1; + } + } else { + rainEffectDay = rainDay; + } + + tmpData.push({ + bscFctCd : bscFctCd, + rainDt : rainDt, + infoRnDay : infoRnDay, + rainDay : rainDay, + rainEffectDay : rainEffectDay + }) + } + } + + res.data.map((item: FacilityPrecipitationProps) => { + const filteredList = tmpData.filter((gridItem) => { + return gridItem.bscFctCd === item.bscFctCd; + }); + + item.rainEffectDay = filteredList.reduce((acc, curr) => acc + curr.rainEffectDay, 0); + item.totalYearRain = filteredList.reduce((acc, curr) => acc + curr.infoRnDay, 0); + item.infoRnDay = filteredList.reduce((acc, curr) => acc + curr.rainDay, 0); + }) + } + return { data: res.data, count: res.count, detailData: tmpData }; +} + +/**==================================== + * Store - expaort + ====================================*/ +export type FacilityPrecipitationActions = { + searchFacilityPrecipitationList: (pageNo: number, pageSize: number, searchYear: string, part?: string, nameCd?: string, fcltCpct?: string, operationValue?: string, fcltyNm?: string) => void; +} + +const dataFormSetting=(item : FacilityPrecipitationProps)=> { + DataForm({ type: 'number', column: 'fctCpc', item: item as unknown as Record<string, unknown> }); + DataForm({ type: 'float1', column: 'totalYearRain', item: item as unknown as Record<string, unknown>}); +}; + +export type FacilityPrecipitationType = { + FacilityPrecipitationSearch: { + isInit: boolean; + labelArray:string[]; + }, + + FacilityPrecipitation : { + isInit: boolean; + upHeadList1: TableUpperProps[]; + upHeadList2: TableUpperProps[]; + upHeadList3: TableUpperProps[]; + facilityPrecipitation: FacilityPrecipitationProps[]; + excelList: ExcelType[]; + facilityPrecipitationCount: number; + }, + + PrecipitationDetail : { + isInit : boolean; + upHeadListPrecipitationDetail : TableUpperProps[]; + gridListPrecipitationDetail : PrecipitationPropsDetailProps[]; + } +} + +export const facilityPrecipitationInitState: FacilityPrecipitationType = { + FacilityPrecipitationSearch: { + isInit: false, + labelArray: LabelArray, + }, + + FacilityPrecipitation : { + isInit : false, + upHeadList1 : upHeadList1, + upHeadList2 : upHeadList2, + upHeadList3 : upHeadList3, + facilityPrecipitation : facilityPrecipitation, + excelList : [], + facilityPrecipitationCount : 0 + }, + + PrecipitationDetail : { + isInit : false, + upHeadListPrecipitationDetail : upHeadListPrecipitationDetail, + gridListPrecipitationDetail : gridListPrecipitationDetail + } +} + +export type FacilityPrecipitationStore = FacilityPrecipitationType & FacilityPrecipitationActions; + +export const facilityPrecipitationReducer:(set:(fn: (state: FacilityPrecipitationType) => FacilityPrecipitationType) => void)=>FacilityPrecipitationActions=(set: (fn: (state: FacilityPrecipitationType) => FacilityPrecipitationType) => void) => { + return { + searchFacilityPrecipitationList: async (pageNo: number, pageSize: number, searchYear: string, part?: string, nameCd?: string, fcltCpct?: string, operationValue?: string, fcltyNm?: string) => { + const res = await fetchFacilityPrecipitationList(pageNo, pageSize, searchYear, part, nameCd, fcltCpct, operationValue, fcltyNm); + const data = res.data; + const excelDownList: ExcelType[] = []; + + if(pageSize>5000){ + data.forEach((item: FacilityPrecipitationProps) => { + dataFormSetting(item) + item.fctCpc = item.fctCpc ? parseFloat(item.fctCpc.toString().replace(/,/g, '')) : 0; + item.totalYearRain = item.totalYearRain ? parseFloat(item.totalYearRain.toString().replace(/,/g, '')) : 0; + excelDownList.push({ + 시설코드: item.bscFctCd, + 권역: item.rvbsn, + 단위유역: item.lgbsn, + 중권역: item.mdmbsn, + 환경청: item.rgnNm, + 시도: item.sido, + 시군구: item.gugun, + 시설이름: item.bscFctNm, + 주소: item.addr, + 시설용량: item.fctCpc, + 관측소정보: item.stnKo, + 강우일: item.infoRnDay, + 강우영향일: item.rainEffectDay, + 연간총강수량: item.totalYearRain, + }) + }) + }else{ + data.forEach((item: FacilityPrecipitationProps) => { + dataFormSetting(item) + item.fctCpc = item.fctCpc ? parseFloat(item.fctCpc.toString().replace(/,/g, '')) : 0; + item.totalYearRain = item.totalYearRain ? parseFloat(item.totalYearRain.toString().replace(/,/g, '')) : 0; + }) + } + set((state:FacilityPrecipitationType) => ({ + ...state, + FacilityPrecipitation: pageSize<5000?{ + ...state.FacilityPrecipitation, + facilityPrecipitation: res.data, + facilityPrecipitationCount: res.count, + }:{ + ...state.FacilityPrecipitation, + excelList: excelDownList, + }, + PrecipitationDetail:{ + ...state.PrecipitationDetail, + gridListPrecipitationDetail: res.detailData + } + })) + } + } +} \ No newline at end of file diff --git a/apps/fo/app/store/facilityWaterQlty/index.tsx b/apps/fo/app/store/facilityWaterQlty/index.tsx new file mode 100644 index 000000000..565c251d2 --- /dev/null +++ b/apps/fo/app/store/facilityWaterQlty/index.tsx @@ -0,0 +1,659 @@ +import { getFetch } from "@/app/components/api"; +import DataForm from "@/app/components/common/DataForm"; +import type { TableUpperProps } from "../dashboard"; +export type { TableUpperProps }; + +/**==================================== + * Store - 타입 + * + * tn, tp는 소수점 3자리까지, 나머지는 소수점 1자리까지 + ====================================*/ + +/** + * (1) 메인페이지 + */ +// 시설수질리스트 타입 +export interface FacilityWaterQltyProps { + bscFctCd: string, + bscFctNm: string, + msrDt: string + sidoNm: string, + sggNm: string, + addr: string, + fctCpc: number, + inflAssctAftWqltBod: string | '', + inflAssctAftWqltToc: string | '', + inflAssctAftWqltSs: string | '', + inflAssctAftWqltTn: string | '', + inflAssctAftWqltTp: string | '', + inflAssctBfrWqltTp: string | '', + efflWqltBod: string | '', + efflWqltToc: string | '', + efflWqltSs: string | '', + efflWqltTn: string | '', + efflWqltTp: string | '', + rgnNm:string|'', + rvbsn:string|'', + lgbsn:string|'', + mdmbsn:string|'', + inflAmnt: number | '', + lclgvrnmntNm: string, + infoRnDay: number | '', + rainEffectDay: number | '', + tpInflWqltTmprtr: number | '', + tpInflWqltPh: string | '', + tpInflWqltBod: string | '', + tpInflWqltCod: string | '', + tpInflWqltSs: string | '', + tpInflWqltTn: string | '', + tpInflWqltTp: string | '', + tpInflWqltEclgyTox: string | '', + tpInflWqltClfgrpcnt: string | '', +} +export interface ExcelType { + 시설코드: string, + 권역:string|'', + 단위유역:string|'', + 중권역:string|'', + 환경청:string|'', + 시도: string, + 시군구: string, + 시설이름: string, + 주소: string, + 용량: number, + 유입BOD: string | '', + 유입TOC: string | '', + 유입SS: string | '', + 유입TN: string | '', + 유입TP: string | '', + 유출BOD: string | '', + 유출TOC: string | '', + 유출SS: string | '', + 유출TN: string | '', + 유출TP: string | '', +} + +/** + * (2) 모달페이지 + */ +// 시설수질리스트 타입 +export type FacilityWaterQltyTmsProps = { + part: string, + toc: number, + ss: number, + tn: number, + tp: number, +} + +// API 응답 데이터 타입 +export interface WaterQualityHistoryResponse { + data: { + wqltObjs: FacilityWaterQltyProps[]; + }; +} + +export type modalExcelType = { + 날짜: string; + "유입BOD(mg/L)": string; + "유입TOC(COD)(mg/L)": string; + "유입SS(mg/L)": string; + "유입TN(mg/L)": string; + "유입TP(mg/L)": string; + "유출BOD(mg/L)": string; + "유출TOC(COD)(mg/L)": string; + "유출SS(mg/L)": string; + "유출TN(mg/L)": string; + "유출TP(mg/L)": string; + /* "강우량(mm/일)": string; */ +} + +/**==================================== + * Store - data + ====================================*/ +/** + * (1) 메인페이지 + */ +// 메인 라벨 배열 +const mainLabelArray: string[] = ['구분', '시도', '시군구', '용량별', '시설명', '조회기간', '시설상태', '권역', '단위유역', '중권역', '환경청별']; + +// 시설수질리스트 헤더(행정별) +const upHeadList1: TableUpperProps[] = [ + { id: "bscFctCd", title: "시설코드", rowSpan: 2, row:0 }, + { id: "C", title: "행정명", colSpan: 2, row: 0 }, + { id: "sidoNm", title: "시도", row: 1 }, + { id: "sggNm", title: "시군구", row: 1 }, + { id: "bscFctNm", title: "시설명", rowSpan: 2, row: 0 }, + { id: "addr", title: "주소", rowSpan: 2, row: 0 }, + { id: "fctCpc", title: "시설용량(m³/일)", align: "right", rowSpan: 2, row: 0 }, + { id: "A", title: "유입", colSpan: 5, row: 0 }, + { id: "B", title: "방류", colSpan: 5, row: 0 }, + { id: "inflAssctAftWqltBod", title: "BOD\n(mg/L)", upName: "upChangeRe3", upSequnce: 1, align: "right", row: 1 }, + { id: "inflAssctAftWqltToc", title: "TOC\n(COD)\n(mg/L)", upName: "upChangeRe3", upSequnce: 2, align: "right", row: 1 }, + { id: "inflAssctAftWqltSs", title: "SS\n(mg/L)", upName: "upChangeRe3", upSequnce: 3, align: "right", row: 1 }, + { id: "inflAssctAftWqltTn", title: "T-N\n(mg/L)", upName: "upChangeRe3", upSequnce: 4, align: "right", row: 1 }, + { id: "inflAssctAftWqltTp", title: "T-P\n(mg/L)", upName: "upChangeRe3", upSequnce: 3, align: "right", row: 1 }, + { id: "efflWqltBod", title: "BOD\n(mg/L)", upName: "upChangeRe4", upSequnce: 1, align: "right", row: 1 }, + { id: "efflWqltToc", title: "TOC\n(COD)\n(mg/L)", upName: "upChangeRe4", upSequnce: 2, align: "right", row: 1 }, + { id: "efflWqltSs", title: "SS\n(mg/L)", upName: "upChangeRe4", upSequnce: 3, align: "right", row: 1 }, + { id: "efflWqltTn", title: "T-N\n(mg/L)", upName: "upChangeRe4", upSequnce: 4, align: "right", row: 1 }, + { id: "efflWqltTp", title: "T-P\n(mg/L)", upName: "upChangeRe4", upSequnce: 3, align: "right", row: 1 }, +]; + +// 시설수질리스트 헤더(권역별) +const upHeadList2: TableUpperProps[] = [ + { id: "bscFctCd", title: "시설코드", row:0, rowSpan:2 }, + { id: "C", title: "유역", colSpan: 3, row: 0 }, + { id: "rvbsn", title: "권역", upName: "upChangeRe1", upSequnce: 1, row:1 }, + { id: "lgbsn", title: "단위유역", upName: "upChangeRe1", upSequnce: 2, row:1 }, + { id: "mdmbsn", title: "중권역", upName: "upChangeRe1", upSequnce: 3, row:1 }, + { id: "bscFctNm", title: "시설명", row:0, rowSpan:2 }, + { id: "addr", title: "주소", row:0, rowSpan:2 }, + { id: "fctCpc", title: "시설용량(m³/L)", align: "right", row:0, rowSpan:2 }, + { id: "A", title: "유입", colSpan: 5, row: 0 }, + { id: "B", title: "방류", colSpan: 5, row: 0 }, + { id: "inflAssctAftWqltBod", title: "BOD\n(mg/L)", upName: "upChangeRe3", upSequnce: 1, align: "right", row:1 }, + { id: "inflAssctAftWqltToc", title: "TOC\n(COD)\n(mg/L)", upName: "upChangeRe3", upSequnce: 2, align: "right", row:1 }, + { id: "inflAssctAftWqltSs", title: "SS\n(mg/L)", upName: "upChangeRe3", upSequnce: 3, align: "right", row:1 }, + { id: "inflAssctAftWqltTn", title: "T-N\n(mg/L)", upName: "upChangeRe3", upSequnce: 2, align: "right", row:1 }, + { id: "inflAssctAftWqltTp", title: "T-P\n(mg/L)", upName: "upChangeRe3", upSequnce: 3, align: "right", row:1 }, + { id: "efflWqltBod", title: "BOD\n(mg/L)", upName: "upChangeRe4", upSequnce: 1, align: "right", row:1 }, + { id: "efflWqltToc", title: "TOC\n(COD)\n(mg/L)", upName: "upChangeRe4", upSequnce: 2, align: "right", row:1 }, + { id: "efflWqltSs", title: "SS\n(mg/L)", upName: "upChangeRe4", upSequnce: 3, align: "right", row:1 }, + { id: "efflWqltTn", title: "T-N\n(mg/L)", upName: "upChangeRe4", upSequnce: 2, align: "right", row:1 }, + { id: "efflWqltTp", title: "T-P\n(mg/L)", upName: "upChangeRe4", upSequnce: 3, align: "right", row:1 }, +]; + +// 시설수질리스트 헤더(환경청별) +const upHeadList3: TableUpperProps[] = [ + { id: "bscFctCd", title: "시설코드", row:0, rowSpan:2 }, + { id: "C", title: "유역", colSpan: 3, row: 0 }, + { id: "rvbsn", title: "권역", row:1 }, + { id: "lgbsn", title: "단위유역", row:1 }, + { id: "mdmbsn", title: "중권역", row:1 }, + { id: "rgnNm", title: "환경청", row:0, rowSpan:2 }, + { id: "D", title: "행정명", colSpan: 2, row: 0 }, + { id: "sidoNm", title: "시도", upName: "upChangeRe2", row:1 }, + { id: "sggNm", title: "시군구", upName: "upChangeRe2", row:1 }, + { id: "bscFctNm", title: "시설명", row:0, rowSpan:2 }, + { id: "addr", title: "주소", row:0, rowSpan:2 }, + { id: "fctCpc", title: "시설용량(m³/L)", align: "right", row:0, rowSpan:2 }, + { id: "A", title: "유입", colSpan: 5, row: 0 }, + { id: "B", title: "방류", colSpan: 5, row: 0 }, + { id: "inflAssctAftWqltBod", title: "BOD\n(mg/L)", upName: "upChangeRe3", upSequnce: 1, align: "right", row:1 }, + { id: "inflAssctAftWqltToc", title: "TOC\n(COD)\n(mg/L)", upName: "upChangeRe3", upSequnce: 2, align: "right", row:1 }, + { id: "inflAssctAftWqltSs", title: "SS\n(mg/L)", upName: "upChangeRe3", upSequnce: 3, align: "right", row:1 }, + { id: "inflAssctAftWqltTn", title: "T-N\n(mg/L)", upName: "upChangeRe3", upSequnce: 2, align: "right", row:1 }, + { id: "inflAssctAftWqltTp", title: "T-P\n(mg/L)", upName: "upChangeRe3", upSequnce: 3, align: "right", row:1 }, + { id: "efflWqltBod", title: "BOD\n(mg/L)", upName: "upChangeRe4", upSequnce: 1, align: "right", row:1 }, + { id: "efflWqltToc", title: "TOC\n(COD)\n(mg/L)", upName: "upChangeRe4", upSequnce: 2, align: "right", row:1 }, + { id: "efflWqltSs", title: "SS\n(mg/L)", upName: "upChangeRe4", upSequnce: 3, align: "right", row:1 }, + { id: "efflWqltTn", title: "T-N\n(mg/L)", upName: "upChangeRe4", upSequnce: 2, align: "right", row:1 }, + { id: "efflWqltTp", title: "T-P\n(mg/L)", upName: "upChangeRe4", upSequnce: 3, align: "right", row:1 }, +]; + +/** + * (2) 모달페이지 + */ +// 처리시설 수질현황 헤더(모달-연간) +const upHeadListModalYear: TableUpperProps[] = [ + { id: "msrDt", title: "날짜" }, + { id: "inflAssctAftWqltBod", title: "BOD\n(mg/L)", upName: "upChangeRe1", upSequnce: 1, align: "right" }, + { id: "inflAssctAftWqltToc", title: "TOC(COD)\n(mg/L)", upName: "upChangeRe1", upSequnce: 2, align: "right" }, + { id: "inflAssctAftWqltSs", title: "SS\n(mg/L)", upName: "upChangeRe1", upSequnce: 3, align: "right" }, + { id: "inflAssctAftWqltTn", title: "T-N\n(mg/L)", upName: "upChangeRe1", upSequnce: 2, align: "right" }, + { id: "inflAssctBfrWqltTp", title: "T-P\n(mg/L)", upName: "upChangeRe1", upSequnce: 3, align: "right" }, + { id: "efflWqltBod", title: "BOD\n(mg/L)", upName: "upChangeRe2", upSequnce: 1, align: "right" }, + { id: "efflWqltToc", title: "TOC(COD)\n(mg/L)", upName: "upChangeRe2", upSequnce: 2, align: "right" }, + { id: "efflWqltSs", title: "SS\n(mg/L)", upName: "upChangeRe2", upSequnce: 3, align: "right" }, + { id: "efflWqltTn", title: "T-N\n(mg/L)", upName: "upChangeRe2", upSequnce: 2, align: "right" }, + { id: "efflWqltTp", title: "T-P\n(mg/L)", upName: "upChangeRe2", upSequnce: 3, align: "right" }, + /* { id: "infoRnDay", title: "강우량(mm/일)", align: "right" }, */ +]; + +// 처리시설 수질현황 헤더(모달-수질TMS) +const upHeadListModalTms: TableUpperProps[] = [ + { id: "part", title: "구분" }, + { id: "toc", title: "TOC(mg/L)", upName: "upChangeRe", upSequnce: 1, align: "right" }, + { id: "ss", title: "SS(mg/L)", upName: "upChangeRe", upSequnce: 2, align: "right" }, + { id: "tn", title: "T-N(mg/L)", upName: "upChangeRe", upSequnce: 3, align: "right" }, + { id: "tp", title: "T-P(mg/L)", upName: "upChangeRe", upSequnce: 4, align: "right" }, +]; + +// 처리시설 수질현황 데이터(모달-연간) +const facilityWaterQltyYearList: FacilityWaterQltyProps[] = [ +]; + +// 처리시설 수질현황 데이터(모달-수질TMS) +const facilityWaterQltyTmsList: FacilityWaterQltyTmsProps[] = [ +]; + +// 총인설비 수질현황 헤더 데이터 +const upHeadListModalChongIn: TableUpperProps[] = [ + { id: "msrDt", title: "날짜" }, + { id: "tpInflWqltBod", title: "BOD(mg/L)", upName: "upChangeRe1", upSequnce: 1, align: "right" }, + { id: "tpInflWqltTp", title: "T-P(mg/L)", upName: "upChangeRe1", upSequnce: 2, align: "right" }, + { id: "efflWqltBod", title: "BOD(mg/L)", upName: "upChangeRe2", upSequnce: 1, align: "right" }, + { id: "efflWqltTp", title: "T-P(mg/L)", upName: "upChangeRe2", upSequnce: 2, align: "right" }, +]; + +/**==================================== +* Store - api (데이터 호출) +====================================*/ +// 메인페이지 +// 시설정보 수질 리스트 데이터 호출 +const fetchFacilityWaterQltyList = async (pageNo: number, pageSize: number, searchYear: string, partCd?: string|undefined, nameCd?: string|undefined, fcltCpct?: string|undefined, operationValue?: string|undefined, fcltyNm?: string | undefined): Promise<{ data: FacilityWaterQltyProps[], count: number }> => { + try { + const tailStr = partCd ? '/'+ partCd + '/' + nameCd + '/' + fcltCpct + '/' + operationValue + (fcltyNm && fcltyNm !== '' ? '/' + fcltyNm : '') : ''; + const url = '/fclt/qlty/' + pageNo + '/' + pageSize + '/' + searchYear + tailStr; + const res = await getFetch('fo', url); + return { data: res.data, count: res.count } + } catch (error) { + console.error('시설정보 수질현황 데이터를 가져오는데 실패했습니다:', error); + return { data: [], count: 0 }; + } +} + +// 시설정보 수질 리스트 데이터 호출(모달-수질TMS) +const fetchFacilityWaterQltyTmsList = async (facilityCd: string|undefined) => { + try { + const res = await getFetch('wr', '/il/getTms/' + facilityCd); + return { data: res.data, count: res.count }; + } catch (error) { + console.error('시설정보 수질현황 TMS 데이터를 가져오는데 실패했습니다:', error); + return { data: [], count: 0 }; + } +}; + +// 시설정보 수질 리스트 데이터 호출(모달-이력) +const fetchFacilityWaterQltyListHist = async (searchYear: string|undefined, facilityCd: string|undefined): Promise<WaterQualityHistoryResponse | null> => { + try {// + const res = await getFetch('fo', '/fclt/qlty/hist/' + searchYear + '/' + facilityCd); + return res; + } catch (error) { + console.error('시설정보 수질현황 이력 데이터를 가져오는데 실패했습니다:', error); + return null; + } +}; + +/**==================================== +* Store - export +====================================*/ +export type FacilityWaterQltyType = { + FacilityWaterQltyMain: { + facilityWaterQltyList: FacilityWaterQltyProps[]; + excelList: ExcelType[]; + facilityWaterQltyCount: number; + isInit: boolean; + mainLabelArray: string[]; + upHeadList1: TableUpperProps[]; + upHeadList2: TableUpperProps[]; + upHeadList3: TableUpperProps[]; + }, + + FacilityWaterQltyModal: { + // 처리시설 수질현황 + upHeadListModalYear: TableUpperProps[]; + facilityWaterQltyYearList: FacilityWaterQltyProps[]; + upHeadListModalTms: TableUpperProps[]; + facilityWaterQltyTmsList: FacilityWaterQltyTmsProps[]; + + // 수질tms + tmsList: FacilityWaterQltyTmsProps[]; + msrmtDate: string; + + // 총인시설 수질현황 + upHeadListModalChongIn: TableUpperProps[]; + } +} + +export const facilityWaterQltyInitState: FacilityWaterQltyType = { + FacilityWaterQltyMain: { + facilityWaterQltyCount: 0, + facilityWaterQltyList: [], + excelList: [], + isInit: false, + mainLabelArray: mainLabelArray, + upHeadList1: upHeadList1, + upHeadList2: upHeadList2, + upHeadList3: upHeadList3, + }, + + FacilityWaterQltyModal: { + // 처리시설 수질현황 + upHeadListModalYear: upHeadListModalYear, + facilityWaterQltyYearList: facilityWaterQltyYearList, + upHeadListModalTms: upHeadListModalTms, + facilityWaterQltyTmsList: facilityWaterQltyTmsList, + + // 수질tms + tmsList: [], + msrmtDate: '', + + // 총인시설 수질현황 + upHeadListModalChongIn: upHeadListModalChongIn, + } +} + +export type FacilityWaterQltyActions = { + /** + * 시설정보 수질현황 리스트 데이터 호출(구분 선택 후 시설명 있을 경우) + * @param pageNo : 페이지 번호 + * @param pageSize : 페이지 크기 + * @param searchYear : 검색 년도 + * @param part : 구분 + * @param nameCd : 시설명 + * @param fcltCpct : 시설용량 + * @param operationValue : 운영상태코드 + * @param fcltyNm : 시설명 + */ + searchFacilityWaterQltyList: (pageNo: number, pageSize: number, searchYear: string, part?: string, nameCd?: string, fcltCpct?: string, operationValue?: string, fcltyNm?: string) => Promise<void>; + + /** + * 시설정보 수질현황 이력 데이터 호출 + * @param searchYear : 검색 년도 + * @param facilityCd : 시설코드 + */ + searchFacilityWaterQltyListHist: (searchYear: string, facilityCd: string) => Promise<void>; +} + +/** + * @param item : 시설수질리스트 타입 + * @returns : 없음 + * @description : 시설수질리스트 타입의 데이터를 표현 방식으로 바꿔주는 함수 + * - msrDt : YYYY-MM-DD 형식으로 변환 + * - inflAssctAftWqltBod : 소수점 1자리까지 표현 + * - inflAssctAftWqltTn : 소수점 1자리까지 표현 + * - inflAssctBfrWqltTp : 소수점 1자리까지 표현 + * - efflWqltBod : 소수점 2자리까지 표현 + * - efflWqltTn : 소수점 2자리까지 표현 + * - efflWqltTp : 소수점 2자리까지 표현 + * @author : 박진우 + * @date : 2025-04-24 + */ +const dataFormSetting = (item: FacilityWaterQltyProps) => { + DataForm({ type: 'date', column: 'msrDt', item: item as unknown as Record<string, unknown> }); +} + +export type FacilityWaterQltyStore = FacilityWaterQltyType & FacilityWaterQltyActions; + +export const facilityWaterQltyReducer = (set: (fn: (state: FacilityWaterQltyType) => FacilityWaterQltyType) => void): FacilityWaterQltyActions => { + return { + // 메인페이지 + /** + * 시설정보 수질현황 리스트 데이터 호출(구분 선택 후 시설명 있을 경우) + * @param pageNo : 페이지 번호 + * @param pageSize : 페이지 크기 + * @param searchYear : 검색 년도 + * @param part : 구분 + * @param nameCd : 시설명 + * @param fcltCpct : 시설용량 + * @param operationValue : 운영상태코드 + * @param fcltyNm : 시설명 + */ + searchFacilityWaterQltyList: async (pageNo: number, pageSize: number, searchYear: string, part?: string, nameCd?: string, fcltCpct?: string, operationValue?: string, fcltyNm?: string) => { + const res = await fetchFacilityWaterQltyList(pageNo, pageSize, searchYear, part, nameCd, fcltCpct, operationValue, fcltyNm); + const rtnData = res.data.map(item => { + dataFormSetting(item) + if(item.inflAssctAftWqltTn) { + item.inflAssctAftWqltTn = Number(Math.round(Number(item.inflAssctAftWqltTn) * 1000)/1000).toFixed(3); + } + if(item.efflWqltTn){ + item.efflWqltTn = Number(Math.round(Number(item.efflWqltTn) * 1000)/1000).toFixed(3); + } + if(item.inflAssctAftWqltTp){ + item.inflAssctAftWqltTp = Number(Math.round(Number(item.inflAssctAftWqltTp) * 1000)/1000).toFixed(3); + } + if(item.efflWqltTp){ + item.efflWqltTp = Number(Math.round(Number(item.efflWqltTp) * 1000)/1000).toFixed(3); + } + if(item.inflAssctAftWqltToc){ + item.inflAssctAftWqltToc = Number(Math.round(Number(item.inflAssctAftWqltToc) * 10)/10).toFixed(1); + } + if(item.efflWqltToc){ + item.efflWqltToc = Number(Math.round(Number(item.efflWqltToc) * 10)/10).toFixed(1); + } + if(item.inflAssctAftWqltBod){ + item.inflAssctAftWqltBod = Number(Math.round(Number(item.inflAssctAftWqltBod) * 10)/10).toFixed(1); + } + if(item.efflWqltBod){ + item.efflWqltBod = Number(Math.round(Number(item.efflWqltBod) * 10)/10).toFixed(1); + } + if(item.inflAssctAftWqltSs){ + item.inflAssctAftWqltSs = Number(Math.round(Number(item.inflAssctAftWqltSs) * 10)/10).toFixed(1); + } + if(item.efflWqltSs){ + item.efflWqltSs = Number(Math.round(Number(item.efflWqltSs) * 10)/10).toFixed(1); + } + if(item.inflAssctAftWqltToc){ + item.inflAssctAftWqltToc = Number(Math.round(Number(item.inflAssctAftWqltToc) * 10)/10).toFixed(1); + } + if(item.efflWqltToc){ + item.efflWqltToc = Number(Math.round(Number(item.efflWqltToc) * 10)/10).toFixed(1); + } + return item; + }); + const excelDownList: ExcelType[] = []; + if(pageSize>5000){ + res.data.forEach(item => { + dataFormSetting(item) + item.fctCpc = parseFloat(item.fctCpc.toString().replace(/,/g, '')); + item.sidoNm = item.lclgvrnmntNm.split(' ')[0]?.toString() || ''; + item.sggNm = item.lclgvrnmntNm.split(' ')[1]?.toString() || ''; + excelDownList.push({ + 시설코드: item.bscFctCd, + 권역: item.rvbsn, + 단위유역: item.lgbsn, + 중권역: item.mdmbsn, + 환경청: item.rgnNm, + 시도: item.sidoNm, + 시군구: item.sggNm, + 시설이름: item.bscFctNm, + 주소: item.addr, + 용량: item.fctCpc, + 유입BOD: item.inflAssctAftWqltBod, + 유입TOC: item.inflAssctAftWqltToc, + 유입SS: item.inflAssctAftWqltSs, + 유입TN: item.inflAssctAftWqltTn, + 유입TP: item.inflAssctAftWqltTp, + 유출BOD: item.efflWqltBod, + 유출TOC: item.efflWqltToc, + 유출SS: item.efflWqltSs, + 유출TN: item.efflWqltTn, + 유출TP: item.efflWqltTp, + }); + }); + }else{ + res.data.forEach((item: FacilityWaterQltyProps) => { + dataFormSetting(item) + item.fctCpc = parseFloat(item.fctCpc.toString().replace(/,/g, '')); + item.sidoNm = item.lclgvrnmntNm.split(' ')[0]?.toString() || ''; + item.sggNm = item.lclgvrnmntNm.split(' ')[1]?.toString() || ''; + }); + } + set((state: FacilityWaterQltyType) => ({ + ...state, + FacilityWaterQltyMain: pageSize<5000?{ + ...state.FacilityWaterQltyMain, + facilityWaterQltyList: rtnData, + facilityWaterQltyCount: res.count, + }:{ + ...state.FacilityWaterQltyMain, + excelList: excelDownList, + } + })) + }, + + searchFacilityWaterQltyListHist: async (searchYear: string, facilityCd: string) => { + const res = await fetchFacilityWaterQltyListHist(searchYear, facilityCd); + const res1 = await fetchFacilityWaterQltyTmsList(facilityCd); + + let rtnMsrmtDate = ''; + const rtnTmsList: FacilityWaterQltyTmsProps[] = []; + if(res1.data){ + rtnTmsList.push({ + part: '평균', + toc: res1.data.tocAvgVl, + ss: res1.data.ssAvgVl, + tn: res1.data.tnAvgVl, + tp: res1.data.tpAvgVl, + }); + + rtnTmsList.push({ + part: '최대', + toc: res1.data.tocMaxVl, + ss: res1.data.ssMaxVl, + tn: res1.data.tnMaxVl, + tp: res1.data.tpMaxVl, + }); + + rtnTmsList.push({ + part: '최소', + toc: res1.data.tocMinVl, + ss: res1.data.ssMinVl, + tn: res1.data.tnMinVl, + tp: res1.data.tpMinVl, + }); + + rtnMsrmtDate = res1.data.msrmtDate; + }else{ + rtnTmsList.push({ + part: '평균', + toc: 0, + ss: 0, + tn: 0, + tp: 0, + }); + + rtnTmsList.push({ + part: '최대', + toc: 0, + ss: 0, + tn: 0, + tp: 0, + }); + + rtnTmsList.push({ + part: '최소', + toc: 0, + ss: 0, + tn: 0, + tp: 0, + }); + + rtnMsrmtDate = ''; + } + + if(Array.isArray(res) || !res || !('data' in res) || !res.data || !res.data.wqltObjs || res.data.wqltObjs.length === 0) { + set((state: FacilityWaterQltyType) => ({ + ...state, + FacilityWaterQltyModal: { + ...state.FacilityWaterQltyModal, + facilityWaterQltyYearList: [], + tmsList : rtnTmsList, + msrmtDate : rtnMsrmtDate + } + })) + return; + } + + const tmpData:FacilityWaterQltyProps[] = []; + + // facilityPrecipitation의 rainEffectDay 계산 로직을 적용 + for(let idx = 0; idx < res.data.wqltObjs.length; idx++) { + const item = res.data.wqltObjs[idx]; + if (!item) continue; + + const rainDay = Number(item.infoRnDay) >= 3 ? 1 : 0; + let rainEffectDay = 0; + + if(idx < res.data.wqltObjs.length - 2) { + if(rainDay == 1) { + rainEffectDay = 1; + } else { + if(idx >= 2) { + const prevItem1 = res.data.wqltObjs[idx-1]; + const prevItem2 = res.data.wqltObjs[idx-2]; + if((prevItem1 && Number(prevItem1.infoRnDay) >= 3) || (prevItem2 && Number(prevItem2.infoRnDay) >= 3)) { + rainEffectDay = 1; + } + } + } + } else if(idx < res.data.wqltObjs.length - 1) { + const nextItem = res.data.wqltObjs[idx+1]; + if(rainDay == 1 || (nextItem && Number(nextItem.infoRnDay) >= 3)) { + rainEffectDay = 1; + } + } else { + rainEffectDay = rainDay; + } + + tmpData.push({ + ...item, + rainEffectDay: rainEffectDay, + } as FacilityWaterQltyProps); + } + + const rtnData = tmpData.map((item: FacilityWaterQltyProps) => { + dataFormSetting(item) + if(item.inflAssctAftWqltTn) { + item.inflAssctAftWqltTn = Number(Math.round(Number(item.inflAssctAftWqltTn) * 1000)/1000).toFixed(3); + } + if(item.efflWqltTn){ + item.efflWqltTn = Number(Math.round(Number(item.efflWqltTn) * 1000)/1000).toFixed(3); + } + if(item.inflAssctBfrWqltTp){ + item.inflAssctBfrWqltTp = Number(Math.round(Number(item.inflAssctBfrWqltTp) * 1000)/1000).toFixed(3); + } + if(item.efflWqltTp){ + item.efflWqltTp = Number(Math.round(Number(item.efflWqltTp) * 1000)/1000).toFixed(3); + } + if(item.inflAssctAftWqltToc){ + item.inflAssctAftWqltToc = Number(Math.round(Number(item.inflAssctAftWqltToc) * 10)/10).toFixed(1); + } + if(item.efflWqltToc){ + item.efflWqltToc = Number(Math.round(Number(item.efflWqltToc) * 10)/10).toFixed(1); + } + if(item.inflAssctAftWqltBod){ + item.inflAssctAftWqltBod = Number(Math.round(Number(item.inflAssctAftWqltBod) * 10)/10).toFixed(1); + } + if(item.efflWqltBod){ + item.efflWqltBod = Number(Math.round(Number(item.efflWqltBod) * 10)/10).toFixed(1); + } + if(item.inflAssctAftWqltSs){ + item.inflAssctAftWqltSs = Number(Math.round(Number(item.inflAssctAftWqltSs) * 10)/10).toFixed(1); + } + if(item.efflWqltSs){ + item.efflWqltSs = Number(Math.round(Number(item.efflWqltSs) * 10)/10).toFixed(1); + } + if(item.inflAssctAftWqltToc){ + item.inflAssctAftWqltToc = Number(Math.round(Number(item.inflAssctAftWqltToc) * 10)/10).toFixed(1); + } + if(item.efflWqltToc){ + item.efflWqltToc = Number(Math.round(Number(item.efflWqltToc) * 10)/10).toFixed(1); + } + + if(item.tpInflWqltBod){ + item.tpInflWqltBod = item.tpInflWqltBod?Number(Math.round(Number(item.tpInflWqltBod) * 10)/10).toFixed(1):Number(item.tpInflWqltBod)===0?'0.0':''; + }else{ + item.tpInflWqltBod = ''; + } + + if(item.tpInflWqltTp){ + item.tpInflWqltTp = item.tpInflWqltTp?Number(Math.round(Number(item.tpInflWqltTp) * 1000)/1000).toFixed(3): Number(item.tpInflWqltTp)===0?'0.000':''; + }else{ + item.tpInflWqltTp = ''; + } + return item; + }) + set((state: FacilityWaterQltyType) => ({ + ...state, + FacilityWaterQltyModal: { + ...state.FacilityWaterQltyModal, + facilityWaterQltyYearList: rtnData, + tmsList : rtnTmsList, + msrmtDate : rtnMsrmtDate + } + })) + }, + } +} \ No newline at end of file diff --git a/apps/fo/app/store/flowRate/index.tsx b/apps/fo/app/store/flowRate/index.tsx new file mode 100644 index 000000000..38cb4e2e6 --- /dev/null +++ b/apps/fo/app/store/flowRate/index.tsx @@ -0,0 +1,368 @@ +import { getFetch } from "@/app/components/api"; +import type { TableUpperProps } from "../dashboard"; +import DataForm from "@/app/components/common/DataForm"; + +/**==================================== + * Store - 타입 + ====================================*/ + +/** + * (1) 메인페이지 + */ +const FlowRateSearchLabelArray:string[] = ['구분', '시도', '시군구', '용량별', '시설명', '조회기간', '시설상태', '권역', '단위유역', '중권역', '환경청별']; + +export type flowRateListProps = { + bscFctCd : string, + sido : string, + gugun : string, + bscFctNm : string, + daddr : string, + fctCpc : number, + lclgvrnmntNm : string, + totInflAmnt : number | '', + inflAmnt : number | '', + sumFlow : number | '', + returnAmnt : number | '', + efflAmnt : number | '', + rgnNm : string, + rvbsn : string, + lgbsn : string, + mdmbsn : string, + infoRnDay : number | '', +} + +export interface ExcelType { + 시설코드: string, + 권역:string|'', + 단위유역:string|'', + 중권역:string|'', + 시도: string, + 시군구: string, + 시설이름: string, + 주소: string, + 용량: number, + 총유입량: number | '', + 하수유입량: number | '', + 연계처리량: number | '', + 총인반류수량: number | '', + 방류량: number | '', + 환경청:string|'', +} + +export type modalExcelType = { + "날짜": string, + "총유입량(m³/일)": number | '', + "하수유입량(m³/일)": number | '', + "연계처리량(m³/일)": number | '', + "총인반류수량(m³/일)": number | '', + "방류량(m³/일)": number | '', + /* "강우량(mm/일)": number | string | '', */ +} +/** + * (2) 모달 + */ +export interface FlowRateModalInterface { + msrDt : string, + totInflAmnt : number | '', + inflAmnt : number | '', + sumFlow : number | '', + returnAmnt : number | '', + efflAmnt : number | '', + infoRnDay : number | string | '', + rainEffectDay : number | string | '' , +} +export type FlowRateModalProps = { + msrDt : string, + totInflAmnt : number, + inflAmnt : number, + sumFlow : number, + returnAmnt : number, + efflAmnt : number, + infoRnDay : number | string | '', + rainEffectDay : number | string | '', +} + +/**==================================== + * Store - data + ====================================*/ + + /** + * (1) 메인페이지 + */ +// 리스트 (행정별) +const upHeadList1:TableUpperProps[] = [ + { id: "bscFctCd", title: "시설코드", row:0, rowSpan:2}, + { id: "A", title: "행정명", row: 0 ,colSpan: 2 }, + { id: "sido", title: "시도", upName:"upChangeRe", row:1 }, + { id: "gugun", title: "시군구", upName:"upChangeRe", row:1 }, + { id: "bscFctNm", title: "시설명", row: 0, rowSpan: 2}, + { id: "daddr", title: "주소", row: 0, rowSpan: 2}, + { id: "fctCpc", title: "시설용량\n(m³/일)", align:"right", row: 0, rowSpan: 2}, + { id: "totInflAmnt", title: "총 유입입량\n(m³/일)", align:"right", row: 0, rowSpan: 2}, + { id: "inflAmnt", title: "하수유입량\n(m³/일)", align:"right", row: 0, rowSpan: 2}, + { id: "sumFlow", title: '연계처리량\n(m³/일)', align:"right", row: 0, rowSpan: 2}, + { id: "returnAmnt", title: '총인 반류수량\n(m³/일)', align:"right", row: 0, rowSpan: 2}, + { id: "efflAmnt", title: '방류량\n(m³/일)', align:"right", row: 0, rowSpan: 2} +]; + +// 리스트 (권역별) +const upHeadList2:TableUpperProps[] = [ + { id: "bscFctCd", title: "시설코드", row:0, rowSpan:2}, + { id: "A", title: "유역", row: 0, colSpan: 3}, + { id: "rvbsn", title: "권역", upName:"upChangeRe", row:1 }, + { id: "lgbsn", title: "단위유역", upName:"upChangeRe", row:1 }, + { id: "mdmbsn", title: "중권역", upName:"upChangeRe", row:1 }, + { id: "bscFctNm", title: "시설명", row: 0, rowSpan: 2}, + { id: "daddr", title: "주소", row: 0, rowSpan: 2}, + { id: "fctCpc", title: "시설용량\n(m³/일)", align:"right", row: 0, rowSpan: 2}, + { id: "totInflAmnt", title: "총 유입입량\n(m³/일)", align:"right", row: 0, rowSpan: 2}, + { id: "inflAmnt", title: "하수유입량\n(m³/일)", align:"right", row: 0, rowSpan: 2}, + { id: "sumFlow", title: '연계처리량\n(m³/일)', align:"right", row: 0, rowSpan: 2}, + { id: "returnAmnt", title: '총인 반류수량\n(m³/일)', align:"right", row: 0, rowSpan: 2}, + { id: "efflAmnt", title: '방류량\n(m³/일)', align:"right", row: 0, rowSpan: 2} +]; + +// 리스트 (환경청별별) +const upHeadList3:TableUpperProps[] = [ + { id: "bscFctCd", title: "시설코드", row:0, rowSpan:2 }, + { id: "A", title: "유역", row: 0, colSpan: 3 }, + { id: "rvbsn", title: "권역", upName:"upChangeRe1", row:1 }, + { id: "lgbsn", title: "단위유역", upName:"upChangeRe1", row:1 }, + { id: "mdmbsn", title: "중권역", upName:"upChangeRe1", row:1 }, + { id: "rgnNm", title: "환경청", row:0, rowSpan:2 }, + { id: "B", title: "행정명", row: 0 ,colSpan: 2 }, + { id: "sido", title: "시도", upName:"upChangeRe2", row:1 }, + { id: "gugun", title: "시군구", upName:"upChangeRe2", row:1 }, + { id: "bscFctNm", title: "시설명", row: 0, rowSpan: 2 }, + { id: "daddr", title: "주소", row: 0, rowSpan: 2 }, + { id: "fctCpc", title: "시설용량\n(m³/일)", align:"right", row: 0, rowSpan: 2 }, + { id: "totInflAmnt", title: "총 유입입량\n(m³/일)", align:"right", row: 0, rowSpan: 2 }, + { id: "inflAmnt", title: "하수유입량\n(m³/일)", align:"right", row: 0, rowSpan: 2 }, + { id: "sumFlow", title: '연계처리량\n(m³/일)', align:"right", row: 0, rowSpan: 2 }, + { id: "returnAmnt", title: '총인 반류수량\n(m³/일)', align:"right", row: 0, rowSpan: 2 }, + { id: "efflAmnt", title: '방류량\n(m³/일)', align:"right", row: 0, rowSpan: 2 } +]; + +const flowRateList:flowRateListProps[] = [ +] + +/** + * (2) 모달 + */ + +const upHeadListFlowRateModal:TableUpperProps[] = [ + { id: "msrDt", title: "날짜"}, + { id: "totInflAmnt", title: "총 유입량(m³/일)", align:"right"}, + { id: "inflAmnt", title: "하수유입량(m³/일)", align:"right"}, + { id: "sumFlow", title: "연계처리량(m³/일)", align:"right"}, + { id: "returnAmnt", title: "총인 반류수량(m³/일)", align:"right"}, + { id: "efflAmnt", title: "방류량(m³/일)", align:"right"}, + /* { id: "infoRnDay", title: "강우량(mm/일)", align:"right"} */ +] + +const modalFlowRateListYear:FlowRateModalProps[] = [ +] + +/**==================================== + * Store - API + ====================================*/ +const fetchFlowRateList = async (pageNo: number, pageSize: number, searchYear: string, partCd?: string, nameCd?: string, fcltCpct?: string, operationValue?: string, fcltyNm?: string) => { + try { + const tailStr = partCd ? '/'+ partCd + '/' + nameCd + '/' + fcltCpct + '/' + operationValue + (fcltyNm && fcltyNm !== '' ? '/' + fcltyNm : '') : ''; const url = '/fclt/flow/' + pageNo + '/' + pageSize + '/' + searchYear + tailStr; + const res = await getFetch('fo', url); + console.log(url); + return { data: res.data, count: res.count } + } catch (error) { + console.error('시설정보 유량현황 데이터를 가져오는데 실패했습니다:', error); + return { data: [], count: 0 }; + } +} + +const fetchFlowRateModalList = async (bscFctCd: string, searchYear: string) => { + try { + const url = '/fclt/flow/' + bscFctCd + '/' + searchYear; + const res = await getFetch('fo', url); + + // msrDt 오름차순으로 정렬 + const sortedData = res.data.sort((a: FlowRateModalInterface, b: FlowRateModalInterface) => { + return new Date(a.msrDt).getTime() - new Date(b.msrDt).getTime(); + }); + + return { data: sortedData } + } catch (error) { + console.error('시설정보 유량현황 모달 데이터를 가져오는데 실패했습니다:', error); + return { data: [] } + } +} + + +/**==================================== + * Store - expaort + ====================================*/ + +export type FlowRateSearchActions = { + searchFlowRateList: (pageNo: number, pageSize: number, searchYear: string, part?: string, nameCd?: string, fcltCpct?: string, operationValue?: string, fcltyNm?: string) => void; + + searchFlowRateModalList : (bscFctCd: string, searchYear: string) => void; +} + +export type FlowRateSearchType = { + FlowRateSearch: { + isInit: boolean; + flowRateSearchLabelArray:string[]; + }, + + FlowRateList : { + isInit : boolean; + upHeadList1: TableUpperProps[]; + upHeadList2: TableUpperProps[]; + upHeadList3: TableUpperProps[]; + flowRateList:flowRateListProps[]; + flowRateCount: number; + excelList: ExcelType[]; + }, + + FlowRateModal : { + isInit : boolean; + modalFlowRateListYear : FlowRateModalProps[]; + upHeadListFlowRateModalYear : TableUpperProps[]; + } +} + +export type FlowRateSearchStore = FlowRateSearchType & FlowRateSearchActions; + +const dataFormSetting=(item : flowRateListProps | FlowRateModalProps)=> { + DataForm({ type: 'date', column: 'msrDt', item: item as unknown as Record<string, unknown> }); + DataForm({ type: 'float1', column: 'totInflAmnt', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float1', column: 'inflAmnt', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float1', column: 'sumFlow', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float1', column: 'efflWqltBod', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float1', column: 'returnAmnt', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float1', column: 'efflAmnt', item: item as unknown as Record<string, unknown>}); +} + +export const flowRateSearchInitState:FlowRateSearchType = { + FlowRateSearch: { + isInit: false, + flowRateSearchLabelArray: FlowRateSearchLabelArray, + }, + FlowRateList : { + isInit : false, + upHeadList1 : upHeadList1, + upHeadList2 : upHeadList2, + upHeadList3 : upHeadList3, + flowRateList : flowRateList, + flowRateCount : 0, + excelList: [] + }, + FlowRateModal : { + isInit : false, + upHeadListFlowRateModalYear : upHeadListFlowRateModal, + modalFlowRateListYear : modalFlowRateListYear, + } +} + +export const flowRateSearchReducer:(set:(fn: (state: FlowRateSearchType) => FlowRateSearchType) => void)=>FlowRateSearchActions=(set: (fn: (state: FlowRateSearchType) => FlowRateSearchType) => void) => { + return { + searchFlowRateList: async (pageNo: number, pageSize: number, searchYear: string, partCd?: string, nameCd?: string, fcltCpct?: string, operationValue?: string, fcltyNm?: string) => { + const res = await fetchFlowRateList(pageNo, pageSize, searchYear, partCd, nameCd, fcltCpct, operationValue, fcltyNm); + const data = res.data; + const excelDownList: ExcelType[] = []; + const rtnData = data.map((item: flowRateListProps) => { + item.fctCpc = parseFloat(item.fctCpc.toString().replace(/,/g, '')); + item.totInflAmnt = Number(item.inflAmnt || 0) + Number(item.sumFlow || 0) + Number(item.returnAmnt || 0) + dataFormSetting(item); + return item; + }); + + if(pageSize>5000){ + rtnData.forEach((item: flowRateListProps) => { + excelDownList.push({ + 시설코드: item.bscFctCd, + 권역: item.rvbsn, + 단위유역: item.lgbsn, + 중권역: item.mdmbsn, + 환경청: item.rgnNm, + 시도: item.sido, + 시군구: item.gugun, + 시설이름: item.bscFctNm, + 주소: item.daddr, + 용량: item.fctCpc, + 총유입량: item.totInflAmnt, + 하수유입량: item.inflAmnt, + 연계처리량: item.sumFlow, + 총인반류수량: item.returnAmnt, + 방류량: item.efflAmnt, + }); + }); + } + + set((state: FlowRateSearchType) => ({ + ...state, + FlowRateList: pageSize<5000?{ + ...state.FlowRateList, + flowRateList: rtnData, + flowRateCount: res.count, + }:{ + ...state.FlowRateList, + excelList: excelDownList, + } + })) + }, + searchFlowRateModalList: async (bscFctCd: string, searchYear: string) => { + const res = await fetchFlowRateModalList(bscFctCd, searchYear); + const data = res.data; + const tmpData:FlowRateModalProps[] = []; + + data.forEach((item: FlowRateModalProps) => { + if(item.infoRnDay && item.infoRnDay.toString().trim() === 'x') { + item.infoRnDay = ''; + } + }); + + // facilityPrecipitation의 rainEffectDay 계산 로직을 적용 + for(let idx = 0; idx < data.length; idx++) { + const item = data[idx]; + const rainDay = Number(item.infoRnDay) >= 3 ? 1 : 0; + let rainEffectDay = 0; + + if(idx < data.length - 2) { + if(rainDay == 1) { + rainEffectDay = 1; + } else { + if(idx >= 2) { + if(Number(data[idx-1].infoRnDay) >= 3 || Number(data[idx-2].infoRnDay) >= 3) { + rainEffectDay = 1; + } + } + } + } else if(idx < data.length - 1) { + if(rainDay == 1 || Number(data[idx+1].infoRnDay) >= 3) { + rainEffectDay = 1; + } + } else { + rainEffectDay = rainDay; + } + + tmpData.push({ + ...item, + rainEffectDay: rainEffectDay, + }); + } + + const rtnData = tmpData.map((item: FlowRateModalProps) => { + item.totInflAmnt = Number(item.inflAmnt || 0) + Number(item.sumFlow || 0) + Number(item.returnAmnt || 0) + dataFormSetting(item); + return item; + }); + + set((state: FlowRateSearchType) => ({ + ...state, + FlowRateModal: { + ...state.FlowRateModal, + modalFlowRateListYear: rtnData || [], + } + })) + } + } +} \ No newline at end of file diff --git a/apps/fo/app/store/index.tsx b/apps/fo/app/store/index.tsx new file mode 100644 index 000000000..bd684542b --- /dev/null +++ b/apps/fo/app/store/index.tsx @@ -0,0 +1,45 @@ +'use client' + +import { type ReactNode, createContext, useContext, useRef } from 'react' +import { useStore } from 'zustand' +import { type FoStore, 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>() + + if (!storeRef.current) { + const store = StoreZus() + storeRef.current = store + } + + return ( + <StoreContext.Provider value={storeRef.current}> + {children} + </StoreContext.Provider> + ) +} + +export const useFoStore = <T,>( + selector: (store: FoStore) => 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/fo/app/store/planSewage/index.tsx b/apps/fo/app/store/planSewage/index.tsx new file mode 100644 index 000000000..e9153305a --- /dev/null +++ b/apps/fo/app/store/planSewage/index.tsx @@ -0,0 +1,226 @@ +import DataForm from "@/app/components/common/DataForm"; +import type { TableUpperProps } from "../dashboard"; +import { getFetch } from "@/app/components/api"; +export type { TableUpperProps }; +/**==================================== + * Store - 타입 + ====================================*/ + +/** + * (1) 메인페이지 + */ +// 시설별 강수현황 +type PlanSewageProps = { + bscFctCd : string, + sido : string, + gugun : string, + bscFctNm : string, + daddr : string, + fctCpc : number, + spareCapacity : number, + variableLoadPercent : number, + classifiedSewerPercent : number, + rgnNm : string, + rvbsn : string, + lgbsn : string, + mdmbsn : string, + sprtProcZoneAr : number, + procZoneSm : number, + avgInflAmnt : number, + avgInflAmntSunny : number, + maxInflAmntSunny : number, +} + +/**==================================== + * Store - data + ====================================*/ + +/** + * (1) 메인페이지 + */ +// 검색단 +const LabelArray:string[] = ['구분', '시도', '시군구','용량별', '시설명', '조회기간', '시설상태', '권역', '단위유역', '중권역', '환경청별']; + +// 리스트 헤더 +const upHeadList1:TableUpperProps[] = [ + { id: "bscFctCd", title: "시설코드", row:0, rowSpan:2 }, + { id: "A", title: "행정구역", upSequnce:1, row:0, colSpan:2 }, + { id: "sido", title: "시도", upName:"upChangeRe", upSequnce:1, row:1 }, + { id: "gugun", title: "시군구", upName:"upChangeRe",upSequnce:2, row:1 }, + { id: "bscFctNm", title: "시설명", row:0, rowSpan:2 }, + { id: "daddr", title: "주소", row:0, rowSpan:2 }, + { id: "fctCpc", title: "시설용량(m³/일)", row:0, rowSpan:2, align:'right' }, + { id: "spareCapacity", title: "여유용량(m³/일)", row:0, rowSpan:2, align:'right' }, + { id: "variableLoadPercent", title: "변동부하율(%)", row:0, rowSpan:2, align:'right' }, + { id: "classifiedSewerPercent", title: "분류식화율(%)", row:0, rowSpan:2, align:'right' }, +]; + +const upHeadList2:TableUpperProps[] = [ + { id: "bscFctCd", title: "시설코드", row:0, rowSpan:2 }, + { id: "A", title: "유역", upName:"upChangeRe", upSequnce:1, row:0, colSpan:3 }, + { id: "rvbsn", title: "권역", upName:"upChangeRe", upSequnce:1, row:1}, + { id: "lgbsn", title: "단위유역", upName:"upChangeRe", upSequnce:2, row:1}, + { id: "mdmbsn", title: "중권역", upName:"upChangeRe", upSequnce:3, row:1}, + { id: "bscFctNm", title: "시설명", row:0, rowSpan:2 }, + { id: "daddr", title: "주소", row:0, rowSpan:2 }, + { id: "fctCpc", title: "시설용량(m³/일)", row:0, rowSpan:2, align:'right' }, + { id: "spareCapacity", title: "여유용량(m³/일)", row:0, rowSpan:2, align:'right' }, + { id: "variableLoadPercent", title: "변동부하율(%)", row:0, rowSpan:2, align:'right' }, + { id: "classifiedSewerPercent", title: "분류식화율(%)", row:0, rowSpan:2, align:'right' }, +]; + +const upHeadList3:TableUpperProps[] = [ + { id: "bscFctCd", title: "시설코드", row:0, rowSpan:2 }, + { id: "A", title: "유역", upName:"upChangeRe", upSequnce:1, row:0, colSpan:3 }, + { id: "rvbsn", title: "권역", upName:"upChangeRe1", upSequnce:1, row:1}, + { id: "lgbsn", title: "단위유역", upName:"upChangeRe1", upSequnce:2, row:1}, + { id: "mdmbsn", title: "중권역", upName:"upChangeRe1", upSequnce:3, row:1}, + { id: "rgnNm", title: "환경청", row:0, rowSpan:2 }, + { id: "B", title: "행정구역", upName:"upChangeRe", upSequnce:1, row:0, colSpan:2 }, + { id: "sido", title: "시도", upName:"upChangeRe2", upSequnce:1, row:1 }, + { id: "gugun", title: "시군구", upName:"upChangeRe2",upSequnce:2, row:1 }, + { id: "bscFctNm", title: "시설명", row:0, rowSpan:2 }, + { id: "daddr", title: "주소", row:0, rowSpan:2 }, + { id: "fctCpc", title: "시설용량(m³/일)", row:0, rowSpan:2, align:'right' }, + { id: "spareCapacity", title: "여유용량(m³/일)", row:0, rowSpan:2, align:'right' }, + { id: "variableLoadPercent", title: "변동부하율(%)", row:0, rowSpan:2, align:'right' }, + { id: "classifiedSewerPercent", title: "분류식화율(%)", row:0, rowSpan:2, align:'right' }, +]; + +const planSewageList:PlanSewageProps[] = [ +] + +/**==================================== + * Store - api + ====================================*/ + const fetchPlanSewageList = async (pageNo: number, pageSize: number, searchYear: string, partCd?: string, nameCd?: string, fcltCpct?: string, operationValue?: string, fcltyNm?: string) => { + const tailStr = partCd ? '/'+ partCd + '/' + (nameCd || '0') + '/' + (fcltCpct || '0') + '/' + (operationValue || '0') +'/0'+ (fcltyNm && fcltyNm !== '' ? '/' + fcltyNm : '') : ''; + const url = '/prcsFclt/info/plan/' + pageNo + '/' + pageSize + '/' + searchYear + tailStr; + console.log(url); + const res = await getFetch('fo', url); + return { data: res.data, count: res.count } + } + +/**==================================== + * Store - expaort + ====================================*/ +export type ExcelType = { + 시설코드: string; + 권역:string|'', + 단위유역:string|'', + 중권역:string|'', + 환경청:string|'', + 시도: string; + 시군구: string; + 시설이름: string; + 주소: string; + 시설용량: number; + 여유용량: number; + 변동부하율 : number; + 분류식화율 : number; +} + +const dataFormSetting=(item : PlanSewageProps)=> { + DataForm({ type: 'float1', column: 'fctCpc', item: item as unknown as Record<string, unknown> }); + DataForm({ type: 'float1', column: 'spareCapacity', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float1', column: 'variableLoadPercent', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float1', column: 'classifiedSewerPercent', item: item as unknown as Record<string, unknown>}); +}; + +export type PlanSewageActions = { + searchPlanSewageList: (pageNo: number, pageSize: number, searchYear: string, part?: string, nameCd?: string, fcltCpct?: string, operationValue?: string, fcltyNm?: string) => void; +} + +export type PlanSewageType = { + PlanSewageSearch: { + isInit: boolean; + labelArray:string[]; + }, + + PlanSewage : { + isInit: boolean; + upHeadList1: TableUpperProps[]; + upHeadList2: TableUpperProps[]; + upHeadList3: TableUpperProps[]; + planSewageList: PlanSewageProps[]; + planSewageCount: number; + excelList: ExcelType[]; + }, +} + +export const planSewageInitState: PlanSewageType = { + PlanSewageSearch: { + isInit: false, + labelArray: LabelArray, + }, + + PlanSewage : { + isInit : false, + upHeadList1 : upHeadList1, + upHeadList2 : upHeadList2, + upHeadList3 : upHeadList3, + planSewageList : planSewageList, + planSewageCount : 0, + excelList : [] + }, +} + +export type PlanSewageStore = PlanSewageType & PlanSewageActions; + +export const planSewageReducer:(set:(fn: (state: PlanSewageType) => PlanSewageType) => void)=>PlanSewageActions=(set: (fn: (state: PlanSewageType) => PlanSewageType) => void) => { + return { + searchPlanSewageList: async (pageNo: number, pageSize: number, searchYear: string, partCd?: string, nameCd?: string, fcltCpct?: string, operationValue?: string, fcltyNm?: string) => { + const res = await fetchPlanSewageList(pageNo, pageSize, searchYear, partCd, nameCd, fcltCpct, operationValue, fcltyNm); + const excelDownList: ExcelType[] = []; + console.log('res' , res.data); + /** + * 결과데이터 계산 + * (1) 여유용량 = 시설용량 - 건기시 평균유량 + * (2) 변동부하율 = 건기시 일최대 유량/일평균유량(기준 최근 3개년) + * (3) 분류식화율 = 분류식지역 면적(㎢)/처리구역 면적합계(㎢) ×100 + */ + const resultData = res.data.map((item:PlanSewageProps) => { + item.spareCapacity = Number(item.fctCpc) - Number(item.avgInflAmntSunny); + item.variableLoadPercent = item.maxInflAmntSunny && item.maxInflAmntSunny!==0 ? Number(item.maxInflAmntSunny) / Number(item.avgInflAmntSunny) : 0; + item.classifiedSewerPercent = item.sprtProcZoneAr && item.sprtProcZoneAr!==0 ? Number(item.sprtProcZoneAr) / Number(item.procZoneSm) * 100 : 0; + return item; + }); + + resultData.forEach((item:PlanSewageProps) => { + dataFormSetting(item) + }) + + if(pageSize>5000){ + resultData.forEach((item:PlanSewageProps) => { + excelDownList.push({ + 시설코드: item.bscFctCd, + 권역: item.lgbsn, + 단위유역: item.rvbsn, + 중권역: item.mdmbsn, + 환경청: item.rgnNm, + 시설이름: item.bscFctNm, + 시도: item.sido, + 시군구: item.gugun, + 주소: item.daddr, + 시설용량: item.fctCpc, + 여유용량: item.spareCapacity, + 변동부하율: item.variableLoadPercent, + 분류식화율: item.classifiedSewerPercent, + }) + }) + } + + set((state:PlanSewageType) => ({ + ...state, + PlanSewage: pageSize<5000?{ + ...state.PlanSewage, + planSewageList: resultData, + planSewageCount: res.count, + }:{ + ...state.PlanSewage, + excelList: excelDownList, + } + })) + } + } +} \ No newline at end of file diff --git a/apps/fo/app/store/processArea/index.tsx b/apps/fo/app/store/processArea/index.tsx new file mode 100644 index 000000000..a43777b1c --- /dev/null +++ b/apps/fo/app/store/processArea/index.tsx @@ -0,0 +1,507 @@ +import { getFetchAreaList, postFetch } from "@/app/components/api"; +import type { TableUpperProps } from "../dashboard"; +import DataForm from "@/app/components/common/DataForm"; +export type { TableUpperProps }; +/**==================================== + * Store - 타입 + ====================================*/ + +/** + * (1) 메인페이지 + */ +// 처리구역 리스트 +export type ProcessAreaProps = { + swtrmtFcltCd : string, + sido : string, + gugun : string, + swtrmtFcltNm : string, + location : string, + fctCpc : number | '', + ruse : boolean, + yrDt4 : string, + yr : string, + yrDc1 : string, + yrDc2 : string, + yrDc3 : string, + yrDc4 : string, + /** + * 증설계획 + */ + // 기승인(당초4단계), 현재, 변경1~4 + planSwageQntSumDt4 : number | '', + planSwageQntSum : number | '', + planSwageQntSumDc1 : number | '', + planSwageQntSumDc2 : number | '', + planSwageQntSumDc3 : number | '', + planSwageQntSumDc4 : number | '', + /** + * 처리인구 + */ + // 기승인(당초4단계), 변경1~4 + swtrmtPpltnCntDt4 : number | '', + swtrmtPpltnCnt : number | '', + swtrmtPpltnCntDc1 : number | '', + swtrmtPpltnCntDc2 : number | '', + swtrmtPpltnCntDc3 : number | '', + swtrmtPpltnCntDc4 : number | '', + /** + * 처리구역면적 + */ + // 기승인(당초4단계), 변경1~4 + swtrmtZoneAreaDt4 : number | '', + swtrmtZoneArea : number | '', + swtrmtZoneAreaDc1 : number | '', + swtrmtZoneAreaDc2 : number | '', + swtrmtZoneAreaDc3 : number | '', + swtrmtZoneAreaDc4 : number | '', + // 처리구역 + trtDstr : string | '', +} + +export interface ExcelType { + 시설코드: string, + 시설명: string, + 주소: string, + 시설용량: number | '', + 증설계획기존: number | '', + 증설계획현재: number | '', + 증설계획1단계: number | '', + 증설계획2단계: number | '', + 증설계획3단계: number | '', + 증설계획4단계: number | '', + 처리인구기존: number | '', + 처리인구현재: number | '', + 처리인구1단계: number | '', + 처리인구2단계: number | '', + 처리인구3단계: number | '', + 처리인구4단계: number | '', + 처리구역기존: number | '', + 처리구역현재: number | '', + 처리구역1단계: number | '', + 처리구역2단계: number | '', + 처리구역3단계: number | '', + 처리구역4단계: number | '', + 처리구역: string | '' +} + +// (2) 처리구역 팝업 +// 시설증설 계획 +export type FcltyImproveInfoProps = { + planSwageQntSumDt4 : number | '', + planSwageQntSum : number | '', + planSwageQntSumDc1 : number | '', + planSwageQntSumDc2 : number | '', + planSwageQntSumDc3 : number | '', + planSwageQntSumDc4 : number | '', +} + +// 처리인구 계획 +export type ProcessPopulationProps = { + swtrmtPpltnCntDt4 : number | '', + swtrmtPpltnCnt : number | '', + swtrmtPpltnCntDc1 : number | '', + swtrmtPpltnCntDc2 : number | '', + swtrmtPpltnCntDc3 : number | '', + swtrmtPpltnCntDc4 : number | '', +} +// 처리구역 계획 +export type ProcessAreaPlanProps = { + swtrmtZoneAreaDt4 : number | '', + swtrmtZoneArea : number | '', + swtrmtZoneAreaDc1 : number | '', + swtrmtZoneAreaDc2 : number | '', + swtrmtZoneAreaDc3 : number | '', + swtrmtZoneAreaDc4 : number | '', + trtDstr : string, +} + +/**==================================== + * Store - data + ====================================*/ + +/** + * (1) 메인페이지 + */ +// 검색단 +const ProcessFacilityLabelArray:string[] = ['구분', '시도', '시군구','용량별', '시설명', '시설상태', '연도별', '권역', '단위유역', '중권역', '환경청별']; + +// 리스트 헤더(행정별) +const upHeadList1:TableUpperProps[] = [ + { id: "swtrmtFcltCd", title: "시설코드", rowSpan:3, row:0}, + { id: "A", title: "행정구역", colSpan:2, rowSpan:2, row:0}, + { id: "sido", title: "시도", upName:"upChangeRe", row:2 }, + { id: "sigungo", title: "시군구", upName:"upChangeRe", row:2 }, + { id: "swtrmtFcltNm", title: "시설명", rowSpan:3, row:0}, + { id: "location", title: "주소", rowSpan:3, row:0, }, + { id: "fctCpc", title: "시설용량(m³/일)", align:"right", rowSpan:3, row:0}, + { id: "B", title: "시설계획", colSpan:19, row:0}, + { id: "C", title: "증설계획(m³/일)", colSpan:6, row:1}, + { id: "planSwageQntSumDt4", title: "기승인\n4단계", row:2}, + { id: "planSwageQntSum", title: "현재", row:2}, + { id: "planSwageQntSumDc1", title: "1단계", row:2}, + { id: "planSwageQntSumDc2", title: "2단계", row:2}, + { id: "planSwageQntSumDc3", title: "3단계", row:2}, + { id: "planSwageQntSumDc4", title: "4단계", row:2}, + { id: "D", title: "처리인구(인)", colSpan:6, row:1}, + { id: "swtrmtPpltnCntDt4", title: "기승인\n4단계", row:2}, + { id: "swtrmtPpltnCnt", title: "현재", row:2}, + { id: "swtrmtPpltnCntDc1", title: "1단계", row:2}, + { id: "swtrmtPpltnCntDc2", title: "2단계", row:2}, + { id: "swtrmtPpltnCntDc3", title: "3단계", row:2}, + { id: "swtrmtPpltnCntDc4", title: "4단계", row:2}, + { id: "E", title: "처리구역(km²)", colSpan:7, row:1}, + { id: "swtrmtZoneAreaDt4", title: "기승인\n4단계", row:2}, + { id: "swtrmtZoneArea", title: "현재", row:2}, + { id: "swtrmtZoneAreaDc1", title: "1단계", row:2}, + { id: "swtrmtZoneAreaDc2", title: "2단계", row:2}, + { id: "swtrmtZoneAreaDc3", title: "3단계", row:2}, + { id: "swtrmtZoneAreaDc4", title: "4단계", row:2}, + { id: "trtDstr", title: "처리구역", row:2} +]; + +const processAreaList:ProcessAreaProps[] = [ +] + +/** + * 처리구역정보 상세팝업 + */ +// 시설증설 계획 (헤더) +const upHeadListFcltyImproveInfo:TableUpperProps[] = [ + { id: "planSwageQntSumDt4", title: "기승인\n4단계(m³/일)", rowSpan:2, row:0}, + { id: "A", title: "증설계획(m³/일)", colSpan:5, row:0}, + { id: "planSwageQntSum", title: "현재", row:1}, + { id: "planSwageQntSumDc1", title: "1단계", row:1}, + { id: "planSwageQntSumDc2", title: "2단계", row:1}, + { id: "planSwageQntSumDc3", title: "3단계", row:1}, + { id: "planSwageQntSumDc4", title: "4단계", row:1}, +]; + +// 시설증설 계획 (리스트) +const gridListFcltyImproveInfo:FcltyImproveInfoProps[] = [ +]; + +// 처리인구 계획 (헤더) +const upHeadListProcessPopulation:TableUpperProps[] = [ + { id: "swtrmtPpltnCntDt4", title: "기승인\n4단계(인)", rowSpan:2, row:0}, + { id: "A", title: "처리인구(인)", colSpan:5, row:0}, + { id: "swtrmtPpltnCnt", title: "현재", row:1}, + { id: "swtrmtPpltnCntDc1", title: "1단계", row:1}, + { id: "swtrmtPpltnCntDc2", title: "2단계", row:1}, + { id: "swtrmtPpltnCntDc3", title: "3단계", row:1}, + { id: "swtrmtPpltnCntDc4", title: "4단계", row:1}, +]; + +// 처리인구 계획 (리스트) +const gridListProcessPopulation:ProcessPopulationProps[] = [ +]; + +// 처리구역 계획 (헤더) +const upHeadListProcessAreaPlan:TableUpperProps[] = [ + { id: "swtrmtZoneAreaDt4", title: "기승인\n4단계(km²)", rowSpan:2, row:0}, + { id: "A", title: "처리면적(km²)", colSpan:6, row:0}, + { id: "swtrmtZoneArea", title: "현재", row:1}, + { id: "swtrmtZoneAreaDc1", title: "1단계", row:1}, + { id: "swtrmtZoneAreaDc2", title: "2단계", row:1}, + { id: "swtrmtZoneAreaDc3", title: "3단계", row:1}, + { id: "swtrmtZoneAreaDc4", title: "4단계", row:1}, + { id: "trtDstr", title: "처리구역", row:1} +]; + +// 시설증설 계획 (리스트) +const gridListProcessAreaPlan:ProcessAreaPlanProps[] = [ +]; + +/**==================================== + * Store - API +=====================================*/ +// 시설 상세 정보 조회 API +const fetchFacilityDetailInfo = async (swtrmtFcltCd: string) => { + try { + const tailUrl = '/1/5/A/0/0/0/SW'; + const res = await postFetch('fo', '/prcsFclt/ruseInfo'+tailUrl, [swtrmtFcltCd]); + return { data:res.data, count:res.count }; + } catch (error) { + console.error('시설정보 데이터를 가져오는데 실패했습니다:', error); + return {data: [], count: 0}; + } +}; + +// 시설정보 리스트 데이터 호출(구분 선택 후 시설명 있을 경우) +const fetchProcessAreaList = async (pageNo : number, pageSize : number, ctpvNm : string | undefined, sggNm : string | undefined, targetYear : string | undefined ): Promise<{data:ProcessAreaProps[], count:number}> => { + try { + targetYear = targetYear ? targetYear : "2024"; + const baseUrl = 'https://apigateway.rsonesoft.com/dc-service/api/v1/nowDmgMtncGnsvy'; + const queryParams = new URLSearchParams(); + + if (ctpvNm && ctpvNm !== '전체') queryParams.append('ctpvNm', ctpvNm); + if (sggNm && sggNm !== '전체') queryParams.append('sggNm', sggNm); + if (targetYear) queryParams.append('targetYear',targetYear); + + const queryString = queryParams.toString(); + const url = queryString ? `${baseUrl}?${queryString}` : baseUrl; + const res = await getFetchAreaList(url); + + if(res && res.length > 0){ + const enrichedData = await Promise.all( + res.map(async (item: ProcessAreaProps) => { + upHeadList1.forEach((head:TableUpperProps) => { + if (head.id === 'planSwageQntSum' || head.id === 'swtrmtPpltnCnt' || head.id === 'swtrmtZoneArea') { + head.title = '현재\n(' + item.yr + '년)'; + } else if(head.id === 'planSwageQntSumDc1' || head.id === 'swtrmtPpltnCntDc1' || head.id === 'swtrmtZoneAreaDc1') { + head.title = '1단계\n(' + item.yrDc1 + '년)'; + } else if(head.id === 'planSwageQntSumDc2' || head.id === 'swtrmtPpltnCntDc2' || head.id === 'swtrmtZoneAreaDc2') { + head.title = '2단계\n(' + item.yrDc2 + '년)'; + } else if(head.id === 'planSwageQntSumDc3' || head.id === 'swtrmtPpltnCntDc3' || head.id === 'swtrmtZoneAreaDc3') { + head.title = '3단계\n(' + item.yrDc3 + '년)'; + } else if(head.id === 'planSwageQntSumDc4' || head.id === 'swtrmtPpltnCntDc4' || head.id === 'swtrmtZoneAreaDc4') { + head.title = '4단계\n(' + item.yrDc4 + '년)'; + } else if(head.id === 'planSwageQntSumDt4' || head.id === 'swtrmtPpltnCntDt4' || head.id === 'swtrmtZoneAreaDt4') { + head.title = '기승인\n4단계(' + item.yrDt4 + '년)'; + } + }); + const res = await fetchFacilityDetailInfo(item.swtrmtFcltCd); + const detailInfo = res.data.filter((item:ProcessAreaProps) => item.ruse === true); + return { + ...item, + sido: detailInfo.length > 0 ? detailInfo[0].sido||'' : '', + sigungo: detailInfo.length > 0 ? detailInfo[0].gugun||'' : '', + location: detailInfo.length > 0 ? detailInfo[0].addr||'' : '', + fctCpc: detailInfo.length > 0 ? detailInfo[0].fctCpc||'' : 0, + trtDstr: detailInfo.length > 0 ? detailInfo[0].trtDstr||'' : '', + }; + }) + ); + const startIndex = (pageNo - 1) * pageSize; + const endIndex = startIndex + pageSize; + const paginatedData = enrichedData.slice(startIndex, endIndex); + return {data: paginatedData, count: enrichedData.length}; + }else{ + return {data: [], count: 0}; + } + } catch (error) { + console.error('시설계획정보를 가져오는데 실패했습니다:', error); + return {data: [], count: 0}; + } +}; + +/**==================================== + * Store - expaort +=====================================*/ +export type ProcessAreaActions = { + searchProcessAreaList : (pageNo: number, pageSize: number, ctpvNm?:string, sggNm?:string, targetYear?:string) => void; + initModalData : (swtrmtFcltCd:string) => void; +} + +export type ProcessAreaType = { + ProcessAreaSearch: { + isInit: boolean; + processFacilityLabelArray:string[]; + }, + + ProcessArea : { + isInit: boolean; + upHeadList1: TableUpperProps[]; + processAreaList: ProcessAreaProps[]; + processAreaCount: number; + excelList: ExcelType[]; + }, + + ProcessAreaDetail : { + isInit: boolean; + upHeadListFcltyImproveInfo: TableUpperProps[]; + gridListFcltyImproveInfo: FcltyImproveInfoProps[]; // Changed from facilityAreaProps to ProcessAreaProps + upHeadListProcessPopulation: TableUpperProps[]; + gridListProcessPopulation: ProcessPopulationProps[]; // Changed from facilityAreaProps to ProcessAreaProps + upHeadListProcessAreaPlan: TableUpperProps[]; + gridListProcessAreaPlan: ProcessAreaPlanProps[]; // Changed from facilityAreaProps to ProcessAreaProps + }, +} + +export const processAreaInitState: ProcessAreaType = { + ProcessAreaSearch: { + isInit: false, + processFacilityLabelArray: ProcessFacilityLabelArray, + }, + + ProcessArea : { + isInit : false, + upHeadList1 : upHeadList1, + processAreaList : processAreaList, + processAreaCount : 0, + excelList : [] + }, + + ProcessAreaDetail : { + isInit : false, + upHeadListFcltyImproveInfo : upHeadListFcltyImproveInfo, + gridListFcltyImproveInfo : gridListFcltyImproveInfo, + upHeadListProcessPopulation : upHeadListProcessPopulation, + gridListProcessPopulation : gridListProcessPopulation, + upHeadListProcessAreaPlan : upHeadListProcessAreaPlan, + gridListProcessAreaPlan : gridListProcessAreaPlan, + }, +} + +// 상태 설정 함수 타입 정의 +type SetStateFunction = (state: ProcessAreaType | ((state: ProcessAreaType) => ProcessAreaType)) => void; + +export type ProcessAreaStore = ProcessAreaType & ProcessAreaActions; + +export const processAreaReducer:(set:SetStateFunction)=>ProcessAreaActions=(set: SetStateFunction) => { + return { + /** + * 시설정보 리스트 데이터 호출 + * @param pageNo : 페이지 번호 + * @param pageSize : 페이지 크기 + */ + searchProcessAreaList: async ( pageNo:number, pageSize:number, ctpvNm?:string, sggNm?:string, targetYear?:string) => { + const res = await fetchProcessAreaList(pageNo, pageSize, ctpvNm, sggNm, targetYear); + res.data.forEach((item:ProcessAreaProps) => { + DataForm({ type: 'float1', column: 'fctCpc', item:item as unknown as Record<string, unknown>}); + }) + const excelDownList: ExcelType[] = []; + if(pageSize>5000){ + res.data.forEach(item => { + item.fctCpc = item.fctCpc ? parseFloat(item.fctCpc.toString().replace(/,/g, '')) : ''; + excelDownList.push({ + 시설코드: item.swtrmtFcltCd, + 시설명: item.swtrmtFcltNm, + 주소: item.location, + 시설용량: item.fctCpc, + 증설계획기존: item.planSwageQntSumDt4, + 증설계획현재: item.planSwageQntSum, + 증설계획1단계: item.planSwageQntSumDc1, + 증설계획2단계: item.planSwageQntSumDc2, + 증설계획3단계: item.planSwageQntSumDc3, + 증설계획4단계: item.planSwageQntSumDc4, + 처리인구기존: item.swtrmtPpltnCntDt4, + 처리인구현재: item.swtrmtPpltnCnt, + 처리인구1단계: item.swtrmtPpltnCntDc1, + 처리인구2단계: item.swtrmtPpltnCntDc2, + 처리인구3단계: item.swtrmtPpltnCntDc3, + 처리인구4단계: item.swtrmtPpltnCntDc4, + 처리구역기존: item.swtrmtZoneAreaDt4, + 처리구역현재: item.swtrmtZoneArea, + 처리구역1단계: item.swtrmtZoneAreaDc1, + 처리구역2단계: item.swtrmtZoneAreaDc2, + 처리구역3단계: item.swtrmtZoneAreaDc3, + 처리구역4단계: item.swtrmtZoneAreaDc4, + 처리구역: item.trtDstr + }); + }); + }else{ + res.data.forEach(item => { + item.fctCpc = item.fctCpc ? parseFloat(item.fctCpc.toString().replace(/,/g, '')) : ''; + }) + } + set((state:ProcessAreaType) => ({ + ...state, + ProcessArea: pageSize<5000?{ + ...state.ProcessArea, + processAreaList: res.data, + processAreaCount: res.count, + }:{ + ...state.ProcessArea, + excelList: excelDownList, + }, + })) + }, + + initModalData: async (swtrmtFcltCd:string) => { + set((state: ProcessAreaType) => { + const rtnData = state.ProcessArea.processAreaList.find((item: ProcessAreaProps) => item.swtrmtFcltCd === swtrmtFcltCd); + if (rtnData) { + upHeadListFcltyImproveInfo.forEach((head:TableUpperProps) => { + if(head.id === 'planSwageQntSum' || head.id === 'swtrmtPpltnCnt' || head.id === 'swtrmtZoneArea') { + head.title = '현재\n(' + rtnData.yr + '년)'; + } else if(head.id === 'planSwageQntSumDc1' || head.id === 'swtrmtPpltnCntDc1' || head.id === 'swtrmtZoneAreaDc1') { + head.title = '1단계\n(' + rtnData.yrDc1 + '년)'; + } else if(head.id === 'planSwageQntSumDc2' || head.id === 'swtrmtPpltnCntDc2' || head.id === 'swtrmtZoneAreaDc2') { + head.title = '2단계\n(' + rtnData.yrDc2 + '년)'; + } else if(head.id === 'planSwageQntSumDc3' || head.id === 'swtrmtPpltnCntDc3' || head.id === 'swtrmtZoneAreaDc3') { + head.title = '3단계\n(' + rtnData.yrDc3 + '년)'; + } else if(head.id === 'planSwageQntSumDc4' || head.id === 'swtrmtPpltnCntDc4' || head.id === 'swtrmtZoneAreaDc4') { + head.title = '4단계\n(' + rtnData.yrDc4 + '년)'; + } else if(head.id === 'planSwageQntSumDt4' || head.id === 'swtrmtPpltnCntDt4' || head.id === 'swtrmtZoneAreaDt4') { + head.title = '기승인\n4단계(' + rtnData.yrDt4 + '년)'; + } + }); + + upHeadListProcessAreaPlan.forEach((head:TableUpperProps) => { + if(head.id === 'planSwageQntSum' || head.id === 'swtrmtPpltnCnt' || head.id === 'swtrmtZoneArea') { + head.title = '현재\n(' + rtnData.yr + '년)'; + } else if(head.id === 'planSwageQntSumDc1' || head.id === 'swtrmtPpltnCntDc1' || head.id === 'swtrmtZoneAreaDc1') { + head.title = '1단계\n(' + rtnData.yrDc1 + '년)'; + } else if(head.id === 'planSwageQntSumDc2' || head.id === 'swtrmtPpltnCntDc2' || head.id === 'swtrmtZoneAreaDc2') { + head.title = '2단계\n(' + rtnData.yrDc2 + '년)'; + } else if(head.id === 'planSwageQntSumDc3' || head.id === 'swtrmtPpltnCntDc3' || head.id === 'swtrmtZoneAreaDc3') { + head.title = '3단계\n(' + rtnData.yrDc3 + '년)'; + } else if(head.id === 'planSwageQntSumDc4' || head.id === 'swtrmtPpltnCntDc4' || head.id === 'swtrmtZoneAreaDc4') { + head.title = '4단계\n(' + rtnData.yrDc4 + '년)'; + } else if(head.id === 'planSwageQntSumDt4' || head.id === 'swtrmtPpltnCntDt4' || head.id === 'swtrmtZoneAreaDt4') { + head.title = '기승인\n4단계(' + rtnData.yrDt4 + '년)'; + } + }); + + upHeadListProcessPopulation.forEach((head:TableUpperProps) => { + if(head.id === 'planSwageQntSum' || head.id === 'swtrmtPpltnCnt' || head.id === 'swtrmtZoneArea') { + head.title = '현재\n(' + rtnData.yr + '년)'; + } else if(head.id === 'planSwageQntSumDc1' || head.id === 'swtrmtPpltnCntDc1' || head.id === 'swtrmtZoneAreaDc1') { + head.title = '1단계\n(' + rtnData.yrDc1 + '년)'; + } else if(head.id === 'planSwageQntSumDc2' || head.id === 'swtrmtPpltnCntDc2' || head.id === 'swtrmtZoneAreaDc2') { + head.title = '2단계\n(' + rtnData.yrDc2 + '년)'; + } else if(head.id === 'planSwageQntSumDc3' || head.id === 'swtrmtPpltnCntDc3' || head.id === 'swtrmtZoneAreaDc3') { + head.title = '3단계\n(' + rtnData.yrDc3 + '년)'; + } else if(head.id === 'planSwageQntSumDc4' || head.id === 'swtrmtPpltnCntDc4' || head.id === 'swtrmtZoneAreaDc4') { + head.title = '4단계\n(' + rtnData.yrDc4 + '년)'; + } else if(head.id === 'planSwageQntSumDt4' || head.id === 'swtrmtPpltnCntDt4' || head.id === 'swtrmtZoneAreaDt4') { + head.title = '기승인\n4단계(' + rtnData.yrDt4 + '년)'; + } + }); + + // 시설증설 계획 데이터 생성 + const fcltyImproveInfo: FcltyImproveInfoProps = { + planSwageQntSumDt4: rtnData.planSwageQntSumDt4, + planSwageQntSum: rtnData.planSwageQntSum, + planSwageQntSumDc1: rtnData.planSwageQntSumDc1, + planSwageQntSumDc2: rtnData.planSwageQntSumDc2, + planSwageQntSumDc3: rtnData.planSwageQntSumDc3, + planSwageQntSumDc4: rtnData.planSwageQntSumDc4 + }; + + // 처리인구 계획 데이터 생성 + const processPopulation: ProcessPopulationProps = { + swtrmtPpltnCntDt4: rtnData.swtrmtPpltnCntDt4, + swtrmtPpltnCnt: rtnData.swtrmtPpltnCnt, + swtrmtPpltnCntDc1: rtnData.swtrmtPpltnCntDc1, + swtrmtPpltnCntDc2: rtnData.swtrmtPpltnCntDc2, + swtrmtPpltnCntDc3: rtnData.swtrmtPpltnCntDc3, + swtrmtPpltnCntDc4: rtnData.swtrmtPpltnCntDc4 + }; + + // 처리구역 계획 데이터 생성 + const processAreaPlan: ProcessAreaPlanProps = { + swtrmtZoneAreaDt4: rtnData.swtrmtZoneAreaDt4, + swtrmtZoneArea: rtnData.swtrmtZoneArea, + swtrmtZoneAreaDc1: rtnData.swtrmtZoneAreaDc1, + swtrmtZoneAreaDc2: rtnData.swtrmtZoneAreaDc2, + swtrmtZoneAreaDc3: rtnData.swtrmtZoneAreaDc3, + swtrmtZoneAreaDc4: rtnData.swtrmtZoneAreaDc4, + trtDstr: rtnData.trtDstr + }; + + return { + ...state, + ProcessAreaDetail: { + ...state.ProcessAreaDetail, + gridListFcltyImproveInfo: [fcltyImproveInfo], + gridListProcessPopulation: [processPopulation], + gridListProcessAreaPlan: [processAreaPlan] + } + }; + } + return state; + }); + } + } +} \ No newline at end of file diff --git a/apps/fo/app/store/processFacility/index.tsx b/apps/fo/app/store/processFacility/index.tsx new file mode 100644 index 000000000..a8c04930d --- /dev/null +++ b/apps/fo/app/store/processFacility/index.tsx @@ -0,0 +1,832 @@ +import { getFetch } from "@/app/components/api"; +import DataForm from "@/app/components/common/DataForm"; +import type { TableUpperProps } from "../dashboard"; +export type { TableUpperProps }; + +/**==================================== + * Store - 타입 + ====================================*/ + +/** + * (1) 메인페이지 + */ +// 처리시설 리스트 +export type facilityListProps = { + bscFctCd : string, + bscFctNm : string, + bscFctClsNm : string, + sido : string, + gugun : string, + daddr : string, + fctCpc : number | string | '', + plInflWqltBod : number | string | '', + dsInflWqltBod : number | string | '', + trtFrfNm : string, + rnDt : string, + cmplDt : string, + trstOprEtpNm : string, + rvbsn : string, + mdmbsn : string, + lgbsn : string, + rgnNm : string, + mnstr : string, + efflWtrrgn : string, + efflWqltStndrdNm : string +} + +export interface ExcelType { + 시설코드: string, + 권역:string|'', + 단위유역:string|'', + 중권역:string|'', + 환경청:string|'', + 시도: string, + 시군구: string, + 시설이름: string, + 주소: string, + 시설용량: number | '', + 계획유입수질: number | '', + 설계유입수질: number | '', + 공법: string, + 가동개시일: string, + 준공일: string, + 관리대행업자: string, +} + +// (2) 처리시설 팝업 +// 시설 기본정보 +export type BasicInfoProps = { + bscFctNm : string, + fctCpc : number | '', + sido : string, + gugun : string, + startDay : string, + addr : string, + cmplDt : string, + trtFrfNm : string, + trstOprEtpNm : string, +} + +// 시설 계획정보 +export type PlanInfoProps = { + plInflWqltBod? : string, + plInflWqltToc? : string, + plInflWqltCod? : string, + plInflWqltSs? : string, + plInflWqltTn? : string, + plInflWqltTp? : string, + dsInflWqltBod? : string, + dsInflWqltToc? : string, + dsInflWqltCod? : string, + dsInflWqltSs? : string, + dsInflWqltTn? : string, + dsInflWqltTp? : string, + sitArea? : string | number, + trtDstrArea? : string | number, + swrTrtPopl? : string | number +} + +// 시설 계획정보 +export type LegalWaterQltyProps = { + totClfgrpcnt : number, + bod : number, + cod : number, + ss : number, + tn : number, + tp : number +} + +// 시설 수역 및 구역정보 +export type fcltyAreaInfoProps = { + rvbsn : string, + efflWtrrgn : string, + lgbsn : string, + efflWqltStndrdNm : string, + rgnNm : string, +} + +// 시설 이력관리 +export type fcltyHistInfoProps = { + extnsSeqno : string, + extnsNm : string, + bzStrtYrMnth : string, + bzEdYrMnth : string, + eqpPrmsDt : string, + cmplDt : string, + trlrnCmplDt : string, + rnDt : string, + sitArea : number, + trtDstrArea : number, + swrTrtPopl : number +} + +// 시설 수질정보 +export type fcltyWaterQltyInfoProps = { + extnsSeqno : string, + plInflWqltBod : string, + plInflWqltToc : string, + plInflWqltCod : string, + plInflWqltSs : string, + plInflWqltTn : string, + plInflWqltTp : string, + dsInflWqltBod : string, + dsInflWqltToc : string, + dsInflWqltCod : string, + dsInflWqltSs : string, + dsInflWqltTn : string, + dsInflWqltTp : string, + plEfflWqltBod : string, + plEfflWqltToc : string, + plEfflWqltCod : string, + plEfflWqltSs : string, + plEfflWqltTn : string, + plEfflWqltTp : string, + bod : number, + cod : number, + ss : number, + tn : number, + tp : number, + totClfgrpcnt : number, +} + +/**==================================== + * Store - data + ====================================*/ + +/** + * (1) 메인페이지 + */ +// 검색단 +const ProcessFacilityLabelArray:string[] = ['구분', '시도', '시군구','용량별', '시설명', '시설상태', '시설구분', '권역', '단위유역', '중권역', '환경청별']; + +// 리스트 헤더 +const upHeadList1:TableUpperProps[] = [ + { id: "bscFctCd", title: "시설코드"}, + { id: "sido", title: "시도", upName:"upChangeRe", upSequnce:1 }, + { id: "gugun", title: "시군구", upName:"upChangeRe",upSequnce:2 }, + { id: "bscFctNm", title: "시설명"}, + { id: "daddr", title: "주소"}, + { id: "fctCpc", title: "시설용량\n(m³/일)", align: 'right'}, + { id: "plInflWqltBod", title: "계획유입수질\n(mg/L)", align:"right"}, + { id: "dsInflWqltBod", title: "설계유입수질\n(mg/L)", align:"right"}, + { id: "trtFrfNm", title: '공법'}, + { id: "rnDt", title: '가동개시일'}, + { id: "cmplDt", title: '준공일' }, + { id: "trstOprEtpNm", title: '관리대행업자'} +]; + +// 처리시설 리스트 (유역) +const upHeadList2:TableUpperProps[] = [ + { id: "bscFctCd", title: "시설코드"}, + { id: "rvbsn", title: "권역", upName:"upChangeRe", upSequnce:1}, + { id: "lgbsn", title: "단위유역", upName:"upChangeRe", upSequnce:2}, + { id: "mdmbsn", title: "중권역", upName:"upChangeRe", upSequnce:3}, + { id: "bscFctNm", title: "시설명"}, + { id: "daddr", title: "주소"}, + { id: "fctCpc", title: "시설용량\n(m³/일)", align: 'right'}, + { id: "plInflWqltBod", title: "계획유입수질\n(mg/L)", align:'right'}, + { id: "dsInflWqltBod", title: "설계유입수질\n(mg/L)", align:'right'}, + { id: "trtFrfNm", title: '공법'}, + { id: "rnDt", title: '가동개시일'}, + { id: "cmplDt", title: '준공일' }, + { id: "trstOprEtpNm", title: '관리대행업자'} +]; + +// 처리시설 리스트 (환경청청) +const upHeadList3:TableUpperProps[] = [ + { id: "bscFctCd", title: "시설코드"}, + { id: "rvbsn", title: "권역", upName:"upChangeRe1", upSequnce:1}, + { id: "lgbsn", title: "단위유역", upName:"upChangeRe1", upSequnce:2}, + { id: "mdmbsn", title: "중권역", upName:"upChangeRe1", upSequnce:3}, + { id: "rgnNm", title: "환경청"}, + { id: "sido", title: "시도", upName:"upChangeRe2", upSequnce:1 }, + { id: "gugun", title: "시군구", upName:"upChangeRe2",upSequnce:2 }, + { id: "bscFctNm", title: "시설명"}, + { id: "daddr", title: "주소"}, + { id: "fctCpc", title: "시설용량\n(m³/일)", align: 'right'}, + { id: "plInflWqltBod", title: "계획유입수질\n(mg/L)", align:'right'}, + { id: "dsInflWqltBod", title: "설계유입수질\n(mg/L)", align:'right'}, + { id: "trtFrfNm", title: '공법'}, + { id: "rnDt", title: '가동개시일'}, + { id: "cmplDt", title: '준공일' }, + { id: "trstOprEtpNm", title: '관리대행업자'} +]; + +const processFacilityList:facilityListProps[] = [ +] + +/** + * 처리시설 상세팝업 + */ +// 시설 기본정보 (헤더) +const upHeadListBasicInfo:TableUpperProps[] = [ + { id: "bscFctNm", title: "시설명" }, + { id: "fctCpc", title: "시설용량n(m³/일)", align: 'right'}, + { id: "sido", title: "시도"}, + { id: "gugun", title: "시군구"}, + { id: "rnDt", title: "가동개시일"}, + { id: "daddr", title: "주소"}, + { id: "cmplDt", title: "준공일"}, + { id: "trtFrfNm", title: "공법"}, + { id: "trstOprEtpNm", title: "관리대행업자"}, +]; + +// 시설 기본정보 (리스트) +const gridListBasicInfo:BasicInfoProps[] = [ + { + bscFctNm : "난지", + fctCpc : 860000, + sido : "서울특별시", + gugun : "", + startDay : "1987-06-30", + addr : "경기도 고양시 덕양구 대덕로 426 (현천동)", + cmplDt : "2013-04-30", + trtFrfNm : "MLE, A2O", + trstOprEtpNm : "자체", + } +]; + +// 시설 법적방류수질 (헤더) +const upHeadListLegalWaterQlty:TableUpperProps[] = [ + { id: "bod", title: "BOD(mg/L)", align: 'right'}, + { id: "cod", title: "TOC(COD)(mg/L)", align: 'right'}, + { id: "ss", title: "SS(mg/L)", align: 'right'}, + { id: "tn", title: "T-N(mg/L)", align: 'right'}, + { id: "tp", title: "T-P(mg/L) ", align: 'right'}, + { id: "totClfgrpcnt", title: "대장균군(개/100ml)"}, +] + +// 시설 법적방류수질 (리스트) +let gridListLegalWaterQlty:LegalWaterQltyProps[] = [ +] + +// 시설 계획정보 (헤더) +const upHeadListPlanInfo:TableUpperProps[] = [ + { id: "plInflWqltBod", title: "BOD", upName:"upChangeRe1", upSequnce:1, align: 'right' }, + { id: "plInflWqltToc", title: "TOC", upName:"upChangeRe1", upSequnce:2, align: 'right' }, + { id: "plInflWqltCod", title: "COD", upName:"upChangeRe1", upSequnce:3, align: 'right' }, + { id: "plInflWqltSs", title: "SS", upName:"upChangeRe1", upSequnce:4, align: 'right' }, + { id: "plInflWqltTn", title: "T-N", upName:"upChangeRe1", upSequnce:5, align: 'right' }, + { id: "plInflWqltTp", title: "T-P", upName:"upChangeRe1", upSequnce:6, align: 'right' }, + { id: "dsInflWqltBod", title: "BOD", upName:"upChangeRe2", upSequnce:1, align: 'right' }, + { id: "dsInflWqltToc", title: "TOC", upName:"upChangeRe2", upSequnce:2, align: 'right' }, + { id: "dsInflWqltCod", title: "COD", upName:"upChangeRe2", upSequnce:3, align: 'right' }, + { id: "dsInflWqltSs", title: "SS", upName:"upChangeRe2", upSequnce:4, align: 'right' }, + { id: "dsInflWqltTn", title: "T-N", upName:"upChangeRe2", upSequnce:5, align: 'right' }, + { id: "dsInflWqltTp", title: "T-P", upName:"upChangeRe2", upSequnce:6, align: 'right' }, + { id: "sitArea", title: "부지면적\n(m²)", align: 'right'}, + { id: "trtDstrArea", title: "처리구역면적\n(ha)", align: 'right'}, + { id: "swrTrtPopl", title: "하수처리인구\n(인)", align: 'right'}, +]; + +// 시설 계획정보 (리스트) +const gridListPlanInfo:PlanInfoProps[] = [ + { + plInflWqltBod : "179.0", + plInflWqltToc : "90.0", + plInflWqltCod : "0.0", + plInflWqltSs : "132.0", + plInflWqltTn : "38.000", + plInflWqltTp : "4.300", + dsInflWqltBod : "178.0", + dsInflWqltToc : "91.0", + dsInflWqltCod : "0.0", + dsInflWqltSs : "131.0", + dsInflWqltTn : "40.200", + dsInflWqltTp : "4.500", + sitArea : "928,574", + trtDstrArea : "7,994.0", + swrTrtPopl : "1,573,277", + } +]; + +// 시설 수역 및 구역정보 (헤더) +const upHeadListFcltyAreaInfo:TableUpperProps[] = [ + { id: "rvbsn", title: "하천명" }, + { id: "efflWtrrgn", title: "방류수역"}, + { id: "lgbsn", title: "단위유역"}, + { id: "efflWqltStndrdNm", title: "지역구분"}, + { id: "rgnNm", title: "관할 유역(지방) 환경청"} +]; + +// 시설 수역 및 구역정보 (리스트) +const gridListFcltyAreaInfo:fcltyAreaInfoProps[] = [ + { + rvbsn : "한강", + efflWtrrgn : "한강수계", + lgbsn : "한강본류", + efflWqltStndrdNm : "500톤 이상\n(Ⅲ지역)", + rgnNm : "한강유역환경청" + } +]; + +// 시설 이력관리 (헤더) +const upHeadListFcltyHistInfo:TableUpperProps[] = [ + { id: "extnsSeqno", title: "차수", align: 'center' }, + { id: "extnsNm", title: "구분"}, + { id: "bzStrtYrMnth", title: "사업기간", upName:"upChangeRe1", upSequnce:1}, + { id: "bzEdYrMnth", title: "사업기간", upName:"upChangeRe1", upSequnce:2}, + { id: "eqpPrmsDt", title: "설치인가일"}, + { id: "cmplDt", title: "준공일"}, + { id: "trlrnCmplDt", title: "시운전완료일"}, + { id: "rnDt", title: "가동개시일"}, +]; + +// 시설 이력관리 (리스트) +const gridListFcltyHistInfo:fcltyHistInfoProps[] = [ + { + extnsSeqno : "최초", + extnsNm : "최초", + bzStrtYrMnth : "1984.12.01", + bzEdYrMnth : "1987.06.01", + eqpPrmsDt : "1985.09.13", + cmplDt : "1987.06.30", + trlrnCmplDt : "1987.06.30", + rnDt : "1987.06.30", + sitArea : 0, + trtDstrArea : 0, + swrTrtPopl : 0, + } +]; + +// 시설 수질정보 (헤더) +const upHeadListFcltyWaterQltyInfo:TableUpperProps[] = [ + { id: "extnsSeqno", title: "차수" }, + { id: "plInflWqltBod", title: "BOD", upName:"upChangeRe1", upSequnce:1, align: 'right' }, + { id: "plInflWqltToc", title: "TOC", upName:"upChangeRe1", upSequnce:2, align: 'right' }, + { id: "plInflWqltCod", title: "COD", upName:"upChangeRe1", upSequnce:3, align: 'right' }, + { id: "plInflWqltSs", title: "SS", upName:"upChangeRe1", upSequnce:4, align: 'right' }, + { id: "plInflWqltTn", title: "T-N", upName:"upChangeRe1", upSequnce:5, align: 'right' }, + { id: "plInflWqltTp", title: "T-P", upName:"upChangeRe1", upSequnce:6, align: 'right' }, + { id: "dsInflWqltBod", title: "BOD", upName:"upChangeRe2", upSequnce:1, align: 'right' }, + { id: "dsInflWqltToc", title: "TOC", upName:"upChangeRe2", upSequnce:2, align: 'right' }, + { id: "dsInflWqltCod", title: "COD", upName:"upChangeRe2", upSequnce:3, align: 'right' }, + { id: "dsInflWqltSs", title: "SS", upName:"upChangeRe2", upSequnce:4, align: 'right' }, + { id: "dsInflWqltTn", title: "T-N", upName:"upChangeRe2", upSequnce:5, align: 'right' }, + { id: "dsInflWqltTp", title: "T-P", upName:"upChangeRe2", upSequnce:6, align: 'right' }, + { id: "plEfflWqltBod", title: "BOD", upName:"upChangeRe3", upSequnce:1, align: 'right' }, + { id: "plEfflWqltToc", title: "TOC", upName:"upChangeRe3", upSequnce:2, align: 'right' }, + { id: "plEfflWqltCod", title: "COD", upName:"upChangeRe3", upSequnce:3, align: 'right' }, + { id: "plEfflWqltSs", title: "SS", upName:"upChangeRe3", upSequnce:4, align: 'right' }, + { id: "plEfflWqltTn", title: "T-N", upName:"upChangeRe3", upSequnce:5, align: 'right' }, + { id: "plEfflWqltTp", title: "T-P", upName:"upChangeRe3", upSequnce:6, align: 'right' }, +]; + +// 시설 수질정보 (리스트) +const gridListFcltyWaterQltyInfo:fcltyWaterQltyInfoProps[] = [ + { + extnsSeqno: "최초", + plInflWqltBod: "111.0", + plInflWqltToc: "(0.0)", + plInflWqltCod: "(0.0)", + plInflWqltSs: "122.0", + plInflWqltTn: "0.000", + plInflWqltTp: "0.000", + dsInflWqltBod: "111.0", + dsInflWqltToc: "(0.0)", + dsInflWqltCod: "(0.0)", + dsInflWqltSs: "122.0", + dsInflWqltTn: "0.000", + dsInflWqltTp: "0.000", + plEfflWqltBod: "20.0", + plEfflWqltCod: "(0.0)", + plEfflWqltToc: "(0.0)", + plEfflWqltSs: "12.0", + plEfflWqltTn: "0.000", + plEfflWqltTp: "0.000", + bod: 0, + cod: 0, + ss: 0, + tn: 0, + tp: 0, + totClfgrpcnt: 0 + } +]; + +/**==================================== + * Store - api (데이터 호출) + ====================================*/ + +// 시설정보 리스트 데이터 호출(구분 선택 후 시설명 있을 경우) +const fetchFacilityList = async (pageNo:number, pageSize:number, partValue?:string, nameCd?:string, fcltCpct?:string, oprtngSttsCd?:string, facilityPartValue?:string, facilityName?:string ): Promise<{data:facilityListProps[], count:number}> => { + try { + const tailStr = partValue ? '/'+ (partValue==='G' ? 'A' : partValue) + '/' + nameCd + '/' + fcltCpct + '/' + oprtngSttsCd + '/' + facilityPartValue + (facilityName && facilityName !== '' ? '/' + facilityName : '') : ''; + const url = '/prcsFclt/info/history/'+pageNo+'/'+pageSize+tailStr; + //const res = await getFetch('fo', '/prcsFclt/info/'+pageNo+'/'+pageSize+tailUrl); + const res = await getFetch('fo', url); + return res; + } catch (error) { + console.error('시설정보 데이터를 가져오는데 실패했습니다:', error); + return {data: [], count: 0}; + } +}; + +// 시설정보 리스트 데이터 호출 (대시보드용) +const fetchFacilityListSearching = async (pageNo:number, pageSize:number, partValue?:string, nameCd?:string, fcltCpct?:string, oprtngSttsCd?:string, facilityPartValue?:string, facilityName?:string ): Promise<{data:facilityListProps[], count:number}> => { + try { + const tailStr = partValue ? '/'+ (partValue==='G' ? 'A' : partValue) + '/' + nameCd + '/' + fcltCpct + '/' + oprtngSttsCd + '/' + facilityPartValue + (facilityName && facilityName !== '' ? '/' + facilityName : '') : ''; + const url = '/prcsFclt/searching/'+pageNo+'/'+pageSize+tailStr; + //const res = await getFetch('fo', '/prcsFclt/info/'+pageNo+'/'+pageSize+tailUrl); + const res = await getFetch('fo', url); + return res; + } catch (error) { + console.error('시설정보 데이터를 가져오는데 실패했습니다:', error); + return {data: [], count: 0}; + } +}; + +// 모달 +// 시설정보 이력정보 데이터 호출 +const fetchFacilityHistInfo = async (facilityCd:string ): Promise<fcltyHistInfoProps[]> => { + try { + const res = await getFetch('fo', '/prcsFclt/hist/'+facilityCd); + const data = res.data; + DataForm({ type: 'date', column: 'trlrnCmplDt', item: data as unknown as Record<string, unknown> }); + DataForm({ type: 'date', column: 'eqpPrmsDt', item: data as unknown as Record<string, unknown> }); + DataForm({ type: 'date', column: 'cmplDt', item: data as unknown as Record<string, unknown> }); + DataForm({ type: 'date', column: 'rnDt', item: data as unknown as Record<string, unknown> }); + DataForm({ type: 'number', column: 'sitArea', item: data as unknown as Record<string, unknown> }); + DataForm({ type: 'number', column: 'trtDstrArea', item: data as unknown as Record<string, unknown> }); + DataForm({ type: 'number', column: 'swrTrtPopl', item: data as unknown as Record<string, unknown> }); + return data; + } catch (error) { + console.error('시설정보 이력정보 데이터를 가져오는데 실패했습니다:', error); + return []; + } +}; + +// 시설정보 수질정보 데이터 호출 +const fetchFacilityWaterQltyInfo = async (facilityCd:string ): Promise<fcltyWaterQltyInfoProps[]> => { + try { + const res = await getFetch('fo', '/prcsFclt/info/history/'+facilityCd); + const data = res.data; + return data; + } catch (error) { + console.error('시설정보 수질정보 데이터를 가져오는데 실패했습니다:', error); + return []; + } +}; + +/**==================================== + * Store - expaort + ====================================*/ +export type ProcessFacilityActions = { + /** + * 시설정보 리스트 데이터 호출(구분 선택 후 시설명 있을 경우) + * @param pageNo : 페이지 번호 + * @param pageSize : 페이지 크기 + * @param partValue : 구분값(행정명, 대유역, 환경청) + * @param nameCd : 행정코드(시도명, 시군구명, 단위유역) + * @param fcltCpct : 시설용량 + * @param oprtngSttsCd : 운영 상태 코드 + * @param facilityName : 시설명 + */ + searchProcessFacility : (pageNo:number, pageSize:number, partValue?:string, nameCd?:string, fcltCpct?:string, oprtngSttsCd?:string, facilityPartValue?:string, facilityName?:string) => void; + + /** + * 시설정보 이력정보 데이터 호출 + * @param facilityCd : 시설코드 + */ + searchProcessFacilityHist : (facilityCd:string) => void; + + /** + * 시설정보 수질정보 데이터 호출 + * @param facilityCd : 시설코드 + */ + searchProcessFacilityWaterQlty : (facilityCd:string) => void; + + /** + * 시설정보 리스트 데이터 호출 (대시보드용) + * @param pageNo : 페이지 번호 + * @param pageSize : 페이지 크기 + */ + searchProcessFacilitySearching : (pageNo:number, pageSize:number, partValue?:string, nameCd?:string, fcltCpct?:string, oprtngSttsCd?:string, facilityPartValue?:string, facilityName?:string) => void; +} + +export type ProcessFacilityType = { + ProcessFacility: { + isInit: boolean; + processFacilityLabelArray:string[]; + }, + + ProcessFacilityList : { + isInit: boolean; + upHeadList1:TableUpperProps[]; + upHeadList2:TableUpperProps[]; + upHeadList3:TableUpperProps[]; + processFacilityList:facilityListProps[]; + processFacilityCount:number; + excelList : ExcelType[]; + }, + + ProcessFacilityDetail : { + isInit : boolean; + upHeadListBasicInfo : TableUpperProps[]; + gridListBasicInfo : BasicInfoProps[]; + upHeadListPlanInfo : TableUpperProps[]; + gridListPlanInfo : PlanInfoProps[]; + upHeadListFcltyAreaInfo : TableUpperProps[]; + gridListFcltyAreaInfo : fcltyAreaInfoProps[]; + + upHeadListFcltyHistInfo : TableUpperProps[]; + gridListFcltyHistInfo : fcltyHistInfoProps[]; + upHeadListFcltyWaterQltyInfo : TableUpperProps[]; + gridListFcltyWaterQltyInfo : fcltyWaterQltyInfoProps[]; + + upHeadListLegalWaterQlty : TableUpperProps[]; + gridListLegalWaterQlty : LegalWaterQltyProps[]; + }, +} + +export const processFacilityInitState:ProcessFacilityType = { + ProcessFacility: { + isInit: false, + processFacilityLabelArray: ProcessFacilityLabelArray, + }, + + ProcessFacilityList : { + isInit : false, + upHeadList1 : upHeadList1, + upHeadList2 : upHeadList2, + upHeadList3 : upHeadList3, + processFacilityList : processFacilityList, + processFacilityCount:1, + excelList : [] + }, + + ProcessFacilityDetail : { + isInit : false, + upHeadListBasicInfo : upHeadListBasicInfo, + gridListBasicInfo : gridListBasicInfo, + upHeadListPlanInfo : upHeadListPlanInfo, + gridListPlanInfo : gridListPlanInfo, + upHeadListFcltyAreaInfo : upHeadListFcltyAreaInfo, + gridListFcltyAreaInfo : gridListFcltyAreaInfo, + + upHeadListFcltyHistInfo : upHeadListFcltyHistInfo, + gridListFcltyHistInfo : gridListFcltyHistInfo, + upHeadListFcltyWaterQltyInfo : upHeadListFcltyWaterQltyInfo, + gridListFcltyWaterQltyInfo : gridListFcltyWaterQltyInfo, + + upHeadListLegalWaterQlty : upHeadListLegalWaterQlty, + gridListLegalWaterQlty : gridListLegalWaterQlty, + } +} + +// 상태 설정 함수 타입 정의 +type SetStateFunction = (state: ProcessFacilityType | ((state: ProcessFacilityType) => ProcessFacilityType)) => void; + +export type ProcessFacilityStore = ProcessFacilityType & ProcessFacilityActions; + +export const processFacilityReducer:(set:SetStateFunction)=>ProcessFacilityActions=(set: SetStateFunction) => { + return{ + // 메인페이지 + + /** + * 시설정보 리스트 데이터 호출 + * @param pageNo : 페이지 번호 + * @param pageSize : 페이지 크기 + */ + searchProcessFacility: async (pageNo:number, pageSize:number, partValue?:string, nameCd?:string, fcltCpct?:string, oprtngSttsCd?:string, facilityPartValue?:string, facilityName?:string) => { + const res = await fetchFacilityList(pageNo, pageSize, partValue, nameCd, fcltCpct, oprtngSttsCd, facilityPartValue, facilityName); + res.data.forEach((item:facilityListProps) => { + DataForm({ type: 'date', column: 'cmplDt', item:item as unknown as Record<string, unknown>}); + DataForm({ type: 'date', column: 'rnDt', item:item as unknown as Record<string, unknown>}); + DataForm({ type: 'float1', column: 'fctCpc', item:item as unknown as Record<string, unknown>}); + }) + + res.data.forEach(item => { + item.plInflWqltBod = item.plInflWqltBod ? Number(item.plInflWqltBod).toFixed(1) : ''; + item.dsInflWqltBod = item.dsInflWqltBod ? Number(item.dsInflWqltBod).toFixed(1) : ''; + }) + + const excelDownList: ExcelType[] = []; + + if(pageSize>5000){ + res.data.forEach(item => { + item.fctCpc = item.fctCpc ? parseFloat(item.fctCpc.toString().replace(/,/g, '')) : ''; + excelDownList.push({ + 시설코드: item.bscFctCd, + 권역: item.rvbsn, + 단위유역: item.lgbsn, + 중권역: item.mdmbsn, + 환경청: item.rgnNm, + 시도: item.sido, + 시군구: item.gugun, + 시설이름: item.bscFctNm, + 주소: item.daddr, + 시설용량: item.fctCpc ? Number(item.fctCpc) : '', + 계획유입수질: item.plInflWqltBod ? Number(item.plInflWqltBod) : '', + 설계유입수질: item.dsInflWqltBod ? Number(item.dsInflWqltBod) : '', + 공법: item.trtFrfNm, + 가동개시일: item.rnDt, + 준공일: item.cmplDt, + 관리대행업자: item.mnstr, + }) + }) + }else{ + res.data.forEach(item => { + item.fctCpc = item.fctCpc ? parseFloat(item.fctCpc.toString().replace(/,/g, '')) : ''; + }) + } + set((state:ProcessFacilityType) => ({ + ...state, + ProcessFacilityList: pageSize<5000?{ + ...state.ProcessFacilityList, + processFacilityList: res.data, + processFacilityCount: res.count, + }:{ + ...state.ProcessFacilityList, + excelList: excelDownList, + }, + })) + }, + + /** + * 시설정보 리스트 데이터 호출 + * @param pageNo : 페이지 번호 + * @param pageSize : 페이지 크기 + */ + searchProcessFacilitySearching: async (pageNo:number, pageSize:number, partValue?:string, nameCd?:string, fcltCpct?:string, oprtngSttsCd?:string, facilityPartValue?:string, facilityName?:string) => { + const res = await fetchFacilityListSearching(pageNo, pageSize, partValue, nameCd, fcltCpct, oprtngSttsCd, facilityPartValue, facilityName); + /* res.data.forEach((item:facilityListProps) => { + DataForm({ type: 'date', column: 'cmplDt', item:item as unknown as Record<string, unknown>}); + DataForm({ type: 'date', column: 'rnDt', item:item as unknown as Record<string, unknown>}); + }) + + res.data.forEach(item => { + item.plInflWqltBod = item.plInflWqltBod ? Number(item.plInflWqltBod).toFixed(1) : ''; + item.dsInflWqltBod = item.dsInflWqltBod ? Number(item.dsInflWqltBod).toFixed(1) : ''; + }) */ + console.log(res.data); + set((state:ProcessFacilityType) => ({ + ...state, + ProcessFacilityList: { + ...state.ProcessFacilityList, + processFacilityList: res.data, + processFacilityCount: res.count, + }, + })) + }, + + + // 모달 이력정보 + /** + * 시설정보 이력정보 데이터 호출 + * @param facilityCd : 시설코드 + */ + searchProcessFacilityHist: async (facilityCd:string) => { + const promiseSet = await Promise.all([fetchFacilityHistInfo(facilityCd)]); + const [facilityHistInfo] = promiseSet; + // extnsSeqno를 기준으로 오름차순 정렬 + const sortedHistInfo = [...facilityHistInfo].sort((a, b) => { + // 숫자로 변환하여 비교 + const numA = Number(a.extnsSeqno); + const numB = Number(b.extnsSeqno); + if (!isNaN(numA) && !isNaN(numB)) { + return numA - numB; + } + // 숫자 변환이 실패하면 문자열로 비교 + return String(a.extnsSeqno).localeCompare(String(b.extnsSeqno)); + }); + + sortedHistInfo.forEach(item => { + if(item.extnsSeqno == '0'){ + item.extnsSeqno = '최초'; + item.extnsNm = '신설'; + } + }); + + // yyyy-MM-dd로 변환 + sortedHistInfo.forEach(item => { + item.eqpPrmsDt = item.eqpPrmsDt ? item.eqpPrmsDt.substring(0,4) + '-' + item.eqpPrmsDt.substring(4,6) + '-' + item.eqpPrmsDt.substring(6,8) : ''; + item.trlrnCmplDt = item.trlrnCmplDt ? item.trlrnCmplDt.substring(0,4) + '-' + item.trlrnCmplDt.substring(4,6) + '-' + item.trlrnCmplDt.substring(6,8) : ''; + item.cmplDt = item.cmplDt ? item.cmplDt.substring(0,4) + '-' + item.cmplDt.substring(4,6) + '-' + item.cmplDt.substring(6,8) : ''; + item.rnDt = item.rnDt ? item.rnDt.substring(0,4) + '-' + item.rnDt.substring(4,6) + '-' + item.rnDt.substring(6,8) : ''; + }); + + // 마지막 이력 데이터의 값을 gridListPlanInfo에 업데이트 + if (sortedHistInfo.length > 0) { + const lastHistInfo = sortedHistInfo[sortedHistInfo.length - 1]; + set((state: ProcessFacilityType) => ({ + ...state, + ProcessFacilityDetail: { + ...state.ProcessFacilityDetail, + gridListFcltyHistInfo: sortedHistInfo, + gridListPlanInfo: [{ + ...state.ProcessFacilityDetail.gridListPlanInfo[0], + swrTrtPopl: lastHistInfo?.swrTrtPopl ?? state.ProcessFacilityDetail.gridListPlanInfo[0]?.swrTrtPopl, + trtDstrArea: lastHistInfo?.trtDstrArea ?? state.ProcessFacilityDetail.gridListPlanInfo[0]?.trtDstrArea, + sitArea: lastHistInfo?.sitArea ?? state.ProcessFacilityDetail.gridListPlanInfo[0]?.sitArea + }] + } + })); + } else { + set((state: ProcessFacilityType) => ({ + ...state, + ProcessFacilityDetail: { + ...state.ProcessFacilityDetail, + gridListFcltyHistInfo: sortedHistInfo, + gridListPlanInfo: [{ + ...state.ProcessFacilityDetail.gridListPlanInfo[0], + swrTrtPopl: '', + trtDstrArea: '', + sitArea: '' + }] + } + })); + } + }, + + // 모달 수질정보 + /** + * 시설정보 수질정보 데이터 호출 + * @param facilityCd : 시설코드 + */ + searchProcessFacilityWaterQlty: async (facilityCd:string) => { + const promiseSet = await Promise.all([fetchFacilityWaterQltyInfo(facilityCd)]); + const [facilityWaterQltyInfo] = promiseSet; + // extnsSeqno를 기준으로 오름차순 정렬 + const sortedWaterQltyInfo = [...facilityWaterQltyInfo].sort((a, b) => { + // 숫자로 변환하여 비교 + const numA = Number(a.extnsSeqno); + const numB = Number(b.extnsSeqno); + if (!isNaN(numA) && !isNaN(numB)) { + return numA - numB; + } + // 숫자 변환이 실패하면 문자열로 비교 + return String(a.extnsSeqno).localeCompare(String(b.extnsSeqno)); + }); + + sortedWaterQltyInfo.forEach(item => { + if(item.extnsSeqno == '0'){ + item.extnsSeqno = '최초'; + } + }); + + // 법적수질정보 초기화 및 push + gridListLegalWaterQlty = []; + + gridListLegalWaterQlty.push({ + bod: facilityWaterQltyInfo[0]?.bod ?? 0, + cod: Number(facilityWaterQltyInfo[0]?.cod ?? 0), + ss: Number(facilityWaterQltyInfo[0]?.ss ?? 0), + tn: Number(facilityWaterQltyInfo[0]?.tn ?? 0), + tp: Number(facilityWaterQltyInfo[0]?.tp ?? 0), + totClfgrpcnt : Number(facilityWaterQltyInfo[0]?.totClfgrpcnt ?? 0), + }); + + // 마지막 수질 데이터의 값을 gridListPlanInfo에 업데이트 + if (sortedWaterQltyInfo.length > 0) { + const lastWaterQltyInfo = sortedWaterQltyInfo[sortedWaterQltyInfo.length - 1]; + set((state: ProcessFacilityType) => ({ + ...state, + ProcessFacilityDetail: { + ...state.ProcessFacilityDetail, + gridListFcltyWaterQltyInfo: sortedWaterQltyInfo, + gridListLegalWaterQlty: gridListLegalWaterQlty, + gridListPlanInfo: [{ + ...state.ProcessFacilityDetail.gridListPlanInfo[0], + dsInflWqltBod: lastWaterQltyInfo?.dsInflWqltBod === 'NULL' || lastWaterQltyInfo?.dsInflWqltBod === undefined ? '' : lastWaterQltyInfo.dsInflWqltBod, + dsInflWqltToc: lastWaterQltyInfo?.dsInflWqltToc === 'NULL' || lastWaterQltyInfo?.dsInflWqltToc === undefined ? '' : lastWaterQltyInfo.dsInflWqltToc, + dsInflWqltCod: lastWaterQltyInfo?.dsInflWqltCod === 'NULL' || lastWaterQltyInfo?.dsInflWqltCod === undefined ? '' : lastWaterQltyInfo.dsInflWqltCod, + dsInflWqltSs: lastWaterQltyInfo?.dsInflWqltSs === 'NULL' || lastWaterQltyInfo?.dsInflWqltSs === undefined ? '' : lastWaterQltyInfo.dsInflWqltSs, + dsInflWqltTn: lastWaterQltyInfo?.dsInflWqltTn === 'NULL' || lastWaterQltyInfo?.dsInflWqltTn === undefined ? '' : lastWaterQltyInfo.dsInflWqltTn, + dsInflWqltTp: lastWaterQltyInfo?.dsInflWqltTp === 'NULL' || lastWaterQltyInfo?.dsInflWqltTp === undefined ? '' : lastWaterQltyInfo.dsInflWqltTp, + plInflWqltBod: lastWaterQltyInfo?.plInflWqltBod === 'NULL' || lastWaterQltyInfo?.plInflWqltBod === undefined ? '' : lastWaterQltyInfo.plInflWqltBod, + plInflWqltToc: lastWaterQltyInfo?.plInflWqltToc === 'NULL' || lastWaterQltyInfo?.plInflWqltToc === undefined ? '' : lastWaterQltyInfo.plInflWqltToc, + plInflWqltCod: lastWaterQltyInfo?.plInflWqltCod === 'NULL' || lastWaterQltyInfo?.plInflWqltCod === undefined ? '' : lastWaterQltyInfo.plInflWqltCod, + plInflWqltSs: lastWaterQltyInfo?.plInflWqltSs === 'NULL' || lastWaterQltyInfo?.plInflWqltSs === undefined ? '' : lastWaterQltyInfo.plInflWqltSs, + plInflWqltTn: lastWaterQltyInfo?.plInflWqltTn === 'NULL' || lastWaterQltyInfo?.plInflWqltTn === undefined ? '' : lastWaterQltyInfo.plInflWqltTn, + plInflWqltTp: lastWaterQltyInfo?.plInflWqltTp === 'NULL' || lastWaterQltyInfo?.plInflWqltTp === undefined ? '' : lastWaterQltyInfo.plInflWqltTp + }] + } + })); + } else { + set((state: ProcessFacilityType) => ({ + ...state, + ProcessFacilityDetail: { + ...state.ProcessFacilityDetail, + gridListFcltyWaterQltyInfo: sortedWaterQltyInfo, + gridListLegalWaterQlty: gridListLegalWaterQlty, + gridListPlanInfo: [{ + ...state.ProcessFacilityDetail.gridListPlanInfo[0], + dsInflWqltBod: '', + dsInflWqltToc: '', + dsInflWqltCod: '', + dsInflWqltSs: '', + dsInflWqltTn: '', + dsInflWqltTp: '', + plEfflWqltBod: '', + plEfflWqltToc: '', + plEfflWqltCod: '', + plEfflWqltSs: '', + plEfflWqltTn: '', + plEfflWqltTp: '' + }] + } + })); + } + } + } +} \ No newline at end of file diff --git a/apps/fo/app/store/publicReuseFaciltyInsert/index.tsx b/apps/fo/app/store/publicReuseFaciltyInsert/index.tsx new file mode 100644 index 000000000..645cae5dc --- /dev/null +++ b/apps/fo/app/store/publicReuseFaciltyInsert/index.tsx @@ -0,0 +1,97 @@ +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, + register?: string +} +const upHeadList:TableUpperProps[] = [ + { id: "sido", title: "시도"}, + { id: "sigungo", title: "시군구"}, + { id: "facilityName", title: "시설명"}, + { id: "location", title: "위치"}, + { id: "locationgubun", title: "지역구분"}, + { id: "facilityCapacity", title: "시설용량"}, + { id: "yn", title: '여부', upName:'upChangeRe' , upSequnce:1 }, + { id: "authoDay", title: '허가일',upName:'upChangeRe', upSequnce:2 }, + { id: "area", title: '면적' ,upName:'upChangeRe',upSequnce:3 }]; + +const publicReuseFacilityUper:PublicReuseFacility[] = [ + { + sido: "서울", + sigungo: "강남구", + facilityName: "난지", + location: "경기도 고양시 덕양구 대차로 4가", + locationgubun: "500", + facilityCapacity: "800,000", + reuse: "Y", + register: "Y" + }, + { + sido: "서울", + sigungo: "강남구", + facilityName: "난지", + location: "경기도 고양시 덕양구 대차로 4가", + locationgubun: "500", + facilityCapacity: "800,000", + reuse: "Y", + register: "Y" + }, + { + sido: "서울", + sigungo: "강남구", + facilityName: "난지", + location: "경기도 고양시 덕양구 대차로 4가", + locationgubun: "500", + facilityCapacity: "800,000", + yn: "Y", + authoDay: "2012", + area:"123" + }, +] + +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, +} + +// 상태 설정 함수 타입 정의 +type SetStateFunction = (state: tableState | ((state: tableState) => tableState)) => void; + +export const tableActionsExport:(set:SetStateFunction)=>tableActions=(set: SetStateFunction) => { + + return { + decrementList:()=>set( + (state:tableState) => { + return { + ...state, + publicReuseFacilityUper: + state.publicReuseFacilityUper.slice(0, state.publicReuseFacilityUper.length - 1) + }} + ) + } +} diff --git a/apps/fo/app/store/rdi/index.tsx b/apps/fo/app/store/rdi/index.tsx new file mode 100644 index 000000000..e7acb2231 --- /dev/null +++ b/apps/fo/app/store/rdi/index.tsx @@ -0,0 +1,274 @@ +import DataForm from "@/app/components/common/DataForm"; +import type { TableUpperProps } from "../dashboard"; +import { getFetch } from "@/app/components/api"; +export type { TableUpperProps }; +/**==================================== + * Store - 타입 + ====================================*/ + +/** + * (1) 메인페이지 + */ +// 시설별 강수현황 +export type RdiProps = { + bscFctCd : string, + sido : string, + sigungo : string, + bscFctNm : string, + lclgvrnmntNm : string, + daddr : string, + fctCpc : number, + summerMinBodValue : number, + inflAmnt : number, + winterAvgBodLowRainfall : number, + rdiiValue : number, + infoRnDay : number, + rgnNm : string, + rvbsn : string, + lgbsn : string, + mdmbsn : string, +} + +export interface RdiInterface { + bscFctCd : string, + sido : string, + sigungo : string, + bscFctNm : string, + lclgvrnmntNm : string, + daddr : string, + fctCpc : number, + summerMinBodValue : number, + inflAmnt : number, + winterAvgBodLowRainfall : number, + rdiiValue : number, + infoRnDay : number, + rgnNm : string, + rvbsn : string, + lgbsn : string, + mdmbsn : string, +} + +// 모달 +export interface RdiInputModalProps { + msrDt : string, + realSewageInput : number | '', + sewageOccur : number | '', + underWaterInput : number | '', +} + +/**==================================== + * Store - data + ====================================*/ + +/** + * (1) 메인페이지 + */ +// 검색단 +const LabelArray:string[] = ['구분', '시도', '시군구','용량별', '시설명', '조회기간', '시설상태', '권역', '단위유역', '중권역', '환경청별']; + +export interface ExcelType { + 시설코드: string, + 권역:string|'', + 단위유역:string|'', + 중권역:string|'', + 환경청:string|'', + 시도: string, + 시군구: string, + 시설명: string, + 주소: string, + 시설용량: number, + 하절기강우일최저유입BOD: number | '', + 하절기최저유입BOD유입유량: number | '', + 동절기청천시유입BOD: number | '', + RDII추정 : number | '', + RDII추정시일강수량 : number | '', +} + +// 리스트 헤더 +const upHeadList1:TableUpperProps[] = [ + { id: "bscFctCd", title: "시설코드", row:0, rowSpan:2 }, + { id: "A", title: "행정구역", row:0, colSpan:2 }, + { id: "sido", title: "시도", upName:"upChangeRe", upSequnce:1, row:1 }, + { id: "sigungo", title: "시군구", upName:"upChangeRe",upSequnce:2, row:1 }, + { id: "bscFctNm", title: "시설명", row:0, rowSpan:2 }, + { id: "daddr", title: "주소", row:0, rowSpan:2 }, + { id: "fctCpc", title: "시설용량\n(m³/일)", align:"right", row:0, rowSpan:2 }, + { id: "B", title: "하절기 운영현황(6~8월)", row:0, colSpan:2 }, + { id: "summerMinBodValue", title: "3mm 이상 강우일 중 최저 유입BOD\n(mg/L)", align:"right", row:1 }, + { id: "inflAmnt", title: "하절기최저유입BOD유입유량\n(m³/일)", row:1 }, + { id: "C", title: "동절기 운영현황(1~2월)", row:0 }, + { id: "winterAvgBodLowRainfall", title: "청천시 유입BOD\n(mg/L)", row:1 }, + { id: "rdiiValue", title: "RDII추정", row:0, rowSpan:2 }, + { id: "infoRnDay", title: "RDII추정시일강수량\n(mm)", row:0, rowSpan:2 }, +]; + +const upHeadList2:TableUpperProps[] = [ + { id: "bscFctCd", title: "시설코드", row:0, rowSpan:2 }, + { id: "A", title: "유역", row:0, colSpan:3 }, + { id: "rvbsn", title: "권역", upName:"upChangeRe", upSequnce:1, row:1 }, + { id: "lgbsn", title: "단위유역", upName:"upChangeRe", upSequnce:2, row:1 }, + { id: "mdmbsn", title: "중권역", upName:"upChangeRe", upSequnce:3, row:1 }, + { id: "bscFctNm", title: "시설명", row:0, rowSpan:2 }, + { id: "daddr", title: "주소", row:0, rowSpan:2 }, + { id: "fctCpc", title: "시설용량(m³/일)", align:"right", row:0, rowSpan:2 }, + { id: "B", title: "하절기 운영현황(6~8월)", row:0, colSpan:2 }, + { id: "summerMinBodValue", title: "하절기강우일최저유입BOD", align:"right", row:1 }, + { id: "inflAmnt", title: "하절기최저유입BOD유입유량", row:1 }, + { id: "D", title: "동절기 운영현황(1~2월)", row:0 }, + { id: "winterAvgBodLowRainfall", title: "동절기청천시유입BOD", row:1 }, + { id: "rdiiValue", title: "RDII추정", row:0, rowSpan:2 }, + { id: "infoRnDay", title: "RDII추정시일강수량", row:0, rowSpan:2 }, +]; + +const upHeadList3:TableUpperProps[] = [ + { id: "bscFctCd", title: "시설코드", row:0, rowSpan:2 }, + { id: "A", title: "유역", row:0, colSpan:3 }, + { id: "rvbsn", title: "권역", upName:"upChangeRe1", upSequnce:1, row:1 }, + { id: "lgbsn", title: "단위유역", upName:"upChangeRe1", upSequnce:2, row:1 }, + { id: "mdmbsn", title: "중권역", upName:"upChangeRe1", upSequnce:3, row:1 }, + { id: "rgnNm", title: "환경청", row:0, rowSpan:2 }, + { id: "B", title: "행정구역", row:0, colSpan:2 }, + { id: "sido", title: "시도", upName:"upChangeRe2", upSequnce:1, row:1 }, + { id: "sigungo", title: "시군구", upName:"upChangeRe2",upSequnce:2, row:1 }, + { id: "bscFctNm", title: "시설명", row:0, rowSpan:2 }, + { id: "daddr", title: "주소", row:0, rowSpan:2 }, + { id: "fctCpc", title: "시설용량(m³/일)", align:"right", row:0, rowSpan:2 }, + { id: "C", title: "하절기 운영현황(6~8월)", row:0, colSpan:2 }, + { id: "summerMinBodValue", title: "하절기강우일최저유입BOD", align:"right", row:1 }, + { id: "inflAmnt", title: "하절기최저유입BOD유입유량", row:1 }, + { id: "D", title: "동절기 운영현황(1~2월)", row:0 }, + { id: "winterAvgBodLowRainfall", title: "동절기청천시유입BOD", row:1 }, + { id: "rdiiValue", title: "RDII추정", row:0, rowSpan:2 }, + { id: "infoRnDay", title: "RDII추정시일강수량", row:0, rowSpan:2 }, +]; + +const rdiList:RdiProps[] = [ +] + +/**==================================== + * Store - API + ====================================*/ + +const fetchRdiList = async (pageNo: number, pageSize: number, searchYear: string, partCd?: string, nameCd?: string, fcltCpct?: string, operationValue?: string, fcltyNm?: string) => { + try{ + const tailStr = partCd ? '/'+ partCd + '/' + nameCd + '/' + fcltCpct + '/' + operationValue + (fcltyNm && fcltyNm !== '' ? '/' + fcltyNm : '') : ''; + const url = '/rdii/list/' + pageNo + '/' + pageSize + '/' + searchYear + tailStr; + console.log(url); + const res = await getFetch('fo', url); + return { data: res.data, count: res.count } + }catch(error){ + console.log(error); + return { data: rdiList, count: 0 }; + } +} + +/**==================================== + * Store - expaort + ====================================*/ +export type RdiActions = { + searchRdiList: (pageNo: number, pageSize: number, searchYear: string, partCd?: string, nameCd?: string, fcltCpct?: string, operationValue?: string, fcltyNm?: string) => void; +} + +export type RdiType = { + RdiSearch: { + isInit: boolean; + labelArray:string[]; + }, + + Rdi : { + isInit: boolean; + upHeadList1: TableUpperProps[]; + upHeadList2: TableUpperProps[]; + upHeadList3: TableUpperProps[]; + rdiList: RdiProps[]; // Changed from facilityAreaProps to ProcessAreaProps + rdiCount: number; + excelList: ExcelType[]; + }, +} + +export const rdiInitState: RdiType = { + RdiSearch: { + isInit: false, + labelArray: LabelArray, + }, + + Rdi : { + isInit : false, + upHeadList1 : upHeadList1, + upHeadList2 : upHeadList2, + upHeadList3 : upHeadList3, + rdiList : rdiList, + rdiCount : 2, + excelList : [] + } +} + +const dataFormSetting=(item : RdiProps)=> { + DataForm({ type: 'float1', column: 'fctCpc', item: item as unknown as Record<string, unknown> }); + DataForm({ type: 'float1', column: 'summerMinBodValue', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float1', column: 'winterAvgBodLowRainfall', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float2', column: 'rdiiValue', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float2', column: 'infoRnDay', item: item as unknown as Record<string, unknown>}); +} + +// Add type definitions for state setter function +type SetStateFunction = (state: RdiType | ((state: RdiType) => RdiType)) => void; + +export type RdiStore = RdiType & RdiActions; + +export const rdiReducer:(set:SetStateFunction)=>RdiActions=(set: SetStateFunction) => { + console.log("tableActionsExport"); + return { + searchRdiList: async (pageNo: number, pageSize: number, searchYear: string, partCd?: string, nameCd?: string, fcltCpct?: string, operationValue?: string, fcltyNm?: string) => { + const res = await fetchRdiList(pageNo, pageSize, searchYear, partCd, nameCd, fcltCpct, operationValue, fcltyNm); + const data = res.data; + const excelDownList: ExcelType[] = []; + + const rtnData = data.map((item: RdiProps) => { + return { + ...item, + sido: item.lclgvrnmntNm.split(' ')[0]?.toString() || '', + sigungo: item.lclgvrnmntNm.split(' ')[1]?.toString() || '', + inflAmnt: item.inflAmnt?item.inflAmnt.toFixed(1) : '' + } + }) + + rtnData.forEach((item: RdiProps) => { + dataFormSetting(item) + }) + + if(pageSize>5000){ + rtnData.forEach((item: RdiProps) => { + excelDownList.push({ + 시설코드: item.bscFctCd, + 권역: item.rvbsn, + 단위유역: item.lgbsn, + 중권역: item.mdmbsn, + 환경청: item.rgnNm, + 시설명: item.bscFctNm, + 시도: item.sido, + 시군구: item.sigungo, + 주소: item.daddr, + 시설용량: item.fctCpc, + 하절기강우일최저유입BOD: item.summerMinBodValue, + 하절기최저유입BOD유입유량: item.inflAmnt, + 동절기청천시유입BOD: item.winterAvgBodLowRainfall, + RDII추정 : item.rdiiValue, + RDII추정시일강수량 : item.infoRnDay, + }); + }); + } + set((state: RdiType) => ({ + ...state, + Rdi: pageSize<5000?{ + ...state.Rdi, + rdiList: rtnData, + rdiCount: res.count, + }:{ + ...state.Rdi, + excelList: excelDownList, + } + })) + }, + } +} \ No newline at end of file diff --git a/apps/fo/app/store/sewageOccur/index.tsx b/apps/fo/app/store/sewageOccur/index.tsx new file mode 100644 index 000000000..f5d300aa1 --- /dev/null +++ b/apps/fo/app/store/sewageOccur/index.tsx @@ -0,0 +1,334 @@ +import type { TableUpperProps } from "../dashboard"; +import { getFetch } from "@/app/components/api"; +export type { TableUpperProps }; +/**==================================== + * Store - 타입 + ====================================*/ + +/** + * (1) 메인페이지 + */ +// 시설별 강수현황 +export type SewageOccurProps = { + bscFctCd : string, + sidoNm : string, + sggNm : string, + lclgvrnmntNm : string, + bscFctNm : string, + daddr : string, + fctCpc : number, + waterUsage : number, + swageChangePercent : number, + estimationSewage : number, + sewageInputYear : number, + sewageInputChungchun : number, + rgnNm : string, + rvbsn : string, + lgbsn : string, + mdmbsn : string, + msrDt : string, +} +export type modalTableType = { msrDt: string,waterUsage: number,swageChangePercent: number,estimationSewage: number}; +export type SewageOccurDetailProps = { + cnt : number, + sum : number, + yr:string, + usege:number +} + +// 검색단 +const LabelArray:string[] = ['구분', '시도', '시군구','용량별', '시설명', '조회기간', '시설상태', '권역', '단위유역', '중권역', '환경청별']; + +export interface ExcelType { + 시설코드: string, + 권역:string|'', + 단위유역:string|'', + 중권역:string|'', + 환경청:string|'', + 시도: string, + 시군구: string, + 시설명: string, + 주소: string, + 시설용량: number, + 물사용량: number | '', + 오수전환율: number | '', + 오수발생량추정: number | '', + 연간하수유입량: number | '', + 청천시하수유입량: number | '', +} + +// 리스트 헤더 +const upHeadList1:TableUpperProps[] = [ + { id: "bscFctCd", title: "시설코드", row:0, rowSpan:2 }, + { id: "A", title: "행정구역", row:0, colSpan:2 }, + { id: "sidoNm", title: "시도", upName:"upChangeRe", upSequnce:1, row:1 }, + { id: "sggNm", title: "시군구", upName:"upChangeRe",upSequnce:2, row:1 }, + { id: "bscFctNm", title: "시설명", row:0, rowSpan:2 }, + { id: "daddr", title: "주소", row:0, rowSpan:2 }, + { id: "fctCpc", title: "시설용량(m³/일)", align:"right", row:0, rowSpan:2 }, + { id: "waterUsage", title: "물사용량(m³/일)", align:"right", row:0, rowSpan:2 }, + { id: "swageChangePercent", title: "오수전환율\n(%)", align:"right", row:0, rowSpan:2 }, + { id: "estimationSewage", title: "오수발생량 추정\n(m³/일)", align:"right", row:0, rowSpan:2 }, + { id: "sewageInputYear", title: "연간 하수유입량\n(m³/일)", align:"right", row:0, rowSpan:2 }, + { id: "sewageInputChungchun", title: "청천시 하수유입량\n(m³/일)", align:"right", row:0, rowSpan:2 }, +]; + +const upHeadList2:TableUpperProps[] = [ + { id: "bscFctCd", title: "시설코드", row:0, rowSpan:2 }, + { id: "A", title: "유역", row:0, colSpan:3 }, + { id: "rvbsn", title: "권역", upName:"upChangeRe", upSequnce:1, row:1 }, + { id: "lgbsn", title: "단위유역", upName:"upChangeRe", upSequnce:2, row:1 }, + { id: "mdmbsn", title: "중권역", upName:"upChangeRe", upSequnce:3, row:1 }, + { id: "bscFctNm", title: "시설명", row:0, rowSpan:2 }, + { id: "daddr", title: "주소", row:0, rowSpan:2 }, + { id: "fctCpc", title: "시설용량(m³/일)", align:"right", row:0, rowSpan:2 }, + { id: "waterUsage", title: "물사용량(m³/일)", align:"right", row:0, rowSpan:2 }, + { id: "swageChangePercent", title: "오수전환율", align:"right", row:0, rowSpan:2 }, + { id: "estimationSewage", title: "오수발생량 추정\n(m³/일)", align:"right", row:0, rowSpan:2 }, + { id: "sewageInputYear", title: "연간 하수유입량\n(m³/일)", align:"right", row:0, rowSpan:2 }, + { id: "sewageInputChungchun", title: "청천시 하수유입량\n(m³/일)", align:"right", row:0, rowSpan:2 }, +]; + +const upHeadList3:TableUpperProps[] = [ + { id: "bscFctCd", title: "시설코드", row:0, rowSpan:2 }, + { id: "A", title: "유역", row:0, colSpan:3 }, + { id: "rvbsn", title: "권역", upName:"upChangeRe1", upSequnce:1, row:1 }, + { id: "lgbsn", title: "단위유역", upName:"upChangeRe1", upSequnce:2, row:1 }, + { id: "mdmbsn", title: "중권역", upName:"upChangeRe1", upSequnce:3, row:1 }, + { id: "rgnNm", title: "환경청", row:0, rowSpan:2 }, + { id: "B", title: "행정구역", row:0, colSpan:2 }, + { id: "sidoNm", title: "시도", upName:"upChangeRe2", upSequnce:1, row:1 }, + { id: "sggNm", title: "시군구", upName:"upChangeRe2",upSequnce:2, row:1 }, + { id: "bscFctNm", title: "시설명", row:0, rowSpan:2 }, + { id: "daddr", title: "주소", row:0, rowSpan:2 }, + { id: "fctCpc", title: "시설용량(m³/일)", align:"right", row:0, rowSpan:2 }, + { id: "waterUsage", title: "물사용량(m³/일)", align:"right", row:0, rowSpan:2 }, + { id: "swageChangePercent", title: "오수전환율", align:"right", row:0, rowSpan:2 }, + { id: "estimationSewage", title: "오수발생량 추정\n(m³/일)", align:"right", row:0, rowSpan:2 }, + { id: "sewageInputYear", title: "연간 하수유입량\n(m³/일)", align:"right", row:0, rowSpan:2 }, + { id: "sewageInputChungchun", title: "청천시 하수유입량\n(m³/일)", align:"right", row:0, rowSpan:2 }, +]; + +const upHeadListSewageOccurDetail:TableUpperProps[] = [ + { id: "msrDt", title: "년월" }, + { id: "waterUsage", title: "물사용량(m³/일)" }, + { id: "swageChangePercent", title: "오수전환율(%)" }, + { id: "estimationSewage", title: "오수발생량 추정(m³/일)", row:0, rowSpan:2 }, +]; + +const sewageOccurList:SewageOccurProps[] = [] + +const tempSewageOccurList:modalTableType[] = [] + +/**==================================== + * Store - API + ====================================*/ + +const fetchSewageOccurList = async (pageNo: number, pageSize: number, searchYear: string, partCd?: string, nameCd?: string, fcltCpct?: string, operationValue?: string, fcltyNm?: string) => { + // 20250701임시시 + console.log("searchYear", searchYear); + const tailStr = partCd ? '/'+ partCd + '/' + nameCd + '/' + fcltCpct + '/' + operationValue + (fcltyNm && fcltyNm !== '' ? '/' + fcltyNm : '') : ''; + const url = '/fclt/flow/' + pageNo + '/' + pageSize + '/' + searchYear + tailStr; + const res = await getFetch('fo', url); + console.log("res", res.data); + return { data: res.data, count: res.count } + + /* + const tailStr = partCd ? '/'+ partCd + '/' + nameCd + '/' + fcltCpct + '/' + operationValue + (fcltyNm && fcltyNm !== '' ? '/' + fcltyNm : '') : ''; + const url = '/fclt/qlty/' + pageNo + '/' + pageSize + '/' + searchYear + tailStr; + const res = await getFetch('fo', url); + return { data: res.data, count: res.count } */ +} + +/**==================================== + * Store - expaort + ====================================*/ +export type SewageOccurActions = { + searchSewageOccurList: (pageNo: number, pageSize: number, searchYear: string, part?: string, nameCd?: string, fcltCpct?: string, operationValue?: string, fcltyNm?: string) => void; + searchSewageOccurDetailList: (bscFctCd: string, searchYear: string) => void; +} + +export type SewageOccurType = { + SewageOccurSearch: { + isInit: boolean; + labelArray:string[]; + }, + + SewageOccur : { + isInit: boolean; + upHeadList1: TableUpperProps[]; + upHeadList2: TableUpperProps[]; + upHeadList3: TableUpperProps[]; + sewageOccurList: SewageOccurProps[]; // Changed from facilityAreaProps to ProcessAreaProps + sewageOccurCount: number; + excelList: ExcelType[]; + }, + + SewageOccurDetail : { + isInit: boolean; + upHeadListSewageOccurDetail: TableUpperProps[]; + gridListSewageOccurDetail: modalTableType[]; + } +} + +export const sewageOccurInitState: SewageOccurType = { + SewageOccurSearch: { + isInit: false, + labelArray: LabelArray, + }, + + SewageOccur : { + isInit : false, + upHeadList1 : upHeadList1, + upHeadList2 : upHeadList2, + upHeadList3 : upHeadList3, + sewageOccurList : sewageOccurList, + sewageOccurCount : 2, + excelList : [] + }, + + SewageOccurDetail : { + isInit: false, + upHeadListSewageOccurDetail: upHeadListSewageOccurDetail, + gridListSewageOccurDetail: tempSewageOccurList, + } +} + +export type SewageOccurStore = SewageOccurType & SewageOccurActions; + +// Add type definitions for API responses +type FlowDataItem = { + inflAmnt: string | number; + infoRnDay: string | number; + bscFctCd: string; + msrDt: string; +} + +type FlowResponse = { + data: FlowDataItem[]; + count: number; +} + +export const sewageOccurReducer:(set:any)=>SewageOccurActions=(set: any) => { + return { + searchSewageOccurList: async (pageNo: number, pageSize: number, searchYear: string, part?: string, nameCd?: string, fcltCpct?: string, operationValue?: string, fcltyNm?: string) => { + const res = await fetchSewageOccurList(pageNo, pageSize, searchYear, part, nameCd, fcltCpct, operationValue, fcltyNm); + const data = res.data; + + // Promise.all을 사용하여 모든 비동기 작업이 완료될 때까지 기다림 + const calData = await Promise.all(data.map(async (item: SewageOccurProps) => { + item.sidoNm = item.lclgvrnmntNm.split(' ')[0]?.toString() || ''; + item.sggNm = item.lclgvrnmntNm.split(' ')[1]?.toString() || ''; + item.fctCpc = parseFloat(item.fctCpc.toString().replace(/,/g, '')); + item.swageChangePercent = 80; + + + const bscFctCd = item.bscFctCd; + const res2 = await getFetch('fo', '/fclt/flow/' + bscFctCd + '/' + searchYear) as FlowResponse; + + let sumChungcunDay = 0; + let chungcunSumFlow = 0; + let totWaterUsage = 0; + res2.data.forEach((item: FlowDataItem) => { + if(Number(item.infoRnDay) ===0){ + chungcunSumFlow += Number(item.inflAmnt); + sumChungcunDay += 1; + } + totWaterUsage += Number(item.inflAmnt); + }); + const avg = totWaterUsage /res2.count; + item.waterUsage = parseFloat((avg * 0.65).toFixed(2)); + item.sewageInputYear = parseFloat(avg.toFixed(2)); + item.sewageInputChungchun = parseFloat((chungcunSumFlow===0?0:(chungcunSumFlow / sumChungcunDay)).toFixed(2)); + item.estimationSewage = parseFloat((item.waterUsage * (item.swageChangePercent/100)).toFixed(2)); + return item; + })); + // 조회 데이터 설정 + set((state: any) => ({ + ...state, + SewageOccur: pageSize<5000?{ + ...state.SewageOccur, + sewageOccurList: calData, + sewageOccurCount: res.count, + }:{ + ...state.SewageOccur, + excelList: [], + } + })) + }, + + searchSewageOccurDetailList: async (bscFctCd: string, searchYear: string) => { + const res = await getFetch('fo', '/fclt/flow/' + bscFctCd + '/' + searchYear); + const monthData: SewageOccurDetailProps[] = []; + let tot = 0, cnt= 0 + res.data.forEach((item: FlowDataItem) => { + const month = item.msrDt?.substring(4, 6); + const monthIdx = month ? Number(month) - 1 : -1; + if (monthIdx >= 0) { + if (typeof monthData[monthIdx] === 'object') { + monthData[monthIdx].sum += Number(item.inflAmnt); + monthData[monthIdx].cnt++; + } else { + monthData[monthIdx] = {cnt:1, sum:Number(item.inflAmnt), yr:item.msrDt.substring(0,4), usege:0}; + } + } + tot += Number(item.inflAmnt) + cnt++ + }) + console.log('tot', tot, cnt, tot/cnt, tot/cnt*0.65); + const tableData:modalTableType[] = [] + + let maxWaterUsage = 0; + let minWaterUsage = 9999999999; + let avgWaterUsage = 0; + monthData.forEach((item, index) => { + item.usege = item.sum/item.cnt *0.65 + tableData.push({ + msrDt: item.yr+'-'+(index+1>10?String(index+1):'0'+(index+1)), + waterUsage: Math.round(item.usege*100) /100, + swageChangePercent: (Math.round((item.usege/item.usege*0.8) * 100) / 100) * 100, + estimationSewage: Math.round(item.usege*0.8 * 100) / 100 + }) + if (item.usege > maxWaterUsage) { + maxWaterUsage = item.usege; + } + + if (item.usege < minWaterUsage) { + minWaterUsage = item.usege; + } + + }); + avgWaterUsage = tot/cnt*0.65 + + const newData= [ + { + msrDt: '평균', + waterUsage: Math.round(avgWaterUsage*100) /100, + swageChangePercent: 80, + estimationSewage: Math.round(avgWaterUsage*0.8*100) /100, + }, + { + msrDt: '최대', + waterUsage: Math.round(maxWaterUsage*100) /100, + swageChangePercent: 80, + estimationSewage: Math.round(maxWaterUsage*0.8*100) /100, + }, + { + msrDt: '최소', + waterUsage: Math.round(minWaterUsage*100) /100, + swageChangePercent: 80, + estimationSewage: Math.round(minWaterUsage*0.8*100) /100, + }, + ...tableData + ] + + set((state: SewageOccurType) => ({ + ...state, + SewageOccurDetail: { + ...state.SewageOccurDetail, + gridListSewageOccurDetail: newData, + } + })) + }, + } +} \ No newline at end of file diff --git a/apps/fo/app/store/store.tsx b/apps/fo/app/store/store.tsx new file mode 100644 index 000000000..512458c90 --- /dev/null +++ b/apps/fo/app/store/store.tsx @@ -0,0 +1,80 @@ +import { LoginApi } from '@/src/entities/auth/api/login-service'; +import { createStore } from 'zustand/vanilla'; +import { CommonReducer, commonState, type CommonStore, type CommonType } from './common'; +import { dashBoardInitState, dashBoardReducer, type DashBoardStore, type DashBoardType } from './dashboard'; +import { + facilityPrecipitationInitState, + facilityPrecipitationReducer, + type FacilityPrecipitationStore, type FacilityPrecipitationType +} from './facilityPrecipitation'; +import { + facilityWaterQltyInitState, + facilityWaterQltyReducer, + type FacilityWaterQltyStore, type FacilityWaterQltyType +} from './facilityWaterQlty'; +import { flowRateSearchInitState, flowRateSearchReducer, type FlowRateSearchStore, type FlowRateSearchType } from './flowRate'; +import { planSewageInitState, planSewageReducer, type PlanSewageStore, type PlanSewageType } from './planSewage'; +import { + processAreaInitState, + processAreaReducer, + type ProcessAreaStore, type ProcessAreaType +} from './processArea'; +import { + processFacilityInitState, + processFacilityReducer, + type ProcessFacilityStore, type ProcessFacilityType +} from './processFacility'; +import { tableActionsExport, tableStateExport, type tableState, type tableType } from './publicReuseFaciltyInsert/index'; +import { + rdiInitState, + rdiReducer, + type RdiStore, type RdiType +} from './rdi'; +import { + sewageOccurInitState, + sewageOccurReducer, + type SewageOccurStore, type SewageOccurType +} from './sewageOccur'; +import { + underWaterInitState, + underWaterReducer, + type UnderWaterStore, type UnderWaterType +} from './underWaterInput'; +export type FoStore = CommonStore & tableType & DashBoardStore & ProcessFacilityStore & ProcessAreaStore & FlowRateSearchStore & FacilityPrecipitationStore & FacilityWaterQltyStore & PlanSewageStore & SewageOccurStore & UnderWaterStore & RdiStore; +export type FoState = CommonType & tableState & ProcessFacilityType & ProcessAreaType & FacilityPrecipitationType & FlowRateSearchType & DashBoardType & FacilityWaterQltyType & PlanSewageType & SewageOccurType & UnderWaterType & RdiType; + +export const StoreZus = + (initState: FoState = { ...commonState, + ...tableStateExport + , ...processFacilityInitState + , ...processAreaInitState + , ...facilityPrecipitationInitState + , ...dashBoardInitState + , ...flowRateSearchInitState + , ...facilityWaterQltyInitState + , ...planSewageInitState + , ...sewageOccurInitState + , ...underWaterInitState + , ...rdiInitState + } + ) => { + return createStore<FoStore>()((set) => { + LoginApi.login({userId: "admin1gmail", password: "test1234!"}) + return { + ...initState, + ...CommonReducer(set), + ...tableActionsExport(set), + ...dashBoardReducer(set), + ...processFacilityReducer(set), + ...processAreaReducer(set), + ...facilityPrecipitationReducer(set), + ...flowRateSearchReducer(set), + ...facilityWaterQltyReducer(set), + ...planSewageReducer(set), + ...sewageOccurReducer(set), + ...underWaterReducer(set), + ...rdiReducer(set), + } + }) +} +//store 등록하는 부분 \ No newline at end of file diff --git a/apps/fo/app/store/underWaterInput/index.tsx b/apps/fo/app/store/underWaterInput/index.tsx new file mode 100644 index 000000000..912d8673e --- /dev/null +++ b/apps/fo/app/store/underWaterInput/index.tsx @@ -0,0 +1,414 @@ +import DataForm from "@/app/components/common/DataForm"; +import type { TableUpperProps } from "../dashboard"; +import { getFetch } from "@/app/components/api"; +import type { ExcelType, UnderWaterInputModalProps, UnderWaterProps } from "./types"; +import type { SewageOccurDetailProps } from "../sewageOccur"; +export type { TableUpperProps }; +/**==================================== + * Store - 타입 + ====================================*/ + +/** + * (1) 메인페이지 + */ +// 시설별 강수현황 + +/**==================================== + * Store - data + ====================================*/ + +/** + * (1) 메인페이지 + */ +// 검색단 +const LabelArray:string[] = ['구분', '시도', '시군구','용량별', '시설명', '조회기간', '시설상태', '권역', '단위유역', '중권역', '환경청별']; + +// 리스트 헤더 +const upHeadList1:TableUpperProps[] = [ + { id: "bscFctCd", title: "시설코드", row:0, rowSpan:2 }, + { id: "A", title: "행정구역", row:0, colSpan:2 }, + { id: "sidoNm", title: "시도", upName:"upChangeRe", upSequnce:1, row:1 }, + { id: "sggNm", title: "시군구", upName:"upChangeRe",upSequnce:2, row:1 }, + { id: "bscFctNm", title: "시설명", row:0, rowSpan:2 }, + { id: "daddr", title: "주소", row:0, rowSpan:2 }, + { id: "fctCpc", title: "시설용량(m³/일)", align:"right", row:0, rowSpan:2 }, + { id: "C", title: "지하수 유입량 추정", align:"right", row:0, colSpan:3 }, + { id: "underWaterInputYear", title: "연간\n(m³/일)", align:"right", row:1 }, + { id: "underWaterInputSummer", title: "하절기\n(m³/일)", align:"right", row:1 }, + { id: "underWaterInputWinter", title: "동절기\n(m³/일)", align:"right", row:1 }, +]; + +const upHeadList2:TableUpperProps[] = [ + { id: "bscFctCd", title: "시설코드", row:0, rowSpan:2 }, + { id: "A", title: "유역", row:0, colSpan:3 }, + { id: "rvbsn", title: "권역", upName:"upChangeRe", upSequnce:1, row:1 }, + { id: "lgbsn", title: "단위유역", upName:"upChangeRe", upSequnce:2, row:1 }, + { id: "mdmbsn", title: "중권역", upName:"upChangeRe", upSequnce:3, row:1 }, + { id: "bscFctNm", title: "시설명", row:0, rowSpan:2 }, + { id: "daddr", title: "주소", row:0, rowSpan:2 }, + { id: "fctCpc", title: "시설용량(m³/일)", align:"right", row:0, rowSpan:2 }, + { id: "C", title: "지하수 유입량 추정", align:"right", row:0, colSpan:3 }, + { id: "underWaterInputYear", title: "연간\n(m³/일)", align:"right", row:1 }, + { id: "underWaterInputSummer", title: "하절기\n(m³/일)", align:"right", row:1 }, + { id: "underWaterInputWinter", title: "동절기\n(m³/일)", align:"right", row:1 }, +]; + +const upHeadList3:TableUpperProps[] = [ + { id: "bscFctCd", title: "시설코드", row:0, rowSpan:2 }, + { id: "A", title: "유역", row:0, colSpan:3 }, + { id: "rvbsn", title: "권역", upName:"upChangeRe1", upSequnce:1, row:1 }, + { id: "lgbsn", title: "단위유역", upName:"upChangeRe1", upSequnce:2, row:1 }, + { id: "mdmbsn", title: "중권역", upName:"upChangeRe1", upSequnce:3, row:1 }, + { id: "rgnNm", title: "환경청", row:0, rowSpan:2 }, + { id: "B", title: "행정구역", row:0, colSpan:2 }, + { id: "sidoNm", title: "시도", upName:"upChangeRe2", upSequnce:1, row:1 }, + { id: "sggNm", title: "시군구", upName:"upChangeRe2",upSequnce:2, row:1 }, + { id: "bscFctNm", title: "시설명", row:0, rowSpan:2 }, + { id: "daddr", title: "주소", row:0, rowSpan:2 }, + { id: "fctCpc", title: "시설용량(m³/일)", align:"right", row:0, rowSpan:2 }, + { id: "C", title: "지하수 유입량 추정", align:"right", row:0, colSpan:3 }, + { id: "underWaterInputYear", title: "연간\n(m³/일)", align:"right", row:1 }, + { id: "underWaterInputSummer", title: "하절기\n(m³/일)", align:"right", row:1 }, + { id: "underWaterInputWinter", title: "동절기\n(m³/일)", align:"right", row:1 }, +]; + +// 모달 +const upHeadListUnderWaterDetail:TableUpperProps[] = [ + { id: "msrDt", title: "날짜" }, + { id: "realSewageInput", title: "실제 하수유입량\n(m³/일)" }, + { id: "sewageOccur", title: "오수발생량\n(m³/일)" }, + { id: "underWaterInput", title: "지하수 유입량\n(m³/일)" }, +]; + +const underWaterList:UnderWaterProps[] = [ +] + +const underWaterDetailList:UnderWaterInputModalProps[] = [ + { + msrDt : "2024-01", + realSewageInput : 178007, + sewageOccur : 10.5, + underWaterInput : 80.6, + }, + { + msrDt : "2024-02", + realSewageInput : 123007, + sewageOccur : 10.7, + underWaterInput : 80.6, + }, + { + msrDt : "2024-03", + realSewageInput : 213007, + sewageOccur : 10.1, + underWaterInput : 80.6, + }, + { + msrDt : "2024-04", + realSewageInput : 83007, + sewageOccur : 9.57, + underWaterInput : 80.6, + }, + { + msrDt : "2024-05", + realSewageInput : 15007, + sewageOccur : 3.57, + underWaterInput : 80.6, + }, + { + msrDt : "2024-06", + realSewageInput : 123007, + sewageOccur : '', + underWaterInput : 80.6, + }, + { + msrDt : "2024-07", + realSewageInput : 101007, + sewageOccur : 1, + underWaterInput : 80.6, + }, + { + msrDt : "2024-08", + realSewageInput : 103807, + sewageOccur : 10.57, + underWaterInput : '', + }, + { + msrDt : "2024-09", + realSewageInput : 9007, + sewageOccur : 10.57, + underWaterInput : 80.6, + }, + { + msrDt : "2024-10", + realSewageInput : 163007, + sewageOccur : 10.57, + underWaterInput : 80.6, + }, + { + msrDt : "2024-11", + realSewageInput : 123007, + sewageOccur : 10.57, + underWaterInput : 80.6, + }, + { + msrDt : "2024-12", + realSewageInput : 1007, + sewageOccur : 10.57, + underWaterInput : 80.6, + }, +] +/**==================================== + * Store - API + ====================================*/ + +// API response types +interface FlowDataItem { + inflAmnt: number; + infoRnDay: number; + msrDt: string; +} + +interface ApiResponse { + data: FlowDataItem[]; + count: number; +} + +const fetchUnderWaterList = async (pageNo: number, pageSize: number, searchYear: string, partCd?: string, nameCd?: string, fcltCpct?: string, operationValue?: string, fcltyNm?: string) => { + const tailStr = partCd ? '/'+ partCd + '/' + nameCd + '/' + fcltCpct + '/' + operationValue + (fcltyNm && fcltyNm !== '' ? '/' + fcltyNm : '') : ''; + const url = '/fclt/flow/' + pageNo + '/' + pageSize + '/' + searchYear + tailStr; + const res = await getFetch('fo', url); + return { data: res.data, count: res.count } + /* + const tailStr = partCd ? '/'+ partCd + '/' + nameCd + '/' + fcltCpct + '/' + operationValue + (fcltyNm && fcltyNm !== '' ? '/' + fcltyNm : '') : ''; + const url = '/fclt/qlty/' + pageNo + '/' + pageSize + '/' + searchYear + tailStr; + const res = await getFetch('fo', url); + return { data: res.data, count: res.count } */ + + //return { data: underWaterList, count: 2 }; +} + +/**==================================== + * Store - expaort + ====================================*/ +export type UnderWaterActions = { + searchUnderWaterList: (pageNo: number, pageSize: number, searchYear: string, part?: string, nameCd?: string, fcltCpct?: string, operationValue?: string, fcltyNm?: string) => void; + unserWaterModal:(bscFctCd:string, searchYear:string)=>void +} + +export type UnderWaterType = { + UnderWaterSearch: { + isInit: boolean; + labelArray:string[]; + }, + + UnderWater : { + isInit: boolean; + upHeadList1: TableUpperProps[]; + upHeadList2: TableUpperProps[]; + upHeadList3: TableUpperProps[]; + underWaterList: UnderWaterProps[]; // Changed from facilityAreaProps to ProcessAreaProps + underWaterCount: number; + excelList: ExcelType[]; + }, + + UnderWaterDetail : { + isInit: boolean; + upHeadListUnderWaterDetail: TableUpperProps[]; + gridListUnderWaterDetail: UnderWaterInputModalProps[]; + } +} + +export const underWaterInitState: UnderWaterType = { + UnderWaterSearch: { + isInit: false, + labelArray: LabelArray, + }, + + UnderWater : { + isInit : false, + upHeadList1 : upHeadList1, + upHeadList2 : upHeadList2, + upHeadList3 : upHeadList3, + underWaterList : underWaterList, + underWaterCount : 4, + excelList : [] + }, + + UnderWaterDetail : { + isInit: false, + upHeadListUnderWaterDetail: upHeadListUnderWaterDetail, + gridListUnderWaterDetail: underWaterDetailList, + } +} + +const dataFormSetting=(item : UnderWaterProps)=> { + DataForm({ type: 'float1', column: 'fctCpc', item: item as unknown as Record<string, unknown> }); + DataForm({ type: 'float1', column: 'underWaterInputYear', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float2', column: 'underWaterInputSummer', item: item as unknown as Record<string, unknown>}); + DataForm({ type: 'float2', column: 'underWaterInputWinter', item: item as unknown as Record<string, unknown>}); +} + +export type UnderWaterStore = UnderWaterType & UnderWaterActions; + +export const underWaterReducer:(set:(fn: (state: UnderWaterType) => UnderWaterType) => void)=>UnderWaterActions=(set: (fn: (state: UnderWaterType) => UnderWaterType) => void) => { + return { + searchUnderWaterList: async (pageNo: number, pageSize: number, searchYear: string, part?: string, nameCd?: string, fcltCpct?: string, operationValue?: string, fcltyNm?: string) => { + const res = await fetchUnderWaterList(pageNo, pageSize, searchYear, part, nameCd, fcltCpct, operationValue, fcltyNm); + const data = res.data; + const calData = await Promise.all(data.map(async (item: UnderWaterProps) => { + item.sidoNm = item.lclgvrnmntNm.split(' ')[0]?.toString() || ''; + item.sggNm = item.lclgvrnmntNm.split(' ')[1]?.toString() || ''; + item.fctCpc = parseFloat(item.fctCpc.toString().replace(/,/g, '')); + + const bscFctCd = item.bscFctCd; + const res2 = await getFetch('fo', '/fclt/flow/' + bscFctCd + '/' + searchYear) as ApiResponse; + + let sumChungcunDay = 0; + let chungcunSumFlow = 0; + + let sumSummerChungcunDay = 0; + let sumSummerChungcunFlow = 0; + + let sumWinterChungcunDay = 0; + let sumWinterChungcunFlow = 0; + + res2.data.forEach((flowItem: FlowDataItem) => { + // 청천시 + if(Number(flowItem.infoRnDay) ===0){ + sumChungcunDay += 1; + chungcunSumFlow += Number(flowItem.inflAmnt); + + if(flowItem.msrDt.substring(4,6) ==='06' || flowItem.msrDt.substring(4,6) ==='07' || flowItem.msrDt.substring(4,6) ==='08'){ + sumSummerChungcunDay += 1; + sumSummerChungcunFlow += Number(flowItem.inflAmnt); + }else if(flowItem.msrDt.includes('01') || flowItem.msrDt.includes('02')){ + sumWinterChungcunDay += 1; + sumWinterChungcunFlow += Number(flowItem.inflAmnt); + } + } + }); + item.underWaterInputYear = Number(chungcunSumFlow==0?0:Math.round((chungcunSumFlow / sumChungcunDay) * 10) / 10) - Number(item.fctCpc * 0.6 * 0.8); + item.underWaterInputSummer = Number(sumSummerChungcunFlow==0?0:Math.round((sumSummerChungcunFlow / sumSummerChungcunDay) * 10) / 10) - Number(item.fctCpc * 0.6 * 0.8); + item.underWaterInputWinter = Number(sumWinterChungcunFlow==0?0:Math.round((sumWinterChungcunFlow / sumWinterChungcunDay) * 10) / 10) - Number(item.fctCpc * 0.6 * 0.8); + + return item; + })); + + const excelDownList: ExcelType[] = []; + if(pageSize>5000){ + calData.forEach(item => { + dataFormSetting(item) + item.fctCpc = parseFloat(item.fctCpc.toString().replace(/,/g, '')); + /* item.sidoNm = item.lclgvrnmntNm.split(' ')[0]?.toString() || ''; + item.sggNm = item.lclgvrnmntNm.split(' ')[1]?.toString() || ''; */ + excelDownList.push({ + 시설코드: item.bscFctCd, + 권역: item.rvbsn, + 단위유역: item.lgbsn, + 중권역: item.mdmbsn, + 환경청: item.rgnNm, + 시도: item.sidoNm, + 시군구: item.sggNm, + 시설명: item.bscFctNm, + 주소: item.daddr, + 시설용량: item.fctCpc, + 연간지하수유입량: item.underWaterInputYear, + 하절기지하수유입량: item.underWaterInputSummer, + 동절기지하수유입량: item.underWaterInputWinter, + + }); + }); + }else{ + data.forEach((item: UnderWaterProps) => { + dataFormSetting(item) + item.fctCpc = parseFloat(item.fctCpc.toString().replace(/,/g, '')); + /* item.sidoNm = item.lclgvrnmntNm.split(' ')[0]?.toString() || ''; + item.sggNm = item.lclgvrnmntNm.split(' ')[1]?.toString() || ''; */ + }); + } + set((state: UnderWaterType) => ({ + ...state, + UnderWater: pageSize<5000?{ + ...state.UnderWater, + underWaterList: calData, + underWaterCount: res.count, + }:{ + ...state.UnderWater, + excelList: excelDownList, + } + })) + }, + unserWaterModal:async (bscFctCd, searchYear)=>{ + const res = await getFetch('fo', '/fclt/flow/' + bscFctCd + '/' + searchYear); + const monthData: SewageOccurDetailProps[] = []; + let tot = 0, cnt= 0 + res.data.forEach((item: FlowDataItem) => { + const month = item.msrDt?.substring(4, 6); + const monthIdx = month ? Number(month) - 1 : -1; + if (monthIdx >= 0) { + if (typeof monthData[monthIdx] === 'object') { + monthData[monthIdx].sum += Number(item.inflAmnt); + monthData[monthIdx].cnt++; + } else { + monthData[monthIdx] = {cnt:1, sum:Number(item.inflAmnt), yr:item.msrDt.substring(0,4), usege:0}; + } + } + tot += Number(item.inflAmnt) + cnt++ + }) + const tableData:UnderWaterInputModalProps[] = [] + + let maxWaterUsage = 0; + let minWaterUsage = 9999999999; + let avgWaterUsage = 0; + monthData.forEach((item, index) => { + item.usege = item.sum/item.cnt + tableData.push({ + msrDt: item.yr+'-'+(index+1>10?String(index+1):'0'+(index+1)), + realSewageInput:Math.round(item.usege*100) /100, + sewageOccur:Math.round(item.usege*0.65*0.8 * 100) / 100, + underWaterInput:Math.round((item.usege-(item.usege*0.65*0.8)) * 100) / 100, + }) + if (item.usege > maxWaterUsage) { + maxWaterUsage = item.usege; + } + + if (item.usege < minWaterUsage) { + minWaterUsage = item.usege; + } + + }); + avgWaterUsage = tot/cnt + + const newData= [ + { + msrDt: '평균', + realSewageInput:Math.round(avgWaterUsage*100) /100, + sewageOccur:Math.round(avgWaterUsage*0.65*0.8 * 100) / 100, + underWaterInput:Math.round((avgWaterUsage-(avgWaterUsage*0.65*0.8)) * 100) / 100, + }, + { + msrDt: '최대', + realSewageInput: Math.round(maxWaterUsage*100) /100, + sewageOccur: Math.round(maxWaterUsage*0.65*0.8 * 100) / 100, + underWaterInput: Math.round((maxWaterUsage-(maxWaterUsage*0.65*0.8)) * 100) / 100, + }, + { + msrDt: '최소', + realSewageInput: Math.round(minWaterUsage*100) /100, + sewageOccur: Math.round(minWaterUsage*0.65*0.8 * 100) / 100, + underWaterInput: Math.round((minWaterUsage-(minWaterUsage*0.65*0.8)) * 100) / 100, + }, + ...tableData + ] + + set((state: UnderWaterType) => ({ + ...state, + UnderWaterDetail: { + ...state.UnderWaterDetail, + gridListUnderWaterDetail:newData + } + })) + } + } +} \ No newline at end of file diff --git a/apps/fo/app/store/underWaterInput/types.tsx b/apps/fo/app/store/underWaterInput/types.tsx new file mode 100644 index 000000000..c90c654ad --- /dev/null +++ b/apps/fo/app/store/underWaterInput/types.tsx @@ -0,0 +1,58 @@ +export type UnderWaterProps = { + bscFctCd : string, + lclgvrnmntNm : string, + sidoNm : string, + sggNm : string, + bscFctNm : string, + daddr : string, + fctCpc : number, + underWaterInputYear : number, + underWaterInputSummer : number, + underWaterInputWinter : number, + infoRnDay : number, + rgnNm : string, + rvbsn : string, + lgbsn : string, + mdmbsn : string, +} +export interface UnderWaterInterface { + bscFctCd : string, + sidoNm : string, + sggNm : string, + bscFctNm : string, + daddr : string, + fctCpc : number, + underWaterInputYear : number, + underWaterInputSummer : number, + underWaterInputWinter : number, + infoRnDay : number, + rgnNm : string, + rvbsn : string, + lgbsn : string, + mdmbsn : string, +} + +// 모달 +export interface UnderWaterInputModalProps { + msrDt : string, + realSewageInput : number | '', + sewageOccur : number | '', + underWaterInput : number | '', +} + + +export interface ExcelType { + 시설코드: string, + 권역:string|'', + 단위유역:string|'', + 중권역:string|'', + 환경청:string|'', + 시도: string, + 시군구: string, + 시설명: string, + 주소: string, + 시설용량: number, + 연간지하수유입량: number | '', + 하절기지하수유입량: number | '', + 동절기지하수유입량: number | '', +} \ No newline at end of file diff --git a/apps/fo/components.json b/apps/fo/components.json new file mode 100644 index 000000000..33cf79ee5 --- /dev/null +++ b/apps/fo/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "src": "@/src", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/apps/fo/eslint.config.mjs b/apps/fo/eslint.config.mjs new file mode 100644 index 000000000..29e63f653 --- /dev/null +++ b/apps/fo/eslint.config.mjs @@ -0,0 +1,20 @@ +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"), + {rules:{ + // other rules.. + "@typescript-eslint/no-explicit-any": "off", + }}, +]; + +export default eslintConfig; diff --git a/apps/fo/middleware.ts b/apps/fo/middleware.ts new file mode 100644 index 000000000..317c6a706 --- /dev/null +++ b/apps/fo/middleware.ts @@ -0,0 +1,118 @@ +import { NextRequest, NextResponse } from "next/server"; +import { SERVER_API_CS_URL } from "./src/shared/config/env"; + +const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; +const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; +const GEOSERVER_URL = process.env.NEXT_PUBLIC_GEOSERVER_URL || ""; +const LX_GEOSERVER_URL = process.env.NEXT_PUBLIC_LX_GEOSERVER_URL || ""; +const SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ""; + +export function middleware(req: NextRequest) { + console.log("Middleware 진입:", req.nextUrl.pathname); + const pathname = req.nextUrl.pathname; + const searchParams = req.nextUrl.searchParams; + + if (pathname.startsWith("/server")) { + const queryParams = searchParams.toString(); + const newUrl = `${SERVER_API_CS_URL}${pathname.replace("/server", "")}${ + queryParams ? `?${queryParams}` : "" + }`; + console.log(`[Middleware] Rewriting from '/server' path to: ${newUrl}`); + return NextResponse.rewrite(new URL(newUrl)); + } + + if (pathname.startsWith("/geoserver")) { + let queryParams = searchParams.toString(); + const typename = searchParams.get("TYPENAME"); + + let newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; + + if ( + typename == "hasudo:tb_il_swage_prcs_fclt" //하수처리시설 + || typename == "hasudo:tb_ms_swage_prcs_zone" //하수처리구역 + || typename == "hasudo:tb_ms_sbmrg_dngr_map" //침수위험지도 + || typename == "hasudo:tb_ms_flood_dngr_map" //도시침수지도 + ) { + newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; + } + else if ( + typename == "hasudo:tb_ms_swage_ppl" //관로 + || typename == "hasudo:tb_ms_swage_mhl" //맨홀 + || typename == "hasudo:tb_ms_swage_pmstn" //펌프장 + || typename == "hasudo:tb_ms_stofcbr" //우수토실 + ) { + newUrl = `${LX_GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; + } + else if ( + typename == "hasudo:om_dt_untrt_swg_rain" //중점우수토실 + || typename == "hasudo:om_wqlt_station" //관측소 + || typename == "hasudo:tb_wr_bdst_ruse_fclt" //재이용시설 + || typename == "hasudo:tb_wr_rnwtr_utztn_fclt" //빗물이용시설 + || typename == "hasudo:tb_wr_rswtr_fclt" //중수도시설 + || typename == "hasudo:tb_wr_htdfl_fclt" //온배수시설 + || typename == "hasudo:tb_wr_ruse_fclt_dmd_co" //수요처 + || typename == "hasudo:tb_wr_tmpr_dmd_co" //잠재수요처 + ){ + const url = req.nextUrl.clone(); + url.searchParams.set('TYPENAME', typename.replace("hasudo:","")); + queryParams = url.searchParams.toString(); + newUrl = `${SEWERAGE_API_URL}/v1/map/geometry${queryParams ? `?${queryParams}` : ""}`; + } + + console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`); + return NextResponse.rewrite(new URL(newUrl)); + } + + if (pathname.startsWith("/sewerage-service/api/v1/map/geometry")) { + const queryParams = searchParams.toString(); + const newUrl = `${SEWERAGE_API_URL}${pathname.replace("/sewerage-service/api", "")}${ + queryParams ? `?${queryParams}` : "" + }`; + console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`); + return NextResponse.rewrite(new URL(newUrl)); + } + + if (pathname.startsWith("/attach-service") || pathname.startsWith("/cms-service")) { + const requestHeaders = new Headers(req.headers); + const token = req.cookies.get(CLAIM_NAME)?.value; + const userId = req.cookies.get(AUTH_USER_ID)?.value; + const newUrl = `${SERVER_API_CS_URL}${pathname}`; + + if (token) requestHeaders.set(CLAIM_NAME, token); + if (userId) requestHeaders.set(AUTH_USER_ID, userId); + + console.log( + `[Middleware] Rewriting from '/attach-service' path to: ${newUrl}`, + ); + return NextResponse.rewrite(new URL(newUrl), { + headers: requestHeaders, + }); + } + + if (pathname.startsWith("/user-service")) { + const requestHeaders = new Headers(req.headers); + const newUrl = `${SERVER_API_CS_URL}${pathname}`; + + console.log( + `[Middleware] Rewriting from '/user-service' path to: ${newUrl}`, + ); + return NextResponse.rewrite(new URL(newUrl), { + headers: requestHeaders, + }); + } + if (pathname === "/") { + return NextResponse.redirect(new URL("/pages/dashboard", req.url)); + } + return NextResponse.next(); +} + +export const config = { + matcher: [ + "/server/:path*", + "/geoserver/:path*", + "/attach-service/:path*", + "/user-service/:path*", + "/cms-service/:path*", + "/" + ], +}; diff --git a/apps/fo/next.config.ts b/apps/fo/next.config.ts new file mode 100644 index 000000000..35ea029a8 --- /dev/null +++ b/apps/fo/next.config.ts @@ -0,0 +1,34 @@ +import type { NextConfig } from "next"; + +const subDomain = + process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13001"; + +const nextConfig: NextConfig = { + output: "standalone", + async rewrites() { + return { + beforeFiles: [ + { + source: "/_next/static/:path*", + destination: `${subDomain}/_next/static/:path*`, + has: [ + { + type: "header", + key: "referer", + value: "(.*)/pst(.*)", + }, + ], + }, + ], + afterFiles: [ + { + source: "/pst/:path*", + destination: `${subDomain}/pst/:path*`, + }, + ], + fallback: [], + }; + }, +}; + +export default nextConfig; diff --git a/apps/fo/package.json b/apps/fo/package.json new file mode 100644 index 000000000..aedf37c33 --- /dev/null +++ b/apps/fo/package.json @@ -0,0 +1,30 @@ +{ + "name": "fo", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@common/business_components": "workspace:*", + "@common/components": "workspace:*", + "@tailwindcss/vite": "^4.0.10", + "html2canvas": "^1.4.1", + "js-cookie": "^3.0.5", + "next": "15.1.7", + "select": "^1.1.2", + "webpack": "^5.98.0", + "zustand": "^5.0.3" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "eslint": "^9", + "eslint-config-next": "15.1.7", + "postcss": "^8", + "tailwindcss": "^3.4.17", + "typescript": "^5.6.3" + } +} diff --git a/apps/fo/postcss.config.mjs b/apps/fo/postcss.config.mjs new file mode 100644 index 000000000..1a69fd2a4 --- /dev/null +++ b/apps/fo/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/apps/fo/public/file.svg b/apps/fo/public/file.svg new file mode 100644 index 000000000..004145cdd --- /dev/null +++ b/apps/fo/public/file.svg @@ -0,0 +1 @@ +<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg> \ No newline at end of file diff --git a/apps/fo/public/globe.svg b/apps/fo/public/globe.svg new file mode 100644 index 000000000..567f17b0d --- /dev/null +++ b/apps/fo/public/globe.svg @@ -0,0 +1 @@ +<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg> \ No newline at end of file diff --git a/apps/fo/public/images/map/layericon/BML_CBAS_PS.png b/apps/fo/public/images/map/layericon/BML_CBAS_PS.png new file mode 100644 index 000000000..5ff4add3e Binary files /dev/null and b/apps/fo/public/images/map/layericon/BML_CBAS_PS.png differ diff --git a/apps/fo/public/images/map/layericon/BML_ELEV_PS.png b/apps/fo/public/images/map/layericon/BML_ELEV_PS.png new file mode 100644 index 000000000..83970a331 Binary files /dev/null and b/apps/fo/public/images/map/layericon/BML_ELEV_PS.png differ diff --git a/apps/fo/public/images/map/layericon/BML_ETCP_PS.png b/apps/fo/public/images/map/layericon/BML_ETCP_PS.png new file mode 100644 index 000000000..df9e4957b Binary files /dev/null and b/apps/fo/public/images/map/layericon/BML_ETCP_PS.png differ diff --git a/apps/fo/public/images/map/layericon/BML_STMS_PS.png b/apps/fo/public/images/map/layericon/BML_STMS_PS.png new file mode 100644 index 000000000..423f2e8fc Binary files /dev/null and b/apps/fo/public/images/map/layericon/BML_STMS_PS.png differ diff --git a/apps/fo/public/images/map/layericon/BML_SYMB_PS.png b/apps/fo/public/images/map/layericon/BML_SYMB_PS.png new file mode 100644 index 000000000..497589bea Binary files /dev/null and b/apps/fo/public/images/map/layericon/BML_SYMB_PS.png differ diff --git a/apps/fo/public/images/map/layericon/RDL_BYST_PS.png b/apps/fo/public/images/map/layericon/RDL_BYST_PS.png new file mode 100644 index 000000000..1e78cba68 Binary files /dev/null and b/apps/fo/public/images/map/layericon/RDL_BYST_PS.png differ diff --git a/apps/fo/public/images/map/layericon/RDL_CROS_PS.png b/apps/fo/public/images/map/layericon/RDL_CROS_PS.png new file mode 100644 index 000000000..57b5f2ca5 Binary files /dev/null and b/apps/fo/public/images/map/layericon/RDL_CROS_PS.png differ diff --git a/apps/fo/public/images/map/layericon/RDL_ETCT_PS.png b/apps/fo/public/images/map/layericon/RDL_ETCT_PS.png new file mode 100644 index 000000000..29c0b77b4 Binary files /dev/null and b/apps/fo/public/images/map/layericon/RDL_ETCT_PS.png differ diff --git a/apps/fo/public/images/map/layericon/RDL_OCUP_PS.png b/apps/fo/public/images/map/layericon/RDL_OCUP_PS.png new file mode 100644 index 000000000..8a1ac0074 Binary files /dev/null and b/apps/fo/public/images/map/layericon/RDL_OCUP_PS.png differ diff --git a/apps/fo/public/images/map/layericon/RDL_RDSN_PS.png b/apps/fo/public/images/map/layericon/RDL_RDSN_PS.png new file mode 100644 index 000000000..5629de9fb Binary files /dev/null and b/apps/fo/public/images/map/layericon/RDL_RDSN_PS.png differ diff --git a/apps/fo/public/images/map/layericon/RDL_SCLT_PS.png b/apps/fo/public/images/map/layericon/RDL_SCLT_PS.png new file mode 100644 index 000000000..7281dc61d Binary files /dev/null and b/apps/fo/public/images/map/layericon/RDL_SCLT_PS.png differ diff --git a/apps/fo/public/images/map/layericon/RDL_SEPC_PS.png b/apps/fo/public/images/map/layericon/RDL_SEPC_PS.png new file mode 100644 index 000000000..087b4e941 Binary files /dev/null and b/apps/fo/public/images/map/layericon/RDL_SEPC_PS.png differ diff --git a/apps/fo/public/images/map/layericon/RDL_STAT_PS.png b/apps/fo/public/images/map/layericon/RDL_STAT_PS.png new file mode 100644 index 000000000..13d8ea689 Binary files /dev/null and b/apps/fo/public/images/map/layericon/RDL_STAT_PS.png differ diff --git a/apps/fo/public/images/map/layericon/RDL_STLT_PS.png b/apps/fo/public/images/map/layericon/RDL_STLT_PS.png new file mode 100644 index 000000000..489524b5c Binary files /dev/null and b/apps/fo/public/images/map/layericon/RDL_STLT_PS.png differ diff --git a/apps/fo/public/images/map/layericon/RDL_TFSN_PS.png b/apps/fo/public/images/map/layericon/RDL_TFSN_PS.png new file mode 100644 index 000000000..6dfff6696 Binary files /dev/null and b/apps/fo/public/images/map/layericon/RDL_TFSN_PS.png differ diff --git a/apps/fo/public/images/map/layericon/RDL_TRCT_PS.png b/apps/fo/public/images/map/layericon/RDL_TRCT_PS.png new file mode 100644 index 000000000..ed1a72d8a Binary files /dev/null and b/apps/fo/public/images/map/layericon/RDL_TRCT_PS.png differ diff --git a/apps/fo/public/images/map/layericon/RDL_TREE_PS.png b/apps/fo/public/images/map/layericon/RDL_TREE_PS.png new file mode 100644 index 000000000..0ee84dedc Binary files /dev/null and b/apps/fo/public/images/map/layericon/RDL_TREE_PS.png differ diff --git a/apps/fo/public/images/map/layericon/RDL_TRSN_PS.png b/apps/fo/public/images/map/layericon/RDL_TRSN_PS.png new file mode 100644 index 000000000..15675cb31 Binary files /dev/null and b/apps/fo/public/images/map/layericon/RDL_TRSN_PS.png differ diff --git a/apps/fo/public/images/map/layericon/RDL_TSCT_PS.png b/apps/fo/public/images/map/layericon/RDL_TSCT_PS.png new file mode 100644 index 000000000..ffc225159 Binary files /dev/null and b/apps/fo/public/images/map/layericon/RDL_TSCT_PS.png differ diff --git a/apps/fo/public/images/map/layericon/SWL_CLAY_PS.png b/apps/fo/public/images/map/layericon/SWL_CLAY_PS.png new file mode 100644 index 000000000..e603ffa9c Binary files /dev/null and b/apps/fo/public/images/map/layericon/SWL_CLAY_PS.png differ diff --git a/apps/fo/public/images/map/layericon/SWL_CLAY_PS_RED.png b/apps/fo/public/images/map/layericon/SWL_CLAY_PS_RED.png new file mode 100644 index 000000000..0e5450688 Binary files /dev/null and b/apps/fo/public/images/map/layericon/SWL_CLAY_PS_RED.png differ diff --git a/apps/fo/public/images/map/layericon/SWL_DEPT_PS.png b/apps/fo/public/images/map/layericon/SWL_DEPT_PS.png new file mode 100644 index 000000000..fede6c304 Binary files /dev/null and b/apps/fo/public/images/map/layericon/SWL_DEPT_PS.png differ diff --git a/apps/fo/public/images/map/layericon/SWL_MANH_PS.png b/apps/fo/public/images/map/layericon/SWL_MANH_PS.png new file mode 100644 index 000000000..75217f494 Binary files /dev/null and b/apps/fo/public/images/map/layericon/SWL_MANH_PS.png differ diff --git a/apps/fo/public/images/map/layericon/SWL_MANH_PS_BLACK.png b/apps/fo/public/images/map/layericon/SWL_MANH_PS_BLACK.png new file mode 100644 index 000000000..5d2e8a70a Binary files /dev/null and b/apps/fo/public/images/map/layericon/SWL_MANH_PS_BLACK.png differ diff --git a/apps/fo/public/images/map/layericon/SWL_MANH_PS_BLUE.png b/apps/fo/public/images/map/layericon/SWL_MANH_PS_BLUE.png new file mode 100644 index 000000000..e521cb84a Binary files /dev/null and b/apps/fo/public/images/map/layericon/SWL_MANH_PS_BLUE.png differ diff --git a/apps/fo/public/images/map/layericon/SWL_MANH_PS_GRAY.png b/apps/fo/public/images/map/layericon/SWL_MANH_PS_GRAY.png new file mode 100644 index 000000000..e19f3d8b3 Binary files /dev/null and b/apps/fo/public/images/map/layericon/SWL_MANH_PS_GRAY.png differ diff --git a/apps/fo/public/images/map/layericon/SWL_MANH_PS_GREEN.png b/apps/fo/public/images/map/layericon/SWL_MANH_PS_GREEN.png new file mode 100644 index 000000000..4503c13c6 Binary files /dev/null and b/apps/fo/public/images/map/layericon/SWL_MANH_PS_GREEN.png differ diff --git a/apps/fo/public/images/map/layericon/SWL_MANH_PS_RED.png b/apps/fo/public/images/map/layericon/SWL_MANH_PS_RED.png new file mode 100644 index 000000000..b8fa45f9c Binary files /dev/null and b/apps/fo/public/images/map/layericon/SWL_MANH_PS_RED.png differ diff --git a/apps/fo/public/images/map/layericon/SWL_MANH_PS_YELLOW.png b/apps/fo/public/images/map/layericon/SWL_MANH_PS_YELLOW.png new file mode 100644 index 000000000..b7de1fbfd Binary files /dev/null and b/apps/fo/public/images/map/layericon/SWL_MANH_PS_YELLOW.png differ diff --git a/apps/fo/public/images/map/layericon/SWL_SPEW_PS.png b/apps/fo/public/images/map/layericon/SWL_SPEW_PS.png new file mode 100644 index 000000000..d260ea3a6 Binary files /dev/null and b/apps/fo/public/images/map/layericon/SWL_SPEW_PS.png differ diff --git a/apps/fo/public/images/map/layericon/SWL_SPOT_PS.png b/apps/fo/public/images/map/layericon/SWL_SPOT_PS.png new file mode 100644 index 000000000..84c7992e4 Binary files /dev/null and b/apps/fo/public/images/map/layericon/SWL_SPOT_PS.png differ diff --git a/apps/fo/public/images/map/layericon/SWL_SPOT_PS_BLACK.png b/apps/fo/public/images/map/layericon/SWL_SPOT_PS_BLACK.png new file mode 100644 index 000000000..fe67f8f98 Binary files /dev/null and b/apps/fo/public/images/map/layericon/SWL_SPOT_PS_BLACK.png differ diff --git a/apps/fo/public/images/map/layericon/SWL_SPOT_PS_BLUE.png b/apps/fo/public/images/map/layericon/SWL_SPOT_PS_BLUE.png new file mode 100644 index 000000000..691f2f45e Binary files /dev/null and b/apps/fo/public/images/map/layericon/SWL_SPOT_PS_BLUE.png differ diff --git a/apps/fo/public/images/map/layericon/SWL_SPOT_PS_GREEN.png b/apps/fo/public/images/map/layericon/SWL_SPOT_PS_GREEN.png new file mode 100644 index 000000000..d09c4bd97 Binary files /dev/null and b/apps/fo/public/images/map/layericon/SWL_SPOT_PS_GREEN.png differ diff --git a/apps/fo/public/images/map/layericon/SWL_SPOT_PS_GREY.png b/apps/fo/public/images/map/layericon/SWL_SPOT_PS_GREY.png new file mode 100644 index 000000000..32d0bf519 Binary files /dev/null and b/apps/fo/public/images/map/layericon/SWL_SPOT_PS_GREY.png differ diff --git a/apps/fo/public/images/map/layericon/SWL_SPOT_PS_RED.png b/apps/fo/public/images/map/layericon/SWL_SPOT_PS_RED.png new file mode 100644 index 000000000..a2da36f9b Binary files /dev/null and b/apps/fo/public/images/map/layericon/SWL_SPOT_PS_RED.png differ diff --git a/apps/fo/public/images/map/layericon/SWL_VENT_PS.png b/apps/fo/public/images/map/layericon/SWL_VENT_PS.png new file mode 100644 index 000000000..834eeec21 Binary files /dev/null and b/apps/fo/public/images/map/layericon/SWL_VENT_PS.png differ diff --git a/apps/fo/public/images/map/layericon/TL_SPBD_ENTRC.png b/apps/fo/public/images/map/layericon/TL_SPBD_ENTRC.png new file mode 100644 index 000000000..8e7de8593 Binary files /dev/null and b/apps/fo/public/images/map/layericon/TL_SPBD_ENTRC.png differ diff --git a/apps/fo/public/images/map/layericon/TL_SPGF_LOC.png b/apps/fo/public/images/map/layericon/TL_SPGF_LOC.png new file mode 100644 index 000000000..74b0adac3 Binary files /dev/null and b/apps/fo/public/images/map/layericon/TL_SPGF_LOC.png differ diff --git a/apps/fo/public/images/map/layericon/TL_SPOT_NOIN.png b/apps/fo/public/images/map/layericon/TL_SPOT_NOIN.png new file mode 100644 index 000000000..4800beaeb Binary files /dev/null and b/apps/fo/public/images/map/layericon/TL_SPOT_NOIN.png differ diff --git a/apps/fo/public/images/map/layericon/TL_SPRD_BANO.png b/apps/fo/public/images/map/layericon/TL_SPRD_BANO.png new file mode 100644 index 000000000..8d08dca29 Binary files /dev/null and b/apps/fo/public/images/map/layericon/TL_SPRD_BANO.png differ diff --git a/apps/fo/public/images/map/layericon/TL_SPRD_CRSRD.png b/apps/fo/public/images/map/layericon/TL_SPRD_CRSRD.png new file mode 100644 index 000000000..57b5f2ca5 Binary files /dev/null and b/apps/fo/public/images/map/layericon/TL_SPRD_CRSRD.png differ diff --git a/apps/fo/public/images/map/layericon/TL_SPRD_NONM.png b/apps/fo/public/images/map/layericon/TL_SPRD_NONM.png new file mode 100644 index 000000000..e68c05ab6 Binary files /dev/null and b/apps/fo/public/images/map/layericon/TL_SPRD_NONM.png differ diff --git a/apps/fo/public/images/map/layericon/TL_SPRD_RENM.png b/apps/fo/public/images/map/layericon/TL_SPRD_RENM.png new file mode 100644 index 000000000..4800beaeb Binary files /dev/null and b/apps/fo/public/images/map/layericon/TL_SPRD_RENM.png differ diff --git a/apps/fo/public/images/map/layericon/TL_SPRD_RWNM.png b/apps/fo/public/images/map/layericon/TL_SPRD_RWNM.png new file mode 100644 index 000000000..e68c05ab6 Binary files /dev/null and b/apps/fo/public/images/map/layericon/TL_SPRD_RWNM.png differ diff --git a/apps/fo/public/images/map/layericon/UFL_BMAN_PS.png b/apps/fo/public/images/map/layericon/UFL_BMAN_PS.png new file mode 100644 index 000000000..f2f64a641 Binary files /dev/null and b/apps/fo/public/images/map/layericon/UFL_BMAN_PS.png differ diff --git a/apps/fo/public/images/map/layericon/UFL_BOPC_PS.png b/apps/fo/public/images/map/layericon/UFL_BOPC_PS.png new file mode 100644 index 000000000..0f8e99f45 Binary files /dev/null and b/apps/fo/public/images/map/layericon/UFL_BOPC_PS.png differ diff --git a/apps/fo/public/images/map/layericon/UFL_BPIP_PS.png b/apps/fo/public/images/map/layericon/UFL_BPIP_PS.png new file mode 100644 index 000000000..dcc3622a7 Binary files /dev/null and b/apps/fo/public/images/map/layericon/UFL_BPIP_PS.png differ diff --git a/apps/fo/public/images/map/layericon/UFL_BPOL_PS.png b/apps/fo/public/images/map/layericon/UFL_BPOL_PS.png new file mode 100644 index 000000000..4f1102989 Binary files /dev/null and b/apps/fo/public/images/map/layericon/UFL_BPOL_PS.png differ diff --git a/apps/fo/public/images/map/layericon/UFL_BTRS_PS.png b/apps/fo/public/images/map/layericon/UFL_BTRS_PS.png new file mode 100644 index 000000000..557c17089 Binary files /dev/null and b/apps/fo/public/images/map/layericon/UFL_BTRS_PS.png differ diff --git a/apps/fo/public/images/map/layericon/UFL_GMNT_PS.png b/apps/fo/public/images/map/layericon/UFL_GMNT_PS.png new file mode 100644 index 000000000..6081d8b00 Binary files /dev/null and b/apps/fo/public/images/map/layericon/UFL_GMNT_PS.png differ diff --git a/apps/fo/public/images/map/layericon/UFL_GPIP_PS.png b/apps/fo/public/images/map/layericon/UFL_GPIP_PS.png new file mode 100644 index 000000000..5d7e86fd2 Binary files /dev/null and b/apps/fo/public/images/map/layericon/UFL_GPIP_PS.png differ diff --git a/apps/fo/public/images/map/layericon/UFL_GPRS_PS.png b/apps/fo/public/images/map/layericon/UFL_GPRS_PS.png new file mode 100644 index 000000000..79c120f20 Binary files /dev/null and b/apps/fo/public/images/map/layericon/UFL_GPRS_PS.png differ diff --git a/apps/fo/public/images/map/layericon/UFL_GVAL_PS.png b/apps/fo/public/images/map/layericon/UFL_GVAL_PS.png new file mode 100644 index 000000000..e8f7cde2e Binary files /dev/null and b/apps/fo/public/images/map/layericon/UFL_GVAL_PS.png differ diff --git a/apps/fo/public/images/map/layericon/UFL_KMAN_PS.png b/apps/fo/public/images/map/layericon/UFL_KMAN_PS.png new file mode 100644 index 000000000..764ce8ba5 Binary files /dev/null and b/apps/fo/public/images/map/layericon/UFL_KMAN_PS.png differ diff --git a/apps/fo/public/images/map/layericon/UFL_KPOL_PS.png b/apps/fo/public/images/map/layericon/UFL_KPOL_PS.png new file mode 100644 index 000000000..d9fa712cf Binary files /dev/null and b/apps/fo/public/images/map/layericon/UFL_KPOL_PS.png differ diff --git a/apps/fo/public/images/map/layericon/UFL_VALV_PS.png b/apps/fo/public/images/map/layericon/UFL_VALV_PS.png new file mode 100644 index 000000000..1b5d70178 Binary files /dev/null and b/apps/fo/public/images/map/layericon/UFL_VALV_PS.png differ diff --git a/apps/fo/public/images/map/layericon/WTL_FIRE_PS.png b/apps/fo/public/images/map/layericon/WTL_FIRE_PS.png new file mode 100644 index 000000000..4137a3f74 Binary files /dev/null and b/apps/fo/public/images/map/layericon/WTL_FIRE_PS.png differ diff --git a/apps/fo/public/images/map/layericon/WTL_FLOW_PS.png b/apps/fo/public/images/map/layericon/WTL_FLOW_PS.png new file mode 100644 index 000000000..285f7edfa Binary files /dev/null and b/apps/fo/public/images/map/layericon/WTL_FLOW_PS.png differ diff --git a/apps/fo/public/images/map/layericon/WTL_MANH_PS.png b/apps/fo/public/images/map/layericon/WTL_MANH_PS.png new file mode 100644 index 000000000..5f2a57cfe Binary files /dev/null and b/apps/fo/public/images/map/layericon/WTL_MANH_PS.png differ diff --git a/apps/fo/public/images/map/layericon/WTL_META_PS.png b/apps/fo/public/images/map/layericon/WTL_META_PS.png new file mode 100644 index 000000000..22c7af31b Binary files /dev/null and b/apps/fo/public/images/map/layericon/WTL_META_PS.png differ diff --git a/apps/fo/public/images/map/layericon/WTL_PIPE_PS.png b/apps/fo/public/images/map/layericon/WTL_PIPE_PS.png new file mode 100644 index 000000000..c9c6863f6 Binary files /dev/null and b/apps/fo/public/images/map/layericon/WTL_PIPE_PS.png differ diff --git a/apps/fo/public/images/map/layericon/WTL_RSRV_PS.png b/apps/fo/public/images/map/layericon/WTL_RSRV_PS.png new file mode 100644 index 000000000..d7b307a61 Binary files /dev/null and b/apps/fo/public/images/map/layericon/WTL_RSRV_PS.png differ diff --git a/apps/fo/public/images/map/layericon/WTL_RSRV_PS_GRAY.png b/apps/fo/public/images/map/layericon/WTL_RSRV_PS_GRAY.png new file mode 100644 index 000000000..33a667a1e Binary files /dev/null and b/apps/fo/public/images/map/layericon/WTL_RSRV_PS_GRAY.png differ diff --git a/apps/fo/public/images/map/layericon/WTL_VALV_PS.png b/apps/fo/public/images/map/layericon/WTL_VALV_PS.png new file mode 100644 index 000000000..ca2b4788b Binary files /dev/null and b/apps/fo/public/images/map/layericon/WTL_VALV_PS.png differ diff --git a/apps/fo/public/images/map/layericon/default.png b/apps/fo/public/images/map/layericon/default.png new file mode 100644 index 000000000..92bf1f587 Binary files /dev/null and b/apps/fo/public/images/map/layericon/default.png differ diff --git a/apps/fo/public/images/map/layericon/reset.png b/apps/fo/public/images/map/layericon/reset.png new file mode 100644 index 000000000..45f889206 Binary files /dev/null and b/apps/fo/public/images/map/layericon/reset.png differ diff --git a/apps/fo/public/images/map/toolbar/arrow_down.png b/apps/fo/public/images/map/toolbar/arrow_down.png new file mode 100644 index 000000000..88590ecaf Binary files /dev/null and b/apps/fo/public/images/map/toolbar/arrow_down.png differ diff --git a/apps/fo/public/images/map/toolbar/arrow_right.png b/apps/fo/public/images/map/toolbar/arrow_right.png new file mode 100644 index 000000000..7090c37dd Binary files /dev/null and b/apps/fo/public/images/map/toolbar/arrow_right.png differ diff --git a/apps/fo/public/images/map/toolbar/backward_icon.png b/apps/fo/public/images/map/toolbar/backward_icon.png new file mode 100644 index 000000000..3a853475e Binary files /dev/null and b/apps/fo/public/images/map/toolbar/backward_icon.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_cont_area.png b/apps/fo/public/images/map/toolbar/btn_cont_area.png new file mode 100644 index 000000000..044562b55 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_cont_area.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_cont_area_on.png b/apps/fo/public/images/map/toolbar/btn_cont_area_on.png new file mode 100644 index 000000000..0129ff1bd Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_cont_area_on.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_cont_next.png b/apps/fo/public/images/map/toolbar/btn_cont_next.png new file mode 100644 index 000000000..075111dcf Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_cont_next.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_cont_next_on.png b/apps/fo/public/images/map/toolbar/btn_cont_next_on.png new file mode 100644 index 000000000..4fdbe3a00 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_cont_next_on.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_cont_prev.png b/apps/fo/public/images/map/toolbar/btn_cont_prev.png new file mode 100644 index 000000000..7e3fae7d2 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_cont_prev.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_cont_prev_on.png b/apps/fo/public/images/map/toolbar/btn_cont_prev_on.png new file mode 100644 index 000000000..fb2d55cfd Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_cont_prev_on.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_cont_print.png b/apps/fo/public/images/map/toolbar/btn_cont_print.png new file mode 100644 index 000000000..82596b83d Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_cont_print.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_cont_print_on.png b/apps/fo/public/images/map/toolbar/btn_cont_print_on.png new file mode 100644 index 000000000..bb7f27334 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_cont_print_on.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_cont_ruler.png b/apps/fo/public/images/map/toolbar/btn_cont_ruler.png new file mode 100644 index 000000000..501e460a4 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_cont_ruler.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_cont_ruler_on.png b/apps/fo/public/images/map/toolbar/btn_cont_ruler_on.png new file mode 100644 index 000000000..676a81bcc Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_cont_ruler_on.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_cont_save.png b/apps/fo/public/images/map/toolbar/btn_cont_save.png new file mode 100644 index 000000000..c057b9377 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_cont_save.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_cont_save_on.png b/apps/fo/public/images/map/toolbar/btn_cont_save_on.png new file mode 100644 index 000000000..bf8e6d623 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_cont_save_on.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_cont_sketch.png b/apps/fo/public/images/map/toolbar/btn_cont_sketch.png new file mode 100644 index 000000000..38a5f507f Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_cont_sketch.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_cont_sketch_on.png b/apps/fo/public/images/map/toolbar/btn_cont_sketch_on.png new file mode 100644 index 000000000..79fb54e03 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_cont_sketch_on.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_cont_zoomin.png b/apps/fo/public/images/map/toolbar/btn_cont_zoomin.png new file mode 100644 index 000000000..ac32fbab0 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_cont_zoomin.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_cont_zoomin_on.png b/apps/fo/public/images/map/toolbar/btn_cont_zoomin_on.png new file mode 100644 index 000000000..f258a965f Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_cont_zoomin_on.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_cont_zoomout.png b/apps/fo/public/images/map/toolbar/btn_cont_zoomout.png new file mode 100644 index 000000000..2ee0b5161 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_cont_zoomout.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_cont_zoomout_on.png b/apps/fo/public/images/map/toolbar/btn_cont_zoomout_on.png new file mode 100644 index 000000000..58dcbfdae Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_cont_zoomout_on.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_edit_add.png b/apps/fo/public/images/map/toolbar/btn_edit_add.png new file mode 100644 index 000000000..fe4f0993f Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_edit_add.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_edit_add_on.png b/apps/fo/public/images/map/toolbar/btn_edit_add_on.png new file mode 100644 index 000000000..37a836298 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_edit_add_on.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_edit_del.png b/apps/fo/public/images/map/toolbar/btn_edit_del.png new file mode 100644 index 000000000..3bb372ab9 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_edit_del.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_edit_del_on.png b/apps/fo/public/images/map/toolbar/btn_edit_del_on.png new file mode 100644 index 000000000..89e558739 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_edit_del_on.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_edit_next.png b/apps/fo/public/images/map/toolbar/btn_edit_next.png new file mode 100644 index 000000000..df4913c90 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_edit_next.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_edit_next_on.png b/apps/fo/public/images/map/toolbar/btn_edit_next_on.png new file mode 100644 index 000000000..4e5540b55 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_edit_next_on.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_edit_prev.png b/apps/fo/public/images/map/toolbar/btn_edit_prev.png new file mode 100644 index 000000000..e94bf6d42 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_edit_prev.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_edit_prev_on.png b/apps/fo/public/images/map/toolbar/btn_edit_prev_on.png new file mode 100644 index 000000000..8645dedfa Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_edit_prev_on.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_edit_save.png b/apps/fo/public/images/map/toolbar/btn_edit_save.png new file mode 100644 index 000000000..5f01dd55c Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_edit_save.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_edit_save_on.png b/apps/fo/public/images/map/toolbar/btn_edit_save_on.png new file mode 100644 index 000000000..b035687d6 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_edit_save_on.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_edit_slt.png b/apps/fo/public/images/map/toolbar/btn_edit_slt.png new file mode 100644 index 000000000..aaf2bbd08 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_edit_slt.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_edit_slt_on.png b/apps/fo/public/images/map/toolbar/btn_edit_slt_on.png new file mode 100644 index 000000000..02d035a74 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_edit_slt_on.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_edit_snap.png b/apps/fo/public/images/map/toolbar/btn_edit_snap.png new file mode 100644 index 000000000..fc91d4304 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_edit_snap.png differ diff --git a/apps/fo/public/images/map/toolbar/btn_edit_snap_on.png b/apps/fo/public/images/map/toolbar/btn_edit_snap_on.png new file mode 100644 index 000000000..f856d8fbe Binary files /dev/null and b/apps/fo/public/images/map/toolbar/btn_edit_snap_on.png differ diff --git a/apps/fo/public/images/map/toolbar/close_icon.png b/apps/fo/public/images/map/toolbar/close_icon.png new file mode 100644 index 000000000..e8e76d5d7 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/close_icon.png differ diff --git a/apps/fo/public/images/map/toolbar/ico_gis_sprite.png b/apps/fo/public/images/map/toolbar/ico_gis_sprite.png new file mode 100644 index 000000000..0ee4b5985 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/ico_gis_sprite.png differ diff --git a/apps/fo/public/images/map/toolbar/icon-viewclose.png b/apps/fo/public/images/map/toolbar/icon-viewclose.png new file mode 100644 index 000000000..d93ce9c05 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/icon-viewclose.png differ diff --git a/apps/fo/public/images/map/toolbar/img_base_map.png b/apps/fo/public/images/map/toolbar/img_base_map.png new file mode 100644 index 000000000..7f96fcf5e Binary files /dev/null and b/apps/fo/public/images/map/toolbar/img_base_map.png differ diff --git a/apps/fo/public/images/map/toolbar/img_network_map.png b/apps/fo/public/images/map/toolbar/img_network_map.png new file mode 100644 index 000000000..df80af6dc Binary files /dev/null and b/apps/fo/public/images/map/toolbar/img_network_map.png differ diff --git a/apps/fo/public/images/map/toolbar/img_road_map.png b/apps/fo/public/images/map/toolbar/img_road_map.png new file mode 100644 index 000000000..02d015d12 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/img_road_map.png differ diff --git a/apps/fo/public/images/map/toolbar/img_settle_map.png b/apps/fo/public/images/map/toolbar/img_settle_map.png new file mode 100644 index 000000000..1a93f50e8 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/img_settle_map.png differ diff --git a/apps/fo/public/images/map/toolbar/loading.gif b/apps/fo/public/images/map/toolbar/loading.gif new file mode 100644 index 000000000..7c3683bb4 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/loading.gif differ diff --git a/apps/fo/public/images/map/toolbar/map_pin.png b/apps/fo/public/images/map/toolbar/map_pin.png new file mode 100644 index 000000000..b6cc2f6aa Binary files /dev/null and b/apps/fo/public/images/map/toolbar/map_pin.png differ diff --git a/apps/fo/public/images/map/toolbar/toggle_off.png b/apps/fo/public/images/map/toolbar/toggle_off.png new file mode 100644 index 000000000..d5dea9f67 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/toggle_off.png differ diff --git a/apps/fo/public/images/map/toolbar/toggle_on.png b/apps/fo/public/images/map/toolbar/toggle_on.png new file mode 100644 index 000000000..9cb573875 Binary files /dev/null and b/apps/fo/public/images/map/toolbar/toggle_on.png differ diff --git a/apps/fo/public/mainDashBoard.xlsx b/apps/fo/public/mainDashBoard.xlsx new file mode 100644 index 000000000..9557d13d2 Binary files /dev/null and b/apps/fo/public/mainDashBoard.xlsx differ diff --git a/apps/fo/public/next.svg b/apps/fo/public/next.svg new file mode 100644 index 000000000..5174b28c5 --- /dev/null +++ b/apps/fo/public/next.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg> \ No newline at end of file diff --git a/apps/fo/public/vercel.svg b/apps/fo/public/vercel.svg new file mode 100644 index 000000000..770539603 --- /dev/null +++ b/apps/fo/public/vercel.svg @@ -0,0 +1 @@ +<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg> \ No newline at end of file diff --git a/apps/fo/public/window.svg b/apps/fo/public/window.svg new file mode 100644 index 000000000..b2b2a44f6 --- /dev/null +++ b/apps/fo/public/window.svg @@ -0,0 +1 @@ +<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg> \ No newline at end of file diff --git a/apps/fo/src/entities/auth/api/login-service.ts b/apps/fo/src/entities/auth/api/login-service.ts new file mode 100644 index 000000000..fafb467e7 --- /dev/null +++ b/apps/fo/src/entities/auth/api/login-service.ts @@ -0,0 +1,81 @@ + +import type { LoginService, ILogin } from "../model/login-interface"; +import { tokenManager } from "../lib/token-manager"; +import {PROXY_HOST, ACCESS_TOKEN, AUTH_USER_ID,SITE_ID, CLAIM_NAME, ENDPOINT, JWT_EXPIRATION_TIME } from "../../../shared/config"; + +export class LoginApiService implements LoginService { + private static instance: LoginApiService; + + private constructor() {} + + public static getInstance(): LoginApiService { + if (!LoginApiService.instance) { + LoginApiService.instance = new LoginApiService(); + } + return LoginApiService.instance; + } + + public async login(payload: ILogin): Promise<void> { + try { + console.log(' ENDPOINT.USER_SERVICE.LOGIN ', ENDPOINT.USER_SERVICE.LOGIN) + console.log(payload.userId+' '+payload.password) + const response = await fetch( PROXY_HOST+ ENDPOINT.USER_SERVICE.LOGIN, { + method: "POST", + body: JSON.stringify({ + userId: payload.userId, + password: payload.password, + }), + }); + // const data = response.headers.get(ACCESS_TOKEN); + const accessToken = response.headers.get(ACCESS_TOKEN); + const tokenId = response.headers.get(AUTH_USER_ID); + + console.log('accessToken ', accessToken) + console.log('tokenId ', tokenId) + + if (!accessToken || !tokenId) { + throw new Error("Missing required authentication tokens"); + } + + tokenManager.setToken(CLAIM_NAME, accessToken); + tokenManager.setToken(AUTH_USER_ID, tokenId); + tokenManager.setToken("site-id-key", SITE_ID); + tokenManager.setupRefreshTimer( + () => this.silentRefresh(), + JWT_EXPIRATION_TIME, + ); + } catch (error) { + + console.log(' login err ' , error) + throw new Error(`Login failed: ${error}`); + } + } + + + public async silentRefresh(): Promise<void> { + console.log(' silentRefresh ') + + try { + const response = await fetch(PROXY_HOST+ + `${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`, + { method: "PUT" }, + ); + + const data = await response.json(); + const accessToken = data[ACCESS_TOKEN]; + const tokenId = data[AUTH_USER_ID]; + + if (!accessToken || !tokenId) { + throw new Error("Missing required authentication tokens"); + } + + tokenManager.setToken(CLAIM_NAME, accessToken); + tokenManager.setToken(AUTH_USER_ID, tokenId); + } catch (error) { + tokenManager.clearAuthToken(); + throw new Error(`Silent refresh failed: ${error}`); + } + } +} + +export const LoginApi = LoginApiService.getInstance(); diff --git a/apps/fo/src/entities/auth/index.ts b/apps/fo/src/entities/auth/index.ts new file mode 100644 index 000000000..9dc9dd368 --- /dev/null +++ b/apps/fo/src/entities/auth/index.ts @@ -0,0 +1,2 @@ +export { LoginApi } from "./api/login-service"; +//export { AuthWrapper } from "./lib/auth-wrapper"; diff --git a/apps/fo/src/entities/auth/lib/auth-wrapper.tsx b/apps/fo/src/entities/auth/lib/auth-wrapper.tsx new file mode 100644 index 000000000..678ff50d5 --- /dev/null +++ b/apps/fo/src/entities/auth/lib/auth-wrapper.tsx @@ -0,0 +1,45 @@ +// "use client"; + +// import { useEffect } from "react"; +// import { useRouter } from "next/navigation"; +// import { LoginApi } from "../api/login-service"; + +// const AuthWrapper = ({ children }: { children: React.ReactNode }) => { +// const router = useRouter(); +// const { userInfo, setUserInfo } = useUserStore((state) => state); + +// async function authCheck() { +// const userId = getCookie(AUTH_USER_ID); +// if (!userId) { +// throw new Error("Missing required authentication tokens"); +// } + +// const response = await client.get<IUserInfo>( +// `${ENDPOINT.USER_SERVICE.USERS}/info/me`, +// ); +// if (!userInfo) setUserInfo(response.data); +// } + +// useEffect(() => { +// (async () => { +// try { +// await authCheck(); +// } catch (error) { +// console.error(error); + +// try { +// await LoginApi.silentRefresh(); +// await authCheck(); +// } catch (refreshError) { +// console.error("Token refresh failed:", refreshError); +// setUserInfo(null); +// router.push("/auth/login"); +// } +// } +// })(); +// }, []); + +// return <>{children}</>; +// }; + +// export { AuthWrapper }; diff --git a/apps/fo/src/entities/auth/lib/token-manager.ts b/apps/fo/src/entities/auth/lib/token-manager.ts new file mode 100644 index 000000000..e86dba5f2 --- /dev/null +++ b/apps/fo/src/entities/auth/lib/token-manager.ts @@ -0,0 +1,59 @@ +import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config"; +import { getCookie, removeCookie, setCookie } from "@/src/shared/lib/cookie"; + +export class TokenManager { + private static instance: TokenManager; + private refreshTimer: NodeJS.Timeout | null = null; + + private constructor() {} + + public static getInstance(): TokenManager { + if (!TokenManager.instance) { + TokenManager.instance = new TokenManager(); + } + return TokenManager.instance; + } + + public getToken(key: string): string | undefined { + return getCookie(key); + } + + public setToken(key: string, value: string): void { + setCookie(key, value, { + secure: location.protocol === "https:", + sameSite: "lax", + }); + } + + public removeToken(key: string): void { + removeCookie(key); + } + + public setupRefreshTimer( + callback: () => Promise<void>, + expirationTime: number, + ): void { + if (this.refreshTimer) { + clearTimeout(this.refreshTimer); + } + + this.refreshTimer = setTimeout(() => { + callback().catch((error) => { + console.error("Silent refresh failed:", error); + this.clearAuthToken(); + }); + }, expirationTime - 60000); + } + + public clearAuthToken(): void { + if (this.refreshTimer) { + clearTimeout(this.refreshTimer); + this.refreshTimer = null; + } + + this.removeToken(CLAIM_NAME); + this.removeToken(AUTH_USER_ID); + } +} + +export const tokenManager = TokenManager.getInstance(); diff --git a/apps/fo/src/entities/auth/model/login-interface.ts b/apps/fo/src/entities/auth/model/login-interface.ts new file mode 100644 index 000000000..5aaf53044 --- /dev/null +++ b/apps/fo/src/entities/auth/model/login-interface.ts @@ -0,0 +1,9 @@ +export interface ILogin { + userId: string; + password: string; +} + +export interface LoginService { + login(payload: ILogin): Promise<void>; + silentRefresh(): Promise<void>; +} diff --git a/apps/fo/src/shared/config/api.ts b/apps/fo/src/shared/config/api.ts new file mode 100644 index 000000000..3eba9fa91 --- /dev/null +++ b/apps/fo/src/shared/config/api.ts @@ -0,0 +1,56 @@ +import { PROXY_HOST } from "./env"; + +export const BASE_URL = `${PROXY_HOST}/server`; + +export const ENDPOINT = { + ATTACH_SERVICE: { + TUS_UPLOAD: "/attach-service/api/v1/tus/upload", + EDITOR_UPLOAD: "/attach-service/api/v1/upload/editor", + IMAGES: "/attach-service/api/v1/images", + MOVIE_THUMBNAIL: "/attach-service/api/v1/movie/thumbnail", + ATTACHMENTS: "/attach-service/api/v1/atfl/attachments", + DOWNLOAD: "/attach-service/api/v1/atflcd/download", + STREAM_VIDEO: "/attach-service/api/v1/stream/video", + }, + USER_SERVICE: { + LOGIN: "/user-service/login", + USERS: "/user-service/api/v1/users", + SILENT_REFRESH: "/user-service/api/v1/users/token/refresh", + AUTHORIZATIONS: "/user-service/api/v1/authorizations", + ROLE_AUTHORIZATIONS: "/user-service/api/v1/role-authorizations", + ROLES: "/user-service/api/v1/roles", + }, + CMS_SERVICE: { + SITES: "/cms-service/api/v1/sites", + IPS: "/cms-service/api/v1/site-ips", + DOMAINS: "/cms-service/api/v1/site-dmns", + POSTINGS: "/cms-service/api/v1/site-pstgs", + CODE: "/cms-service/api/v1/code/cache", + MENUS: "/cms-service/api/v1/menus", + MENU_ROLES: "/cms-service/api/v1/menu-roles", + MENU_AUTHRTS: "/cms-service/api/v1/menu-authrts", + CONTENTS: "/cms-service/api/v1/contents", + CONTENTS_DETAILS: "/cms-service/api/v1/contents-details", + ORGANIZATIONS: "/cms-service/api/v1/ognzs", + USER_ORGANIZATION: "/cms-service/api/v1/user-ognz", + IMAGES: "/cms-service/api/v1/images", + BANNERS: "/cms-service/api/v1/bnrs", + TEMPLATE: "/cms-service/api/v1/tmplt", + TERMS: "/cms-service/api/v1/trms", + LWPO_CODE: "/cms-service/api/v1/lwpo-cds", + UP_CODE: "/cms-service/api/v1/up-codes", + POPUPS: "/cms-service/api/v1/popups", + SCHEDULES: "/cms-service/api/v1/schdls", + SURVEY: "/cms-service/api/v1/srvy", + SURVEY_QUESTION: "/cms-service/api/v1/srvy-qitem", + SURVEY_ARTICLE: "/cms-service/api/v1/srvy-artcl", + }, + BOARD_SERVICE: { + BBS: "/board-service/api/v1/bbs", + BBS_TEMPLATES: "/board-service/api/v1/bbsTmplt", + POSTS: "/board-service/api/v1/posts", + POSTS_COMMENTS: "/board-service/api/v1/posts-cmnts", + }, +}; + +export const JWT_EXPIRATION_TIME = 1800000; diff --git a/apps/fo/src/shared/config/env.ts b/apps/fo/src/shared/config/env.ts new file mode 100644 index 000000000..8baf1b0f0 --- /dev/null +++ b/apps/fo/src/shared/config/env.ts @@ -0,0 +1,14 @@ +export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "FO"; +export const PORT = process.env.NEXT_PUBLIC_PORT || "3000"; +export const PROXY_HOST = process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`; +export const SERVER_API_WR_URL = process.env.NEXT_PUBLIC_WR_SERVER_API_URL ||'http://con.mind-one.co.kr:11080/api/v1'; +export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL||' http://con.mind-one.co.kr:11082/api/v1'; +export const SERVER_API_CS_URL = process.env.NEXT_PUBLIC_CS_SERVER_API_URL||'http://dev.onasys.co.kr:8000'; +export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; +export const ACCESS_TOKEN = process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token"; +export const REFRESH_TOKEN =process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token"; +export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; +export const SERVER_SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL; + + + diff --git a/apps/fo/src/shared/config/index.ts b/apps/fo/src/shared/config/index.ts new file mode 100644 index 000000000..cd83447d5 --- /dev/null +++ b/apps/fo/src/shared/config/index.ts @@ -0,0 +1,2 @@ +export * from "./api"; +export * from "./env"; diff --git a/apps/fo/src/shared/lib/cookie.ts b/apps/fo/src/shared/lib/cookie.ts new file mode 100644 index 000000000..4bc0fe142 --- /dev/null +++ b/apps/fo/src/shared/lib/cookie.ts @@ -0,0 +1,31 @@ + +import Cookies from "js-cookie"; + + +export function getCookie(name: string): string | undefined { + if (typeof window === "undefined") { + return undefined; + } + return Cookies.get(name); +} + +export function setCookie( + name: string, + value: string, + options?: Cookies.CookieAttributes, +): void { + if (typeof window === "undefined") { + return; + } + Cookies.set(name, value, options); +} + +export function removeCookie( + name: string, + options?: Cookies.CookieAttributes, +): void { + if (typeof window === "undefined") { + return; + } + Cookies.remove(name, options); +} diff --git a/apps/fo/tailwind.config.ts b/apps/fo/tailwind.config.ts new file mode 100644 index 000000000..33ac9204f --- /dev/null +++ b/apps/fo/tailwind.config.ts @@ -0,0 +1,12 @@ +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/business_components/src/ui/**/*.{js,ts,jsx,tsx,mdx}", + "../../packages/common/components/src/ui/**/*.{js,ts,jsx,tsx,mdx}", + ], +} satisfies Config; \ No newline at end of file diff --git a/apps/fo/tsconfig.json b/apps/fo/tsconfig.json new file mode 100644 index 000000000..493584782 --- /dev/null +++ b/apps/fo/tsconfig.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "noImplicitAny": false, + "baseUrl": ".", + "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" + } + ], + "paths": { + "@/*": ["./*"], + "@common/components/*": ["../../packages/common/components/src/*"], + "@common/components": ["../../packages/common/components/src/index"], + "@common/business_components/*": [ + "../../packages/common/business_components/src/*" + ], + "@common/business_components": [ + "../../packages/common/business_components/src/index" + ], + "@common/network": ["../../packages/common/network"] + } + }, + "include": [ + ".eslintrc.*", + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + "**/*.mts", + "**/*.js", + "**/*.cjs", + "**/*.mjs", + "**/*.jsx", + "**/*.json", + ".next/types/**/*.ts" + ], + "exclude": ["**/node_modules", "**/.*/*"] +} diff --git a/apps/sn/.env.development b/apps/sn/.env.development new file mode 100644 index 000000000..f12d2c38e --- /dev/null +++ b/apps/sn/.env.development @@ -0,0 +1,13 @@ +NEXT_PUBLIC_SITE_ID = SN +NEXT_PUBLIC_PORT = 11302 +NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11302 +NEXT_PUBLIC_ADDR_URL = http://con.mind-one.co.kr:11082 +NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver +NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver #TODO 운영에서 바꿔야 함 +NEXT_PUBLIC_DC_URL = https://apigateway.rsonesoft.com #자료관 데이터 +NEXT_PUBLIC_CLAIM_NAME = Authorization +NEXT_PUBLIC_AUTH_USER_ID = token-id +NEXT_PUBLIC_ACCESS_TOKEN = access-token +NEXT_PUBLIC_REFRESH_TOKEN = refresh-token +NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API \ No newline at end of file diff --git a/apps/sn/.env.local b/apps/sn/.env.local new file mode 100644 index 000000000..253ed7528 --- /dev/null +++ b/apps/sn/.env.local @@ -0,0 +1,15 @@ +NEXT_PUBLIC_SITE_ID = SN +NEXT_PUBLIC_PORT = 3002 +NEXT_PUBLIC_PROXY_HOST = http://localhost:3002 +NEXT_PUBLIC_ADDR_URL = http://con.mind-one.co.kr:11082 +NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver +NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver +NEXT_PUBLIC_DC_URL = https://apigateway.rsonesoft.com #자료관 데이터 +NEXT_PUBLIC_CLAIM_NAME = Authorization +NEXT_PUBLIC_AUTH_USER_ID = token-id +NEXT_PUBLIC_ACCESS_TOKEN = access-token +NEXT_PUBLIC_REFRESH_TOKEN = refresh-token +NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API +#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성 부장님 API +#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.222:8883/sewerage-service/api # 박이정 차장님 API \ No newline at end of file diff --git a/apps/sn/.env.production b/apps/sn/.env.production new file mode 100644 index 000000000..fd98438d7 --- /dev/null +++ b/apps/sn/.env.production @@ -0,0 +1,13 @@ +NEXT_PUBLIC_SITE_ID = SN +NEXT_PUBLIC_CLAIM_NAME = Authorization +NEXT_PUBLIC_AUTH_USER_ID = token-id +NEXT_PUBLIC_ACCESS_TOKEN = access-token +NEXT_PUBLIC_REFRESH_TOKEN = refresh-token +NEXT_PUBLIC_PORT = 3000 +NEXT_PUBLIC_PROXY_HOST = https://sn.hasuon.or.kr +NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000 +NEXT_PUBLIC_ADDR_URL = http://con.mind-one.co.kr:11082 +NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver +NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl +NEXT_PUBLIC_DC_URL = https://apigateway.rsonesoft.com #자료관 데이터 +NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api diff --git a/apps/sn/app/(map-layout)/layout.tsx b/apps/sn/app/(map-layout)/layout.tsx new file mode 100644 index 000000000..e245cfdf1 --- /dev/null +++ b/apps/sn/app/(map-layout)/layout.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { usePathname, useRouter } from "next/navigation"; +import { useQueryClient } from "@tanstack/react-query"; +import { BaseLayout } from "@common/components"; +import type { MenuItem } from "@/entities/menu"; +import { useMenuStore } from "@/shared/lib"; + +export default function MapLayout({ + children, +}: { + children: React.ReactNode; +}) { + const router = useRouter(); + const pathname = usePathname(); + const queryClient = useQueryClient(); + + const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo); + + const handleMenuClick = (item: MenuItem) => { + setCurrentMenuInfo(item); + router.push(item.urlPath); + }; + + return ( + <BaseLayout + queryClient={queryClient} + currentPath={pathname ?? ""} + handleMenuClick={handleMenuClick} + > + {children} + </BaseLayout> + ); +} diff --git a/apps/sn/app/(map-layout)/map/commonStore.ts b/apps/sn/app/(map-layout)/map/commonStore.ts new file mode 100644 index 000000000..ff903e1e7 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/commonStore.ts @@ -0,0 +1,25 @@ +import { create } from 'zustand'; + +export type SelectDataType = { + text: string; + val: string; + sido?: string; // 시도 코드 (선택 사항) +} + +interface CityCdStore { + cityCd: string; + setCityCd: (cityCd: string) => void; + sidos: SelectDataType[]; + setSidos: (sidos: SelectDataType[]) => void; + siguns : SelectDataType[]; + setSiguns: (siguns: SelectDataType[]) => void; +} + +export const useCityCdStore = create<CityCdStore>((set) => ({ + cityCd: '', + setCityCd: (cityCd: string) => set({ cityCd }), + sidos: [], + setSidos: (sidos: SelectDataType[]) => set({ sidos }), + siguns: [], + setSiguns: (siguns: SelectDataType[]) => set({ siguns }), +})); diff --git a/apps/sn/app/(map-layout)/map/components/InputSection.tsx b/apps/sn/app/(map-layout)/map/components/InputSection.tsx new file mode 100644 index 000000000..e35165bd2 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/InputSection.tsx @@ -0,0 +1,58 @@ +import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@common/components/ui"; +import React from "react"; + +interface InputSectionProps { + title: string; + data: { year: string; type: string, bizNm: string, bizType: 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 whitespace-nowrap">{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 w-auto whitespace-nowrap">구분</TableHead> + <TableHead className="border border-gray-300 px-2 py-2 text-sm w-[80px] whitespace-nowrap">진단연도</TableHead> + <TableHead className="border border-gray-300 px-2 py-2 text-sm w-auto whitespace-nowrap">사업명</TableHead> + <TableHead className="border border-gray-300 px-2 py-2 text-sm w-auto whitespace-nowrap">유형</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {data.map((row, index) => ( + <TableRow key={index}> + <TableCell className="border border-gray-300 px-2 py-2 text-sm text-center leading-normal w-auto whitespace-nowrap"> + {row.type} + </TableCell> + <TableCell className="border border-gray-300 px-2 py-2 text-sm w-[80px] whitespace-nowrap"> + <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 w-auto"> + {row.bizNm} + </TableCell> + <TableCell className="border border-gray-300 px-2 py-2 text-sm text-center leading-normal w-auto whitespace-nowrap"> + {row.bizType} + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </div> + </div> + ); +}; + +export default InputSection; \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/components/common/codeMapper.ts b/apps/sn/app/(map-layout)/map/components/common/codeMapper.ts new file mode 100644 index 000000000..61a11c8a8 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/common/codeMapper.ts @@ -0,0 +1,155 @@ +import { MS_SWAGE_PPL_USE_CODES, MS_SWAGE_PPL_CODES, MS_SWAGE_MHL_CODES, MS_SWAGE_MHL_USE_CODES, DRTW_SEWAGE_PUMP_USE_CODES } from './table'; + +interface CodeMapperProps { + layerName: string; + data: any; +} + +/** + * 레이어별 코드 변환 함수 + * @param layerName - 레이어 이름 + * @param data - 변환할 데이터 객체 + * @returns 변환된 데이터 객체 + */ +export const codeMapper = ({ layerName, data }: CodeMapperProps): any => { + if (!data) return data; + + const processedData = { ...data }; + + switch (layerName) { + case '하수관로': + // usg_cd 값 변환 (용도코드) + if (processedData.usg_cd) { + const usageCodeItem = MS_SWAGE_PPL_USE_CODES.find( + code => code.val === processedData.usg_cd.trim() + ); + if (usageCodeItem) { + processedData.usg_cd = usageCodeItem.text; + } + } + + // mqlt_cd 값 변환 (재질코드) + if (processedData.mqlt_cd) { + const materialCodeItem = MS_SWAGE_PPL_CODES.find( + code => code.val === processedData.mqlt_cd.trim() + ); + if (materialCodeItem) { + processedData.mqlt_cd = materialCodeItem.text; + } + } + break; + + case '하수맨홀': + // usg_cd 값 변환 (용도코드) + if (processedData.usg_cd) { + const usageCodeItem = MS_SWAGE_MHL_USE_CODES.find( + code => code.val === processedData.usg_cd.trim() + ); + if (usageCodeItem) { + processedData.usg_cd = usageCodeItem.text; + } + } + + // mqlt_cd 값 변환 (재질코드) + if (processedData.mqlt_cd) { + const materialCodeItem = MS_SWAGE_MHL_CODES.find( + code => code.val === processedData.mqlt_cd.trim() + ); + if (materialCodeItem) { + processedData.mqlt_cd = materialCodeItem.text; + } + } + break; + + case '하수펌프장': + // usg_cd 값 변환 (용도코드) + if (processedData.usg_cd) { + const usageCodeItem = DRTW_SEWAGE_PUMP_USE_CODES.find( + code => code.val === processedData.usg_cd.trim() + ); + if (usageCodeItem) { + processedData.usg_cd = usageCodeItem.text; + } + } + break; + + case '우수토실': + // prrtymngYn 값 변환 (중점여부) + if (processedData.prrtymngYn) { + if (processedData.prrtymngYn === 'Y') { + processedData.prrtymngYn = '중점우수토실'; + } else if (processedData.prrtymngYn === 'N') { + processedData.prrtymngYn = '우수토실'; + } + } + break; + case '중점우수토실': + if (processedData.manage_no) { + processedData.mqlt_cd = processedData.manage_no; + } + break; + + default: + // 다른 레이어는 변환하지 않음 + break; + } + + return processedData; +}; + +/** + * 저장 시 코드 역변환 함수 (한글 → 코드) + * @param layerName - 레이어 이름 + * @param data - 역변환할 데이터 객체 + * @returns 역변환된 데이터 객체 + */ +export const reverseCodeMapper = ({ layerName, data }: CodeMapperProps): any => { + if (!data) return data; + + const processedData = { ...data }; + + switch (layerName) { + case '하수맨홀': + // mqlt_cd 값 역변환 (텍스트 → 코드) + if (processedData.mqlt_cd && typeof processedData.mqlt_cd === 'string') { + const materialCodeItem = MS_SWAGE_MHL_CODES.find( + code => code.text === processedData.mqlt_cd.trim() + ); + if (materialCodeItem) { + processedData.mqlt_cd = materialCodeItem.val; + } + } + break; + + case '우수토실': + // prrtymngYn 값 역변환 (텍스트 → Y/N) + if (processedData.prrtymngYn) { + if (processedData.prrtymngYn === '중점우수토실') { + processedData.prrtymngYn = 'Y'; + } else if (processedData.prrtymngYn === '우수토실') { + processedData.prrtymngYn = 'N'; + } + } + break; + + case '하수펌프장': + // usg_cd 값 역변환 (텍스트 → 코드) + if (processedData.usg_cd && typeof processedData.usg_cd === 'string') { + const usageCodeItem = DRTW_SEWAGE_PUMP_USE_CODES.find( + code => code.text === processedData.usg_cd.trim() + ); + if (usageCodeItem) { + processedData.usg_cd = usageCodeItem.val; + } + } + break; + + default: + break; + } + + return processedData; +}; + +// 기본 내보내기 +export default { codeMapper, reverseCodeMapper }; \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/components/common/index.ts b/apps/sn/app/(map-layout)/map/components/common/index.ts new file mode 100644 index 000000000..9860b211a --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/common/index.ts @@ -0,0 +1,65 @@ +import MsSwagePpl from "../popupComponent/MsSwagePpl"; +import sewagePump from "../popupComponent/sewagePump"; +import smh from "../popupComponent/smh"; +import Ssc from "../popupComponent/Ssc"; +import StormWaterPump from "../popupComponent/stormWaterPump"; +import SwtrmtFclt from "../popupComponent/SwtrmtFclt"; + +type LayerComponentProps = { + featureData: any; + isMainSsc?: boolean; + isDrtwSewagePump?: boolean; + rainMeasureData?: any; + onSaveClick?: () => void; + onFormDataChange?: (data: any) => void; // 추가 +}; +type LayerContentMap = { + [key: string]: { + component?: React.FC<LayerComponentProps>; + props?: any; + }; +}; +// 레이어 이름과 컴포넌트를 매핑 +// 레이어 선택시 변경되는 팝업 컨텐츠 동적으로 변경하도록 작업 +const layerContentMap: LayerContentMap = { + // "하수처리시설": { component: SwtrmtFclt, }, // 디지털 자료관 연계정보가 없으니 일단 주석 처리 + "하수처리구역": {}, + "우수토실": { component: Ssc, }, + "중점우수토실": { component: Ssc, }, // 중점우수토실 여부 설정 + "하수관로": { component: MsSwagePpl, }, + "하수맨홀": { component: smh, }, + "하수펌프장": { component: sewagePump }, + // "빗물펌프": { component: StormWaterPump }, + // "오수펌프": { component: sewagePump }, +}; + +// 동적 컴포넌트 반환 함수 +export const getLayerContentComponent = (layerNm: string): React.FC<LayerComponentProps> | null => { + 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, bizNm: string, bizType: string }[]; + setData: React.Dispatch<React.SetStateAction<{ year: string; type: string, bizNm: string, bizType: 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-layout)/map/components/common/table.ts b/apps/sn/app/(map-layout)/map/components/common/table.ts new file mode 100644 index 000000000..84de2b4ab --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/common/table.ts @@ -0,0 +1,690 @@ +// 하수관로 +export const MsSwagePplCol = [ + { id: 'fclt_mng_no', title: '시설관리번호' }, + { id: 'inst_mng_no', title: '기관관리번호' }, + { id: 'instl_ymd', title: '설치일자' }, + { id: 'mapsht_no', title: '도엽번호' }, + { id: 'pdmt', title: '관경(mm)' }, + { id: 'prlg', title: '연장(m)' }, + { id: 'usg_cd', title: '용도' }, + { id: 'mqlt_cd', title: '재질' }, + { id: 'avg_grdn', title: '평균구배(m/m)' }, + { id: 'drtw_mltv_area', title: '오수배수면적(m²)' }, + { id: 'excln_mltv_area', title: '우수배수면적(m²)' }, + { id: 'tmpt_dpth', title: '시점깊이(m)' }, + { id: 'tmpt_mhl_mng_no', title: '시점맨홀관리번호' }, + { id: 'scend_dpth', title: '종점깊이(m)' }, + { id: 'scend_mhl_mng_no', title: '종점맨홀관리번호' }, + { id: 'trfln_cnt', title: '차선수' }, + { id: 'vrtc_len', title: '세로길이(m)' }, + { id: 'wdth_len', title: '가로길이(m)' }, + // { id: 'rnyssn_fvlc', title: '우기 시 유속(m/s)' }, + // { id: 'clar_fvlc', title: '건기 시 유속(m/s)' }, + // { id: 'mng_inst_cd', title: '관리기관코드' }, + // { id: 'pbadms_emd_cd', title: '행정읍면동코드' }, + // { id: 'fclty_shp_cd', title: '시설물형태코드' }, + // { id: 'fid', title: 'FID' }, + // { id: 'ftre_smbol_cd', title: '지형지물부호코드' }, + // { id: 'lyrTypeNm', title: '레이어타입명' }, +] +export const MsSwagePplCol2 = [ + { id: 'rowNumber', title: '일련번호' }, + { id: 'fcltMngNo', title: '시설 관리번호' }, + { id: 'instMngNo', title: '기관 관리번호' }, + { id: 'instlYmd', title: '설치일자' }, + // { id: 'mapshtNo', title: '도엽번호' }, + { id: 'pdmt', title: '관경(mm)' }, + { id: 'prlg', title: '연장(m)' }, + { id: 'avgGrdn', title: '평균구배(m/m)' }, + { id: 'tmptDpth', title: '시점깊이(m)' }, + { id: 'tmptMhlMngNo', title: '시점맨홀 관리번호' }, + { id: 'scendDpth', title: '종점깊이(m)' }, + { id: 'scendMhlMngNo', title: '종점맨홀 관리번호' }, + { id: 'vrtcLen', title: '세로길이(m)' }, + { id: 'wdthLen', title: '가로길이(m)' }, + // { id: 'fcltyShpCd', title: '시설물형태코드' }, + // { id: 'fid', title: 'FID' }, + // { id: 'ftreSmbolCd', title: '지형지물부호코드' }, + // { id: 'mngInstCd', title: '관리기관코드' }, + // { id: 'mqltCd', title: '재질코드' }, + // { id: 'pbadmsEmdCd', title: '행정읍면동코드' }, + // { id: 'usgCd', title: '용도코드' }, + // { id: 'lyrTypeNm', title: '레이어타입명' }, +] +// 하수관로 용도 코드표 +export const MS_SWAGE_PPL_USE_CODES = [ + { val: "SBA000", text: "미분류" }, + { val: "SBA001", text: "합류관" }, + { val: "SBA002", text: "차집관" }, + { val: "SBA003", text: "우수관" }, + { val: "SBA004", text: "오수관" }, + { val: "SBA999", text: "기타" }, +]; +// 하수관로 재질 코드표 +export const MS_SWAGE_PPL_CODES = [ + { val: "MOP000", text: "미분류" }, + { val: "MOP001", text: "닥타일주철관(DCIP)" }, + { val: "MOP002", text: "강관(SP)" }, + { val: "MOP003", text: "아연도강관(GS)" }, + { val: "MOP004", text: "염화비닐관(PVC)" }, + { val: "MOP005", text: "폴리에틸렌관(PE)" }, + { val: "MOP006", text: "스테인레스관(SSP)" }, + { val: "MOP007", text: "동관(COP)" }, + { val: "MOP008", text: "P.S.콘크리트관(PC)" }, + { val: "MOP009", text: "석면시멘트관(AS)" }, + { val: "MOP010", text: "회주철관(CIP)" }, + { val: "MOP011", text: "연관(LP)" }, + { val: "MOP012", text: "흉관(HP)" }, + { val: "MOP013", text: "철근콘크리트관(RC)" }, + { val: "MOP014", text: "강관(ST)" }, + { val: "MOP015", text: "주철관(CI)" }, + { val: "MOP016", text: "화학제품류관(PV)" }, + { val: "MOP017", text: "도관(VC)" }, + { val: "MOP018", text: "석축(SE)" }, + { val: "MOP019", text: "도복장 강관(SP)" }, + { val: "MOP020", text: "흉관(EP)" }, + { val: "MOP021", text: "아연도강관(GP)" }, + { val: "MOP022", text: "청동관(BR)" }, + { val: "MOP023", text: "고무몰딩(RM)" }, + { val: "MOP024", text: "주강(CS)" }, + { val: "MOP025", text: "VR관" }, + { val: "MOP026", text: "HDPE관" }, + { val: "MOP027", text: "시멘트라이닝주철관(C)" }, + { val: "MOP028", text: "수도용 PE관(PE)" }, + { val: "MOP029", text: "석면시멘트관(AC)" }, + { val: "MOP030", text: "폴리에틸렌분체라이닝강관(PEP)" }, + { val: "MOP031", text: "에폭시코팅강관(EPS)" }, + { val: "MOP032", text: "HI-3P" }, + { val: "MOP033", text: "철근콘크리트박스(RCB)" }, + { val: "MOP034", text: "콘크리트중선함(CD)" }, + { val: "MOP035", text: "후랙시불전선함(FP)" }, + { val: "MOP036", text: "전선함(FD)" }, + { val: "MOP038", text: "이중보온관" }, + { val: "MOP039", text: "폴리에틸렌피복강관(PLP)" }, + { val: "MOP041", text: "FRP" }, + { val: "MOP042", text: "강재" }, + { val: "MOP999", text: "기타" }, + { val: "MOP800", text: "2중벽PE관" }, + { val: "MOP801", text: "3중벽PE관" }, + { val: "MOP802", text: "가스배관용 강관" }, + { val: "MOP803", text: "강관(피복 Double, 용접접합)" }, + { val: "MOP804", text: "강관(피복 Single, 용접접합)" }, + { val: "MOP805", text: "강관(CP)" }, + { val: "MOP806", text: "강철" }, + { val: "MOP807", text: "강화플라스틱복합관" }, + { val: "MOP808", text: "경질염화비닐관(HI-VP)" }, + { val: "MOP809", text: "고강성PVC이중벽관" }, + { val: "MOP810", text: "고광도PVC이중벽관" }, + { val: "MOP811", text: "고밀도폴리에틸렌관(THP)" }, + { val: "MOP812", text: "고밀도폴리에틸렌파상형이중벽관" }, + { val: "MOP813", text: "공장제 박스관거" }, + { val: "MOP814", text: "관로없음(알케이블)" }, + { val: "MOP815", text: "나선형강관" }, + { val: "MOP816", text: "내충격수도관" }, + { val: "MOP817", text: "내충격용하수도용경질염화비닐관(HI-VG)" }, + { val: "MOP818", text: "내충격HI-VG2관" }, + { val: "MOP819", text: "내충격PVC하수관" }, + { val: "MOP820", text: "녹석(돌산호과 일종)" }, + { val: "MOP821", text: "동관(구리관, 용접)" }, + { val: "MOP822", text: "둥근잔돌" }, + { val: "MOP823", text: "무근콘크리트" }, + { val: "MOP824", text: "백관" }, + { val: "MOP825", text: "복합철판파형관" }, + { val: "MOP826", text: "분체라이닝관" }, + { val: "MOP827", text: "석축박스" }, + { val: "MOP828", text: "석축철콘" }, + { val: "MOP829", text: "수도용경질염화비관" }, + { val: "MOP830", text: "수도용경질염화비닐이음관" }, + { val: "MOP831", text: "수도용엑포시수지분체내외면코팅강관" }, + { val: "MOP832", text: "수지파형강관(PCS)" }, + { val: "MOP833", text: "수지CSP" }, + { val: "MOP834", text: "스테인레스 주름관" }, + { val: "MOP835", text: "스테인레스관(STS)" }, + { val: "MOP836", text: "스테인레스스틸파이프관" }, + { val: "MOP837", text: "아스콘" }, + { val: "MOP838", text: "아스팔트" }, + { val: "MOP839", text: "아스팔트도복장강관(ASP)" }, + { val: "MOP840", text: "아연" }, + { val: "MOP841", text: "아연도금(백관)" }, + { val: "MOP842", text: "알루미늄" }, + { val: "MOP843", text: "알루미늄합금" }, + { val: "MOP844", text: "압력배관용 탄소강관" }, + { val: "MOP845", text: "양철관" }, + { val: "MOP846", text: "양철주름관(CSP)" }, + { val: "MOP847", text: "연합인슈강판" }, + { val: "MOP848", text: "와사관(GSP)" }, + { val: "MOP849", text: "원심력철근콘크리트" }, + { val: "MOP850", text: "유리섬유관(GRP)" }, + { val: "MOP851", text: "유리섬유복합관(GRP)" }, + { val: "MOP852", text: "이음매없는동및동합금관" }, + { val: "MOP853", text: "일반배관용스테인리스강관" }, + { val: "MOP854", text: "전압 콘크리트관" }, + { val: "MOP855", text: "주름관" }, + { val: "MOP856", text: "주름주철파형강관" }, + { val: "MOP857", text: "지중 저압케이블 입상관" }, + { val: "MOP858", text: "진동및전압철근콘크리트관" }, + { val: "MOP859", text: "철근콘크리트" }, + { val: "MOP860", text: "철근콘크리트 블록" }, + { val: "MOP861", text: "철근콘크리트조립흙막이" }, + { val: "MOP862", text: "철판매입관" }, + { val: "MOP863", text: "청동" }, + { val: "MOP864", text: "코아식프리스트레스트콘크리트관" }, + { val: "MOP865", text: "콘크리트" }, + { val: "MOP866", text: "콘크리트 폴륨관" }, + { val: "MOP867", text: "콘크리트블록" }, + { val: "MOP868", text: "탄소강" }, + { val: "MOP869", text: "탄소강(카본스틸)" }, + { val: "MOP870", text: "탄소주강" }, + { val: "MOP871", text: "트러후" }, + { val: "MOP872", text: "파선형강관(RCF)" }, + { val: "MOP873", text: "파형강관(CSP)" }, + { val: "MOP874", text: "파형관" }, + { val: "MOP875", text: "파형플라스틱관" }, + { val: "MOP876", text: "폴리머관" }, + { val: "MOP877", text: "폴리에스테르수지콘크리트관" }, + { val: "MOP878", text: "폴리에틸렌 전선관" }, + { val: "MOP879", text: "폴리에틸렌 피복강관" }, + { val: "MOP880", text: "폴리에틸렌관 1호관" }, + { val: "MOP881", text: "폴리에틸렌관 2호관" }, + { val: "MOP882", text: "폴리에틸렌관(2중벽)" }, + { val: "MOP883", text: "폴리에틸렌관(3중벽)" }, + { val: "MOP884", text: "폴리에틸렌분체라이닝" }, + { val: "MOP885", text: "폴리에틸렌이중벽하수관" }, + { val: "MOP886", text: "폴리탄산에스테르" }, + { val: "MOP887", text: "프리스트레스트콘크리트실린더관" }, + { val: "MOP888", text: "플라스틱" }, + { val: "MOP889", text: "플라스틱주름관" }, + { val: "MOP890", text: "하수도용고강성경질염화비닐이중벽주름관" }, + { val: "MOP891", text: "현장타설관" }, + { val: "MOP892", text: "현장타설철근콘크리트관" }, + { val: "MOP893", text: "흑관" }, + { val: "MOP894", text: "흙" }, + { val: "MOP895", text: "AP" }, + { val: "MOP896", text: "API관" }, + { val: "MOP897", text: "ASP" }, + { val: "MOP898", text: "CMCIP" }, + { val: "MOP899", text: "CSP" }, + { val: "MOP900", text: "DRP" }, + { val: "MOP901", text: "DSP" }, + { val: "MOP902", text: "DTC관" }, + { val: "MOP903", text: "ELP" }, + { val: "MOP904", text: "EPP" }, + { val: "MOP905", text: "FC관" }, + { val: "MOP906", text: "H" }, + { val: "MOP907", text: "H6" }, + { val: "MOP908", text: "HCP" }, + { val: "MOP909", text: "HI_2P관" }, + { val: "MOP910", text: "HI-KP" }, + { val: "MOP911", text: "HL" }, + { val: "MOP912", text: "IP" }, + { val: "MOP913", text: "KCGP" }, + { val: "MOP914", text: "KP관" }, + { val: "MOP915", text: "P2S관" }, + { val: "MOP916", text: "PC박스(PCB)" }, + { val: "MOP917", text: "PCS" }, + { val: "MOP918", text: "PDP" }, + { val: "MOP919", text: "PE이중벽관" }, + { val: "MOP920", text: "PE적측벽1종관" }, + { val: "MOP921", text: "PFP관" }, + { val: "MOP922", text: "PG관" }, + { val: "MOP923", text: "PIP관(이중보온관)" }, + { val: "MOP924", text: "PLCSP" }, + { val: "MOP925", text: "PS" }, + { val: "MOP926", text: "PUB" }, + { val: "MOP927", text: "PVB" }, + { val: "MOP928", text: "PVC이중벽관(PVC2)" }, + { val: "MOP929", text: "PVC-DC" }, + { val: "MOP930", text: "PVC-DV" }, + { val: "MOP931", text: "SBK" }, + { val: "MOP932", text: "SCP" }, + { val: "MOP933", text: "SEB" }, + { val: "MOP934", text: "SIS관(이중보온관)" }, + { val: "MOP935", text: "SMC" }, + { val: "MOP936", text: "STW" }, + { val: "MOP937", text: "SUS" }, + { val: "MOP938", text: "THP나선관" }, + { val: "MOP939", text: "U관" }, + { val: "MOP940", text: "VG2" }, + { val: "MOP941", text: "WSP(와사피)" }, + { val: "MOP942", text: "Steel Grating" }, + { val: "MOP943", text: "모래" }, + { val: "MOP944", text: "인조석" }, + { val: "MOP945", text: "SPOL" }, + { val: "MOP946", text: "PCF관(강판보강 PE 나선형관)" }, + { val: "MOP947", text: "PVC3(PVC3중벽관)" }, + { val: "MOP948", text: "PE다중벽관" }, + { val: "MOP977", text: "전주용입상관" }, + { val: "MOP978", text: "MD관" }, +]; + +// 하수처리시설 +export const SwtrmtFcltCol = [ + { id: 'swtrmt_fclt_nm', title: '하수처리시설명' }, + { id: 'fclt_cpct', title: '시설용량(㎥/d)' }, + { id: 'rgn_expln', title: '지역설명' }, + { id: 'cmcn_ymd', title: '준공일자' }, + { id: 'oprtng_strt_ymd', title: '가동개시일자' }, + { id: 'clsg_ymd', title: '폐쇄일자' }, + // { id: 'prcs_msrc_cd', title: '처리공법코드' }, + // { id: 'lae_cd', title: '지자체코드' }, + { id: 'lae_nm', title: '지자체명' }, + { id: 'ctpv_nm', title: '시도명' }, + { id: 'sgg_nm', title: '시군구명' }, + // { id: 'sarea_cd', title: '권역코드' }, + { id: 'sarea_nm', title: '권역명' }, + { id: 'lrge_sarea_nm', title: '대권역명' }, + { id: 'rvr_nm', title: '하천명' }, + { id: 'addr', title: '주소' }, + { id: 'dtl_addr', title: '상세주소' }, + { id: 'rdnm_addr', title: '도로명주소' }, + { id: 'dtl_rdnm_addr', title: '상세도로명주소' }, + { id: 'dschg_wtst_nm', title: '방류수계명' }, + { id: 'plln_area', title: '부지면적(m²)' }, + { id: 'mng_rprtv_bzenty_nm', title: '관리대행업제명' }, + { id: 'swtrmt_ppltn_cnt', title: '하수처리인구수(인)' }, + { id: 'prcs_zone_area_sum', title: '처리구역면적합계(m²)' }, + { id: 'xcrd', title: 'X좌표' }, + { id: 'ycrd', title: 'Y좌표' }, + // { id: 'oprtngSttsCd', title: '가동상태코드' }, + { id: 'rprs_msrc_cn', title: '대표공법내용' }, + // { id: 'stty_crtr_bod', title: '법정기준생물화학적산소요구량(mg/L)' }, + // { id: 'stty_crtr_toc', title: '법정기준TOC((mg/L))' }, + // { id: 'stty_crtr_ss', title: '법정기준SS(mg/L)' }, + // { id: 'stty_crtr_tn', title: '법정기준총질소(mg/L)' }, + // { id: 'stty_crtr_tph', title: '법정기준총인(mg/L)' }, + // { id: 'stty_crtr_clbcl_cnt', title: '법정기준대장균수(군수/100mL)' }, + { id: 'bsn_nm', title: '유역명' }, + // { id: 'obsrvnSptCd', title: '관측지점코드' }, +]; +export const SwtrmtFcltCol2 = [ + { id: 'rowNumber', title: '일련번호' }, + { id: 'swtrmtFcltNm', title: '하수처리시설명' }, + { id: 'fcltCpct', title: '시설용량(㎥/d)' }, + { 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: 'dtlAddr', title: '상세주소' }, + // { id: 'rdnmAddr', title: '도로명주소' }, + // { id: 'dtlRdnmAddr', title: '상세도로명주소' }, + { id: 'dschgWtstNm', title: '방류수계명' }, + // { id: 'pllnArea', title: '부지면적(m²)' }, + // { id: 'mngRprtvBzentyNm', title: '관리대행업체명' }, + { id: 'swtrmtPpltnCnt', title: '하수처리인구수(인)' }, + // { id: 'prcsZoneAreaSum', title: '처리구역면적합계(m²)' }, + // { id: 'xcrd', title: 'X좌표' }, + // { id: 'ycrd', title: 'Y좌표' }, + // { id: 'oprtngSttsCd', title: '가동상태코드' }, + { id: 'rprsMsrcCn', title: '대표공법내용' }, + // { id: 'sttyCrtrBod', title: '법정기준생물화학적산소요구량(mg/L)' }, + // { id: 'sttyCrtrToc', title: '법정기준TOC(mg/L)' }, + // { id: 'sttyCrtrSs', title: '법정기준SS(mg/L)' }, + // { id: 'sttyCrtrTn', title: '법정기준총질소(mg/L)' }, + // { id: 'sttyCrtrTph', title: '법정기준총인(mg/L)' }, + // { id: 'sttyCrtrClbclCnt', title: '법정기준대장균수(군수/100mL)' }, + { id: 'bsnNm', title: '유역명' }, + // { id: 'obsrvnSptCd', title: '관측지점코드' }, +]; + +// 하수처리구역 +export const SwtrmtPrcsZoneCol = [ + // { id: 'swtrmtZoneSn', title: '하수처리구역 일련번호' }, + { id: 'swtrmt_zone_nm', title: '하수처리구역명' }, + // { id: 'swtrmt_fclt_cd', title: '하수처리시설코드' }, + // { id: 'ctpv_cd', title: '시도코드' }, + { id: 'ctpv_nm', title: '시도명' }, + // { id: 'sgg_cd', title: '시군구코드' }, + { id: 'sgg_nm', title: '시군구명' }, + { id: 'swtrmt_fclt_nm', title: '하수처리시설명' }, +]; +export const SwtrmtPrcsZoneCol2 = [ + { id: 'rowNumber', title: '일련번호' }, + // { id: 'swtrmtZoneSn', title: '하수처리구역 일련번호' }, + { id: 'swtrmtZoneNm', title: '하수처리구역명' }, + // { id: 'swtrmt_fclt_cd', title: '하수처리시설코드' }, + // { id: 'ctpv_cd', title: '시도코드' }, + { id: 'ctpvNm', title: '시도명' }, + // { id: 'sgg_cd', title: '시군구코드' }, + { id: 'sggNm', title: '시군구명' }, + { id: 'swtrmtFcltNm', title: '하수처리시설명' }, +]; + +// 하수맨홀 +export const MsSwageMhlCol = [ + // { id: 'fid', title: 'FID' }, + // { id: 'geom', title: 'geometry' }, + // { id: 'ftre_smbol_cd', title: '지형지물부호코드' }, + { id: 'fclt_mng_no', title: '시설관리번호' }, + // { id: 'pbadms_emd_cd', title: '행정읍면동코드' }, + { id: 'mapsht_no', title: '도엽번호' }, + // { id: 'mng_inst_cd', title: '관리기관코드' }, + { id: 'instl_ymd', title: '설치일자' }, + { id: 'last_drdgn_ymd', title: '최종준설일자' }, + // { id: 'usg_cd', title: '용도코드' }, + // { id: 'fclty_shp_cd', title: '시설물형태코드' }, + // { id: 'mhl_knd_cd', title: '맨홀종류코드' }, + { id: 'swage_mh_diam', title: '하수맨홀구경(mm)' }, + { id: 'mqlt_cd', title: '재질' }, + { id: 'wdth_len', title: '가로길이(m)' }, + { id: 'vrtc_len', title: '세로길이(m)' }, + // { id: 'ang_dir', title: '표고' }, + { id: 'altd_vl', title: '표고(EL.m)' }, + { id: 'dpthl_vl', title: '심도(m)' }, + // { id: 'inst_mng_no', title: '기관관리번호' } +]; +export const MsSwageMhlCol2 = [ + { id: 'rowNumber', title: '일련번호' }, + // { id: 'fid', title: 'FID' }, + // { id: 'geom', title: 'geometry' }, + // { id: 'ftreSmbolCd', title: '지형지물부호코드' }, + { id: 'fcltMngNo', title: '시설관리번호' }, + // { id: 'pbadmsEmdCd', title: '행정읍면동코드' }, + // { id: 'mapshtNo', title: '도엽번호' }, + // { id: 'mngInstCd', title: '관리기관코드' }, + { id: 'instlYmd', title: '설치일자' }, + // { id: 'lastDrdgnYmd', title: '최종준설일자' }, + // { id: 'usgCd', title: '용도코드' }, + // { id: 'fcltyShpCd', title: '시설물형태코드' }, + // { id: 'mhlKndCd', title: '맨홀종류코드' }, + { id: 'swageMhDiam', title: '하수맨홀구경(mm)' }, + { id: 'mqltCd', title: '재질코드' }, + { id: 'wdthLen', title: '가로길이(m)' }, + { id: 'vrtcLen', title: '세로길이(m)' }, + // { id: 'angDir', title: '표고' }, + { id: 'altdVl', title: '표고(EL.m)' }, + { id: 'dpthlVl', title: '심도(m)' }, + // { id: 'instMngNo', title: '기관관리번호' } +]; +// 하수맨홀 뚜껑재질 코드표 +export const MS_SWAGE_MHL_CODES = [ + { val: "SBC000", text: "미분류" }, + { val: "SBC001", text: "주철" }, + { val: "SBC002", text: "철판" }, + { val: "SBC003", text: "콘크리트" }, + { val: "SBC004", text: "그레이팅" }, + { val: "SBC091", text: "칼라" }, + { val: "SBC092", text: "S.G" }, + { val: "SBC999", text: "기타" }, + { val: "SBC900", text: "각주철" }, + { val: "SBC901", text: "강재" }, + { val: "SBC902", text: "나무" }, + { val: "SBC903", text: "나무,널판지,합판" }, + { val: "SBC904", text: "대리석" }, + { val: "SBC905", text: "뚜껑없음" }, + { val: "SBC906", text: "보도용콘크리트" }, + { val: "SBC907", text: "스틸(ST)" }, + { val: "SBC908", text: "신형 CON" }, + { val: "SBC909", text: "알루미늄" }, + { val: "SBC910", text: "염화비닐(PE)" }, + { val: "SBC911", text: "인조석" }, + { val: "SBC912", text: "인조석(주철)" }, + { val: "SBC913", text: "인조CONC" }, + { val: "SBC914", text: "철, 철근" }, + { val: "SBC915", text: "칼라콘크리트" }, + { val: "SBC916", text: "플라스틱" }, + { val: "SBC917", text: "PVC" }, + { val: "SBC918", text: "SP" }, + { val: "SBC919", text: "SS" }, + { val: "SBC920", text: "닥타일주철" }, + { val: "SBC921", text: "철근G" }, + { val: "SBC922", text: "폴리에틸렌(PE)" }, + { val: "SBC923", text: "회주철" } +]; +// 하수맨홀 용도 코드표 +export const MS_SWAGE_MHL_USE_CODES = [ + { val: "SMU000", text: "미분류" }, + { val: "SMU001", text: "우수" }, + { val: "SMU002", text: "오수" }, + { val: "SMU003", text: "집수" }, + { val: "SMU004", text: "합류" }, + { val: "SMU005", text: "차집" }, + { val: "SMU999", text: "기타" }, + { val: "SMU900", text: "1호" }, + { val: "SMU901", text: "2호" }, + { val: "SMU902", text: "3호" }, + { val: "SMU903", text: "4호" }, + { val: "SMU904", text: "5호" }, + { val: "SMU905", text: "6호" }, + { val: "SMU906", text: "7호" }, + { val: "SMU907", text: "8호" }, + { val: "SMU908", text: "구거용맨홀" }, + { val: "SMU909", text: "부관부착맨홀" }, + { val: "SMU910", text: "조립식맨홀" }, + { val: "SMU911", text: "특1호" }, + { val: "SMU912", text: "특2호" }, + { val: "SMU913", text: "특3호" }, + { val: "SMU914", text: "특4호" }, + { val: "SMU915", text: "특5호" }, + { val: "SMU916", text: "하수맨홀용도" }, + { val: "SMU917", text: "현장타설관거용" } +]; + +// 우수펌프장 +export const ExclnSewagePumpCol = [ + // { id: 'fid', title: 'FID' }, + // { id: 'geom', title: 'geometry' }, + // { id: 'ftreSmbolCd', title: '지형지물부호코드' }, + { id: 'fclt_mng_no', title: '시설관리번호' }, + // { id: 'pbadmsEmdCd', title: '행정읍면동코드' }, + { id: 'mapsht_no', title: '도엽번호' }, + // { id: 'mngInstCd', title: '관리기관코드' }, + { id: 'instl_ymd', title: '설치일자' }, + { id: 'swage_pmstn_nm', title: '하수펌프장명' }, + { id: 'plln_area', title: '부지면적(m²)' }, + // { id: 'opngSttsCd', title: '개통상태코드' }, + // { id: 'usgCd', title: '용도코드' }, + { id: 'daily_prcs_cpct', title: '일일처리용량(㎥)' }, + { id: 'max_impnd_cpct', title: '최대저수용량(㎥)' }, + { id: 'altd_vl', title: '표고(EL.m)' }, + { id: 'wtlv_vl', title: '수위(m)' }, + { id: 'excln_wpmp_ablt', title: '우수양수능력(m²/d)' }, + // { id: 'instMngNo', title: '기관관리번호' }, +] +export const ExclnSewagePumpCol2 = [ + { id: 'rowNumber', title: '일련번호' }, + // { id: 'fid', title: 'FID' }, + // { id: 'geom', title: 'geometry' }, + // { id: 'ftreSmbolCd', title: '지형지물부호코드' }, + { id: 'fcltMngNo', title: '시설관리번호' }, + // { id: 'pbadmsEmdCd', title: '행정읍면동코드' }, + // { id: 'mapshtNo', title: '도엽번호' }, + // { id: 'mngInstCd', title: '관리기관코드' }, + { id: 'instlYmd', title: '설치일자' }, + { id: 'swagePmstnNm', title: '하수펌프장명' }, + { id: 'pllnArea', title: '부지면적(m²)' }, + // { id: 'opngSttsCd', title: '개통상태코드' }, + // { id: 'usgCd', title: '용도코드' }, + { id: 'dailyPrcsCpct', title: '일일처리용량(㎥)' }, + { id: 'max_impndCpct', title: '최대저수용량(㎥)' }, + { id: 'altdVl', title: '표고(EL.m)' }, + { id: 'wtlvVl', title: '수위(m)' }, + { id: 'exclnWpmpAblt', title: '우수양수능력(m²/d)' }, + // { id: 'instMngNo', title: '기관관리번호' }, +] +// 오수펌프장 +export const DrtwSewagePumpCol = [ + // { id: 'fid', title: 'FID' }, + // { id: 'geom', title: 'geometry' }, + // { id: 'ftreSmbolCd', title: '지형지물부호코드' }, + { id: 'fclt_mng_no', title: '시설관리번호' }, + // { id: 'pbadmsEmdCd', title: '행정읍면동코드' }, + { id: 'mapsht_no', title: '도엽번호' }, + // { id: 'mngInstCd', title: '관리기관코드' }, + { id: 'instl_ymd', title: '설치일자' }, + { id: 'swage_pmstn_nm', title: '하수펌프장명' }, + { id: 'plln_area', title: '부지면적(m²)' }, + // { id: 'opngSttsCd', title: '개통상태코드' }, + // { id: 'usgCd', title: '용도코드' }, + { id: 'daily_prcs_cpct', title: '일일처리용량(㎥)' }, + { id: 'max_impnd_cpct', title: '최대저수용량(㎥)' }, + { id: 'altd_vl', title: '표고(EL.m)' }, + { id: 'wtlv_vl', title: '수위(m)' }, + { id: 'excln_wpmp_ablt', title: '오수양수능력(m²/d)' }, + // { id: 'instMngNo', title: '기관관리번호' }, +] +export const DrtwSewagePumpCol2 = [ + { id: 'rowNumber', title: '일련번호' }, + // { id: 'fid', title: 'FID' }, + // { id: 'geom', title: 'geometry' }, + // { id: 'ftreSmbolCd', title: '지형지물부호코드' }, + { id: 'fcltMngNo', title: '시설관리번호' }, + // { id: 'pbadmsEmdCd', title: '행정읍면동코드' }, + { id: 'mapshtNo', title: '도엽번호' }, + // { id: 'mngInstCd', title: '관리기관코드' }, + { id: 'instlYmd', title: '설치일자' }, + { id: 'swagePmstnNm', title: '하수펌프장명' }, + { id: 'pllnArea', title: '부지면적(m²)' }, + // { id: 'opngSttsCd', title: '개통상태코드' }, + // { id: 'usgCd', title: '용도코드' }, + { id: 'dailyPrcsCpct', title: '일일처리용량(㎥)' }, + { id: 'max_impndCpct', title: '최대저수용량(㎥)' }, + { id: 'altdVl', title: '표고(EL.m)' }, + { id: 'wtlvVl', title: '수위(m)' }, + { id: 'exclnWpmpAblt', title: '오수양수능력(m²/d)' }, + // { id: 'instMngNo', title: '기관관리번호' }, +] +// 펌프장 용도 코드표 +export const DRTW_SEWAGE_PUMP_USE_CODES = [ + { val: "SBE000", text: "미분류" }, + { val: "SBE001", text: "빗물펌프장" }, + { val: "SBE002", text: "오수펌프장" }, + { val: "SBE003", text: "처리장내펌프장" }, + { val: "SBE900", text: "오수배제" }, + { val: "SBE901", text: "우수배제" }, + { val: "SBE999", text: "기타" }, +]; + +// 우수토실 +export const stofcbrCol = [ + // { id: 'ftre_smbol_cd', title: '지형지물부호코드' }, + { id: 'fclt_mng_no', title: '시설관리번호' }, + // { id: 'pbadms_emd_cd', title: '행정읍면동코드' }, + { id: 'mapsht_no', title: '도엽번호' }, + // { id: 'mng_inst_cd', title: '관리기관코드' }, + { id: 'instl_ymd', title: '설치일자' }, + // { id: 'fclty_shp_cd', title: '시설물형태코드' }, + { id: 'worflw_wall_hgt', title: '월류벽높이(m)' }, + // { id: 'mhl_exst_yn_cd', title: '맨홀존재여부코드' }, + // { id: 'swage_mhl_ftre_smbol_cd', title: '하수맨홀지형지물부호코드' }, + { id: 'swage_mhl_mng_no', title: '하수맨홀관리번호' }, + { id: 'drtn_angl', title: '방향각도(°)' }, +] +export const stofcbrCol2 = [ + { id: 'rowNumber', title: '일련번호' }, + // { id: 'ftreSmbolCd', title: '지형지물부호코드' }, + { id: 'fcltMngNo', title: '시설관리번호' }, + // { id: 'pbadmsEmdCd', title: '행정읍면동코드' }, + { id: 'mapshtNo', title: '도엽번호' }, + // { id: 'mngInstCd', title: '관리기관코드' }, + { id: 'instlYmd', title: '설치일자' }, + // { id: 'fcltyShpCd', title: '시설물형태코드' }, + { id: 'worflwWallHgt', title: '월류벽높이(m)' }, + // { id: 'mhlExstYnCd', title: '맨홀존재여부코드' }, + // { id: 'swageMhlFtreSmbolCd', title: '하수맨홀지형지물부호코드' }, + { id: 'swageMhlMngNo', title: '하수맨홀관리번호' }, + { id: 'drtnAngl', title: '방향각도(°)' }, +] + +// 중점우수토실 측정지점 +export const untrtSwgRainCol1 = [ + { id: 'manage_no', title: '관리번호' }, + { id: 'swr_ppl_eqp_nm', title: '처리구역' }, + { id: 'swr_ppl_trt_nm', title: '처리분구' }, + { id: 'addr', title: '인근주소' }, + { id: 'chmbr_xcnts', title: '토실 위도' }, + { id: 'chmbr_ydnts', title: '토실 경도' } +]; + +// 중점우수토실 측정지점 시설현황 +export const untrtSwgRainCol2 = [ + { id: 'flwmtr_mesure_tp_etc', title: '구분' }, + { id: 'discharge_pipe_spec', title: '방류관로 규격(m)' }, + { id: 'swr_ppl_eqp_ar', title: '배수구역 면적(m²)' }, + { id: 'swr_trt_popl', title: '하수처리 인구(인)' }, + { id: 'screen_instl_at', title: '스크린설치 여부' }, + { id: 'weir_hg', title: '위어 높이(m)' }, + { id: 'weir_lt', title: '위어 길이(m)' } +]; + +// 중점우수토실 강우정보 (id값 틀릴 수 있음) +export const untrtSwgRainCol3 = [ + { id: 'rainBeginDt', title: '강우시작일시' }, + { id: 'rainEndDt', title: '강우종료일시' }, + { id: 'precdngDry', title: '선행건기(h)' }, + { id: 'rainInfo', title: '강우정보' }, + { id: 'rainFall', title: '누적강우량(mm)' }, + { id: 'cntncTime', title: '지속시간(h)' }, + { id: 'rainIn', title: '강우강도(mm/h)' } +]; + +// 중점우수토실 측정정보 (id값 틀릴 수 있음) +export const untrtSwgRainCol4 = [ + { id: 'mesureBeginDt', title: '측정시작일시' }, + { id: 'mesureEndDt', title: '측정종료일시' }, + { id: 'mesureIntrvl', title: '측정간격(분)' }, + { id: 'mesureMhrlsInstlLc', title: '측정기기 설치위치' }, + { id: 'flwmtrCombined', title: '유량계 종류 및 모델명' }, // 합쳐진 컬럼 + { id: 'qltwtrBeginDt', title: '채수시작일시' }, + { id: 'qltwtrEndDt', title: '채수종료일시' }, + { id: 'qltwtrCo', title: '채수횟수' } +]; + +// 중점우수토실 수량/수질측정 정보 +export const untrtSwgRainCol5 = [ + { id: 'odr', title: '회차' }, + { id: 'mesureDt', title: '측정일자' }, + { id: 'mesureQy', title: '수량(m)' }, + { id: 'mesureBod', title: 'BOD(mg/L)' }, + { id: 'mesureToc', title: 'TOC(mg/L)' }, + { id: 'mesureLoad', title: '오염부하량(kg)' } +]; + +// 🎯 날짜 필드들 (yyyymmdd -> yyyy년 mm월 dd일) +export const dateFields = [ + 'instl_ymd', 'last_drdgn_ymd', 'cmcn_ymd', 'oprtng_strt_ymd', 'clsg_ymd', + 'mesure_begin_dt', 'mesure_end_dt', 'qltwtr_begin_dt', 'qltwtr_end_dt', + 'rain_begin_dt', 'rain_end_dt', 'mesureDt', + // 🎯 camelCase 버전 추가 + 'instlYmd', 'lastDrdgnYmd', 'cmcnYmd', 'oprtngStrtYmd', 'clsgYmd', + 'mesureBeginDt', 'mesureEndDt', 'qltwtrBeginDt', 'qltwtrEndDt', + 'rainBeginDt', 'rainEndDt', + 'flfmtBgngYmd', 'flfmtEndYmd', 'flfmtYr' +]; + +// 🎯 연월 필드들 (yyyymm -> yyyy년 m월) +export const yearMonthFields = ['aplcnYm',]; + +// 🎯 날짜시간 필드들 (yyyy-mm-dd hh:mm:ss -> yyyy년 m월 d일 hh:mm) +export const dateTimeFields = ['regDt',]; + +// 🎯 숫자 필드들 (천 단위 구분자) +export const numberFields = [ + 'pdmt', 'prlg', 'avg_grdn', 'drtw_mltv_area', 'excln_mltv_area', 'tmpt_dpth', 'scend_dpth', 'trfln_cnt', 'vrtc_len', 'wdth_len', + 'fclt_cpct', 'plln_area', 'prcs_zone_area_sum', 'swtrmt_ppltn_cnt', 'stty_crtr_bod', 'stty_crtr_toc', 'stty_crtr_ss', 'stty_crtr_tn', 'stty_crtr_tph', 'stty_crtr_clbcl_cnt', + 'swage_mh_diam', 'wdth_len', 'vrtc_len', 'altd_vl', 'dpthlVl', + 'plln_area', 'daily_prcs_cpct', 'max_impnd_cpct', 'altd_vl', 'wtlv_vl', 'excln_wpmp_ablt', + 'worflw_wall_hgt', + 'swr_ppl_eqp_ar', 'swr_trt_popl', 'weir_hg', 'weir_lt', + 'precdng_dry', 'rain_fall', 'cntnc_time', 'rain_in', + 'mesure_intrvl', 'qltwtr_co', 'mesureQy', 'mesureBod', 'mesureToc', 'mesureLoad', + // 🎯 camelCase 버전 추가 + 'avgGrdn', 'drtwMltvArea', 'exclnMltvArea', 'tmptDpth', 'scendDpth', 'trflnCnt', 'vrtcLen', 'wdthLen', + 'fcltCpct', 'pllnArea', 'prcsZoneAreaSum', 'swtrmtPpltnCnt', 'sttyCltrBod', 'sttyCltrToc', 'sttyCltrSs', 'sttyCltrTn', 'sttyCltrTph', 'sttyCltrClbclCnt', + 'swageMhDiam', 'altdVl', 'dpthlVl', + 'dailyPrcsCpct', 'maxImpndCpct', 'wtlvVl', 'exclnWpmpAblt', + 'worflwWallHgt', + 'swrPplEqpAr', 'swrTrtPopl', 'weirHg', 'weirLt', + 'precdngDry', 'rainFall', 'cntncTime', 'rainIn', + 'mesureIntrvl', 'qltwtrCo', 'rnysnFvlc', 'clarFvlc', + 'bizAmt', 'count' +]; \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx new file mode 100644 index 000000000..08443c73f --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx @@ -0,0 +1,165 @@ +'use client' +import { VerticalTable } from '@/components/ui/verticalTable'; +import React, { useState, useEffect } from 'react' +import InputSection from '../InputSection'; +import { handleInputChange } from '../common'; +import { getData } from "@/components/lib/api"; +// import { useBizByFcltStore } from "../../../../store/StoreProvider"; + +// 하수관로 +const inputColumn = [ + { id: "instlMbdyCd", title: "설치주제", type: "select", options: ["BTL", "재정사업"] }, + { id: "swtrmtZoneCd", title: "처리구역", type: "input" }, + { id: "mltvZoneNm", title: "배수구역", type: "input" }, + { id: "mltvDtdvNm", title: "배수분구", type: "input" }, + { id: "lineCd", title: "지선/간선", type: "select", options: ["지선", "간선"] }, + { id: "floCd", title: "흐름방식", type: "select", options: ["자연유하", "압송"] }, + { id: "layCd", title: "매설위치", type: "select", options: ["도로", "하상"] }, +]; + +type operationDataType = { year: string; type: string, bizNm: string, bizType: string }; + +const MsSwagePpl = ({ featureData, onSaveClick }: { featureData: any, onSaveClick?: () => void }) => { + const [formData, setFormData] = useState<any>({}); + const [repairData, setRepairData] = useState<operationDataType[]>([]); + const [cleaningData, setCleaningData] = useState<operationDataType[]>([]); + const [inspectionData, setInspectionData] = useState<operationDataType[]>([]); + const dataMap = { + repair: { data: repairData, setData: setRepairData }, + cleaning: { data: cleaningData, setData: setCleaningData }, + inspection: { data: inspectionData, setData: setInspectionData }, + }; + + + // const BIZ_SE_CD = useBizByFcltStore(state => state.BIZ_SE_CD); + const BIZ_SE_CD = [ + { lwpoCd: "100", lwpoCdNm: "관로기술진단" }, + { lwpoCd: "200", lwpoCdNm: "개대체" }, + { lwpoCd: "201", lwpoCdNm: "유지보수" }, + { lwpoCd: "203", lwpoCdNm: "청소, 준설" }, + { lwpoCd: "204", lwpoCdNm: "단순보수" }, + ]; + // const BIZ_TY_CD = useBizByFcltStore(state => state.BIZ_TY_CD); + const BIZ_TY_CD = [ + { lwpoCd: "SV", lwpoCdNm: "용역" }, + { lwpoCd: "SF", lwpoCdNm: "자체" }, + { lwpoCd: "SC", lwpoCdNm: "공사" }, + ]; + + // 마운트 및 업데이트 시 API 호출 + + useEffect(() => { + if (featureData) { + setFormData(featureData); + } + + const fetchAll = async () => { + const operationData = await getData(`v1/bizInfo/bizByFclt?mngInstCd=${featureData?.mng_inst_cd}&fcltMngNo=${featureData?.fclt_mng_no}&page=0&size=1`); + + let inspectionArr: operationDataType[] = []; + let cleaningArr: operationDataType[] = []; + let repairArr: operationDataType[] = []; + + if (Array.isArray(operationData)) { + operationData.forEach(item => { + if (!item.flfmt_yr) return; + // biz_se_cd에 해당하는 type명 찾기 + const seCodeObj = BIZ_SE_CD.find(code => code.lwpoCd === item.biz_se_cd); + const tyCodeObj = BIZ_TY_CD.find(code => code.lwpoCd === item.biz_ty_cd); + const seTypeName = seCodeObj ? seCodeObj.lwpoCdNm : ""; + const tyTypeName = tyCodeObj ? tyCodeObj.lwpoCdNm : ""; + switch (item.biz_se_cd) { + case "100": + inspectionArr.push({ + year: String(item.flfmt_yr), + type: seTypeName, + bizNm: String(item.biz_nm), + bizType: tyTypeName, + }); + break; + case "203": + cleaningArr.push({ + year: String(item.flfmt_yr), + type: seTypeName, + bizNm: String(item.biz_nm), + bizType: tyTypeName, + }); + break; + case "200": + case "201": + case "204": + repairArr.push({ + year: String(item.flfmt_yr), + type: seTypeName, + bizNm: String(item.biz_nm), + bizType: tyTypeName, + }); + break; + } + }); + } + + setInspectionData(inspectionArr); + setCleaningData(cleaningArr); + setRepairData(repairArr); + console.log('inspectionArr', inspectionArr); + console.log('cleaningArr', cleaningArr); + console.log('repairArr', repairArr); + }; + fetchAll(); + }, [featureData]); + + // 폼 데이터가 변경될 때 호출될 핸들러 + const handleFormDataChange = (newFormData: any) => { + setFormData(newFormData); + }; + + // 저장 버튼 클릭 시 최신 formData를 부모에게 전달 + const handleSaveClick = () => { + if (onSaveClick) { + // 저장 전에 formData를 부모 컴포넌트의 handleSaveClick에 전달 + onSaveClick(); + } + }; + + + return ( + <> + <VerticalTable + headName='입력정보' + tableData={featureData ? [formData] : []} + headlist={inputColumn} + pageSize={20} + total={0} + cellClick={undefined} + isEditable={true} + onPageChange={function (page: number): void { + throw new Error("Function not implemented."); + }} + showSaveButton={true} + onSaveClick={handleSaveClick} + onFormDataChange={handleFormDataChange} // 폼 데이터 변경 콜백 추가 + /> + <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)} + /> + </> + ) +} + +export default MsSwagePpl \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/Ssc.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/Ssc.tsx new file mode 100644 index 000000000..52fc5523d --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/Ssc.tsx @@ -0,0 +1,237 @@ +// 우수토실 +import { UiModal, UiTable } from '@common/business_components/ui'; +import { Button, TableHead, TableRow } from '@common/components/ui'; +import React, { useState, useEffect, useCallback } from 'react'; +import { untrtSwgRainCol3, untrtSwgRainCol4, untrtSwgRainCol5 } from '../common/table'; +import { VerticalTable } from '@/components/ui/verticalTable'; +import { getData } from "@/components/lib/api"; +import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue'; + +const inputColumn = [ + { id: "prrtymngYn", title: "중점여부", type: "select", options: ["우수토실", "중점우수토실"] }, + { id: "swtrmtZoneCd", title: "처리구역", type: "input" }, +]; + +const Ssc = ({ featureData, isMainSsc, rainMeasureData, onSaveClick }: { + featureData: any, + isMainSsc?: boolean, + rainMeasureData?: any, + onSaveClick?: () => void +}) => { + const [isModalOpen, setIsModalOpen] = useState(false); + const [isDataLoaded, setIsDataLoaded] = useState(false); + const [formData, setFormData] = useState<any>({}); + const [mntrngsData, setMntrngsData] = useState<any[]>([]); // 수량/수질 측정 정보 + const [selectedRowIndex, setSelectedRowIndex] = useState<number>(-1); + + // console.log('featureData data received:', featureData); // 추가 데이터 확인용 + // console.log('rainMeasureData received:', rainMeasureData); // 추가 데이터 확인용 + + const openModal = () => { + setIsModalOpen(true); + }; + + const closeModal = () => { + setIsModalOpen(false); + if (!isDataLoaded) { + setTimeout(() => setIsDataLoaded(true), 100); + } + }; + + // featureData가 변경될 때 formData 초기화 + useEffect(() => { + if (featureData) { + setFormData(featureData); + setMntrngsData([]); + } + }, [featureData]); + + // 폼 데이터가 변경될 때 호출될 핸들러 + const handleFormDataChange = (newFormData: any) => { + setFormData(newFormData); + }; + + // 저장 버튼 클릭 시 최신 formData를 부모에게 전달 + const handleSaveClick = () => { + if (onSaveClick) { + // 저장 전에 formData를 부모 컴포넌트의 handleSaveClick에 전달 + onSaveClick(); + } + }; + + // 테이블 셀 클릭 핸들러 추가 + const handleCellClick = async (index: number | undefined, headId: string) => { + if (index !== undefined && rainMeasureData) { + const data = await fetchMntrngsData(rainMeasureData[index].adstDstrCd, rainMeasureData[index].manageNo); + + setMntrngsData(data); + setSelectedRowIndex(index); + } + }; + + // 🎯 추가 데이터 API 호출 함수 (독립적으로 실행) + const fetchMntrngsData = useCallback(async (adstDstrCd: string, manageNo: string) => { + if (!adstDstrCd || !manageNo) { + console.warn('추가 데이터 호출 실패: 필수 매개변수 누락', { adstDstrCd, manageNo }); + return []; + } + + try { + const queryParams = new URLSearchParams({ + pageable: `{}`, + }); + + console.log(`SSC 추가 데이터 API 호출 시작: v1/onsemiro/swgRain?${queryParams.toString()}`); + + const result = await getData(`v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs?${queryParams.toString()}`); + + console.log('SSC 추가 데이터 API 호출 성공:', result); + + // 🎯 result가 페이지네이션 객체인 경우 content 배열 추출 + let dataArray: any[] = []; + if (result && typeof result === 'object' && 'content' in result && Array.isArray(result.content)) { + dataArray = result.content; + } else if (Array.isArray(result)) { + dataArray = result; + } else { + console.warn('예상치 못한 API 응답 형식:', result); + return []; + } + + // 🎯 각 아이템을 전처리하여 반환 + const processedDataArray = dataArray.map((data: any) => { + // 🎯 formatDisplayData로 기본 포맷팅 적용 + const formattedItem = formatDisplayData(data); + + // 🎯 추가 데이터 조합 + return { + ...formattedItem, + }; + }); + + console.log('processedDataArray', processedDataArray); + + return processedDataArray; + } catch (error) { + console.error('추가 데이터 로딩 실패:', error); + return []; + } + }, []); + + const renderModalBody = () => ( + <div className='grid grid-cols-1 gap-4 p-4'> + {/* 강우정보 테이블 */} + <UiTable + headName='강우정보' + tableData={Array.isArray(rainMeasureData) ? rainMeasureData : []} + headlist={untrtSwgRainCol3} + pageSize={10} + total={Array.isArray(rainMeasureData) ? rainMeasureData.length : 0} + cellClick={handleCellClick} + onPageChange={function (page: number): void { + console.log('Rain data page change:', page); + }} + /> + + {/* 측정정보 테이블 */} + <UiTable + headName='측정정보' + tableData={Array.isArray(rainMeasureData) ? rainMeasureData : []} + headlist={untrtSwgRainCol4} + pageSize={10} + total={Array.isArray(rainMeasureData) ? rainMeasureData.length : 0} + cellClick={handleCellClick} + // onPageChange={setCurPage} + onPageChange={function (page: number): void { + console.log('Rain data page change:', page); + }} + resetPageNo={false} + // index={curPage} + > + {/* 커스텀 헤더 */} + <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={Array.isArray(mntrngsData) ? mntrngsData : []} + headlist={untrtSwgRainCol5} + pageSize={10} + total={Array.isArray(mntrngsData) ? mntrngsData.length : 0} + cellClick={undefined} + onPageChange={function (page: number): void { + console.log('Quality data page change:', page); + }} + /> + </div> + ) + + return ( + <div> + {!isMainSsc && ( + <VerticalTable + headName='입력정보' + tableData={featureData ? [formData] : []} + headlist={inputColumn} + pageSize={0} + total={0} + cellClick={undefined} + isEditable={true} + onPageChange={function (): void { + throw new Error('Function not implemented.'); + }} + showSaveButton={true} + onSaveClick={handleSaveClick} + onFormDataChange={handleFormDataChange} // 폼 데이터 변경 콜백 추가 + /> + )} + + {isMainSsc && ( + <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> + + <UiModal + open={isModalOpen} + onOpenChange={setIsModalOpen} + bodyClass={` + h-[100vh] + w-[60vw] + max-w-none + max-h-none + overflow-visible + z-[101] + `} + body={renderModalBody()} + noHeaderYn='Y' + noFooterYn='N' + // footerContent={renderFooterContent()} + /> + </div> + )} + </div> + ) +} + +export default Ssc \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/SwtrmtFclt.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/SwtrmtFclt.tsx new file mode 100644 index 000000000..50d6d5606 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/SwtrmtFclt.tsx @@ -0,0 +1,49 @@ +import Modal from '@/components/ui/modal'; +import { VerticalTable } from '@/components/ui/verticalTable'; +import { UiTable } from '@common/business_components/ui'; +import { Button, TableCell } from '@common/components/ui'; +import React, { useState } from 'react' + +// 하수처리시설 +const headList = [ + { id: "title", title: "하수도정비 기본계획보고서" }, + { id: "cad", title: "하수도대장 평면도" },] +const modalHeadList = [ + { id: "no", title: "번호" }, + { id: "year", title: "승인연도" }, + { id: "cad", title: '평면도' }, + { id: "fileNm", title: "파일명" }, + { id: "date", title: "입력시간" }, +] + +const SwtrmtFclt = () => { + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalTitle, setModalTitle] = useState<string>(''); + const openModal = (title: string) => { + setIsModalOpen(true); + setModalTitle(title) + }; + const closeModal = () => { + setIsModalOpen(false); + }; + return ( + <div> + <VerticalTable children={{ + title: <Button onClick={() => openModal('하수도정비 기본계획보고서')}>보고서</Button>, + cad: <Button onClick={() => openModal('하수도대장 평면도')}>평면도</Button>, + }} headName='디지털 자료관 연계정보' tableData={[]} headlist={headList} pageSize={20} total={0} cellClick={undefined} isEditable={true} onPageChange={function (page: number): void { + throw new Error("Function not implemented."); + }}></VerticalTable> + <Modal isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} closeModal={closeModal}> + <div className='grid grid-col-2'> + <UiTable headName={modalTitle} tableData={[]} headlist={modalHeadList} pageSize={20} total={0} cellClick={undefined} children={undefined} onPageChange={function (page: number): void { + throw new Error("Function not implemented."); + } + }></UiTable> + </div> + </Modal> + </div> + ) +} + +export default SwtrmtFclt \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/aplcnYms.ts b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/aplcnYms.ts new file mode 100644 index 000000000..857749bf1 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/aplcnYms.ts @@ -0,0 +1,287 @@ +import {useState, useEffect} from 'react'; +import { getData } from '@/components/lib/api'; +import moment from "moment"; + +export type TInftwtAnls = { + inftwtAnlsSn:number; + aplcnYm: string; + enoCd: string; + ctpvCd: string; + sggCd: string; + swageTmplntCd: string; + swageTmplntNm: string; + swtrmtZoneCd: string; + swtrmtZoneNm: string; + atflCd: string; + atflId: string; + atflInpNm: string; + atflHrPttnNm: string; + atflMpngNm: string; + atflOilqNm: string; + atflNm: string; + drtwSwtcRt:number; + anlsInfo: string; + scsYn: string; +}; +export type TInftwtAnlsRslt = { + inftwtAnlsSn: number; + pplMngNo: string; + anlsTm: string; + predcOilq:number; +} +export type TRdiiAnls = { + rainInfwtAnlsSn:number; + aplcnYm: string; + enoCd: string; + ctpvCd: string; + sggCd: string; + swageTmplntCd: string; + swageTmplntNm: string; + swtrmtZoneCd: string; + swtrmtZoneNm: string; + atflCd: string; + atflId: string; + atflInpNm: string; + atflHrPttnNm: string; + atflMpngNm: string; + atflRainNm: string; + atflNm: string; + drtwSwtcRt:number; + anlsInfo: string; + scsYn: string; + srcNm: string; // 출처이름 +}; +export type TRdiiAnlsRslt = { + rainInfwtAnlsSn: number; + pplMngNo: string; + anlsTm: string; + predcOilq:number; + minq:number; +} +export type TOilqData = { + inftwtAnlsSn: number; // 침입수분석 일련번호 + pplMngNo: string; // 관로관리번호 + mtrngDt: string; // 계측일시 + oilq: number; // 유량 +} +export type TRainData = { + rainInfwtAnlsSn: number; + mtrngDt: string; + amrf: number; // 강우량 +} + +function roundTo(n: number, digits: number): number { + const factor = Math.pow(10, digits); + return Math.round(n * factor) / factor; +} + +const getOptionsAplcnYm = (yyyymms: string[]): { text: string; val: string, hidden?: boolean }[] => { + const yyyymms1 : { text: string; val: string; hidden?: boolean }[] = yyyymms.map(dateStr => { + const year = dateStr.slice(0, 4); + const month = dateStr.slice(4, 6); + return { + text: `${year}-${month}`, + val: dateStr + }; + }); + yyyymms1.unshift({ val: '0', text: '선택', hidden: true }); + return yyyymms1; +}; +const selectAplcnYms = async(gubun:string) => { // 상수사용월 + try { + const queryString = ''; + const result = await getData(`v1/${gubun}/aplcnYm${queryString ? `?${queryString}` : ''}`); + if (result && result.length > 0) { + console.log("selectAplcnYms", result); + return getOptionsAplcnYm(result); + } + } catch (error) { + console.error(error); + } +}; + +export const useAplcnYms = (gubun:string) => { + const [data, setData] = useState<{ text: string; val: string }[]>([]); + + useEffect(() => { + const fetch = async (gubun:string) => { + const result = await selectAplcnYms(gubun); + if (result) setData(result); + }; + fetch(gubun); + }, [gubun]); + + return data; +}; +export const getOptionsInftwtAnlsInfo = (anlsArr: TInftwtAnls[]): { text: string; val: number, hidden?: boolean }[] => { + const options : { text: string; val: number; hidden?: boolean }[] = anlsArr.map(anls => ({ + text: anls.anlsInfo, + val: anls.inftwtAnlsSn, + })); + options.unshift({ val: 0, text: '선택', hidden: true }); + return options; +}; + +export const selectInftwtAnls = async(aplcnYm:string) => { // 침입수분석 + try { + const queryString = aplcnYm ? `aplcnYmFrom=${aplcnYm}&aplcnYmTo=${aplcnYm}` : ''; + const result = await getData(`v1/inftwtAnls${queryString ? `?${queryString}` : ''}`); + if (result && result.numberOfElements > 0) { + console.log("selectInftwtAnls", result.content); + return result.content; + } + } catch (error) { + console.error(error); + } +}; +export const getChartDataInftwtAnlsRslt = (arr: TInftwtAnlsRslt[]): { label: string; val: number }[] => { + const options = arr.map(anlsRslt => ({ + label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("HH시"), + val: roundTo(anlsRslt.predcOilq, 2), + })); + return options; +}; +export const selectInftwtAnlsRslt = async(inftwtAnlsSn:number, pplMngNo:string) => { // 침입수분석 결과 + try { + if ((inftwtAnlsSn === undefined || inftwtAnlsSn === 0) || !pplMngNo) { + console.warn("inftwtAnlsSn or pplMngNo is not provided"); + return []; + } + const result = await getData(`v1/inftwtAnlsRslt/ppl/${inftwtAnlsSn}/${pplMngNo}`); + if (result && result.numberOfElements > 0) { + console.log("selectInftwtAnlsRslt", result.content); + return result.content; + } + } catch (error) { + console.error(error); + } +}; +export const getChartDataOilq = (arr: TOilqData[]): { label: string; val: number }[] => { + const options = arr.map(anlsRslt => ({ + label: anlsRslt.mtrngDt, + val: roundTo(anlsRslt.oilq, 2), + })); + return options; +}; +export const selectOilq = async(inftwtAnlsSn:number, pplMngNo:string) => { // 유량 + try { + if ((inftwtAnlsSn === undefined || inftwtAnlsSn === 0) || !pplMngNo) { + console.warn("inftwtAnlsSn or pplMngNo is not provided"); + return []; + } + const result = await getData(`v1/inftwtAnlsRslt/oilqPpl/${inftwtAnlsSn}/${pplMngNo}`); + if (result && result.numberOfElements > 0) { + console.log("selectOilq", result.content); + return result.content; + } + } catch (error) { + console.error(error); + } +}; +export const selectPplInftwtQty = async(inftwtAnlsSn:number, pplMngNo:string) => { // 침입수량 + try { + if ((inftwtAnlsSn === undefined || inftwtAnlsSn === 0) || !pplMngNo) { + console.warn("inftwtAnlsSn or pplMngNo is not provided"); + return []; + } + const result = await getData(`v1/inftwtAnlsRslt/pplInftwtQty/${inftwtAnlsSn}/${pplMngNo}`); + if (result.length > 0) { + console.log("selectPplInftwtQty", result); + return result[0].avg_day_flow; + } else { + return 0; // 데이터가 없을 경우 0 반환 + } + } catch (error) { + console.error(error); + } +}; +//---------- +export const selectPplRdiiQty = async(rainInfwtAnlsSn:number, pplMngNo:string) => { // rdii값 + try { + if ((rainInfwtAnlsSn === undefined || rainInfwtAnlsSn === 0) || !pplMngNo) { + console.warn("rainInfwtAnlsSn or pplMngNo is not provided"); + return []; + } + const result = await getData(`v1/rdiiAnlsRslt/pplRdiiQty/${rainInfwtAnlsSn}/${pplMngNo}`); + if (result?.sustaintime > 0) { + console.log("selectPplRdiiQty", result); + return result; + } + } catch (error) { + console.error(error); + } +}; + +export const getOptionsRdiiAnlsInfo = (anlsArr: TRdiiAnls[]): { text: string; val: number, hidden?: boolean }[] => { + const options : { text: string; val: number; hidden?: boolean }[] = anlsArr.map(anls => ({ + text: anls.anlsInfo, + val: anls.rainInfwtAnlsSn, + })); + options.unshift({ val: 0, text: '선택', hidden: true }); + return options; +}; + +export const selectRdiiAnls = async(aplcnYm:string) => { // 침입수분석 + try { + const queryString = aplcnYm ? `aplcnYmFrom=${aplcnYm}&aplcnYmTo=${aplcnYm}` : ''; + const result = await getData(`v1/rdiiAnls${queryString ? `?${queryString}` : ''}`); + if (result && result.numberOfElements > 0) { + console.log("selectRdiiAnls", result.content); + return result.content; + } + } catch (error) { + console.error(error); + } +}; +export const getChartDataRdiiAnlsRslt = (arr: TRdiiAnlsRslt[]): { label: string; lineVal0: number, barVal0:number }[] => { + const options = arr.map(anlsRslt => ({ + label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("MM-DD HH시"), + lineVal0: roundTo(anlsRslt.predcOilq, 2), + barVal0: 0 + })); + return options; +}; +export const getChartDataRdiiAnlsRsltMinq = (arr: TRdiiAnlsRslt[]): { label: string; val: number }[] => { + const options = arr.map(anlsRslt => ({ + label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("MM-DD HH시"), + val: roundTo(anlsRslt.minq, 2), + })); + return options; +}; +export const selectRdiiAnlsRslt = async(rainInfwtAnlsSn:number, pplMngNo:string) => { // RDII분석 결과 + try { + if ((rainInfwtAnlsSn === undefined || rainInfwtAnlsSn === 0) || !pplMngNo) { + console.warn("RdiiAnlsSn or pplMngNo is not provided"); + return []; + } + const result = await getData(`v1/rdiiAnlsRslt/ppl/${rainInfwtAnlsSn}/${pplMngNo}`); + if (result && result.numberOfElements > 0) { + return result.content; + } + } catch (error) { + console.error(error); + } +}; +export const selectRainData = async(rainInfwtAnlsSn:number) => { // 강우데이터 + try { + if ((rainInfwtAnlsSn === undefined || rainInfwtAnlsSn === 0)) { + console.warn("RdiiAnlsSn or pplMngNo is not provided"); + return []; + } + const result = await getData(`v1/rdiiAnlsRslt/rainData/${rainInfwtAnlsSn}`); + if (result && result.numberOfElements > 0) { + console.log("selectRainData", result.content); + return result.content; + } + } catch (error) { + console.error(error); + } +}; +export const getChartDataRainData = (arr: TRainData[]): { label: string; lineVal0: number, barVal0: number }[] => { + const options = arr.map(data => ({ + label: moment(data.mtrngDt, "YYYY-MM-DD HH:mm:ss").format("MM-DD HH시"), + lineVal0: 0, + barVal0: roundTo(data.amrf,2), + })); + return options; +}; \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx new file mode 100644 index 000000000..542156c2a --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx @@ -0,0 +1,287 @@ +'use client' +import { CHART_FORM, type ChartConfig, MainChart, UiModal } from "@common/business_components"; +import { SelectBox } from '@common/business_components/ui'; +import { Button, Input, Label } from '@common/components'; +import React, { useEffect, useState } from 'react'; +import moment from "moment"; + +import type { TInftwtAnls } from './aplcnYms'; +import * as fn from './aplcnYms'; +import { useInfilChartModalStore } from '../chartModalStore'; // 모달 상태 관리 스토어 +type ModalOpenProp = { + pplMngNo?: string, // 관로관리번호 +} +type DataType = { label: string; val: number }; + +const InfilChart = ({ pplMngNo }: ModalOpenProp) => { + const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useInfilChartModalStore(); // 모달 상태 관리 스토어 + const [popupOpen, setPopupOpen] = React.useState(false); // 조회조건팝업 + const buttonRef = React.useRef<HTMLButtonElement>(null); // 버튼 참조 + const [popupPosition, setPopupPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 }); // 팝업 위치 + const [selectedAplcnYm, setSelectedAplcnYm] = useState(""); + const [inftwtAnlss, setInftwtAnlss] = useState<TInftwtAnls[]>([]); + const [inftwtAnlsInfos, setInftwtAnlsInfos] = useState<{ text: string; val: number; hidden?: boolean }[]>([{ val: 0, text: '선택' }]); // 침입수분석정보 options + const [inftwtAnls, setInftwtAnls] = useState<TInftwtAnls | null>(null); // 침입수분석정보 + const [selectedInftwtAnls, setSelectedInftwtAnls] = useState<number>(0); // 침입수분석 정보 선택시 사용 + const [chartData, setChartData] = useState<{ chartData1: DataType[]; chartData2: DataType[] }>({ + chartData1: [], chartData2: [], + }); + const [inftwtQty, setInftwtQty] = useState<string>("0"); // 침입수량 + const aplcnYms = fn.useAplcnYms('inftwtAnlsRslt'); // 상수사용월 데이터 가져오기 + + pplMngNo = "L" + pplMngNo; // inp파일에 하수관로와 맨홀의 번호가 같으면 분석 오류가 발행하여 + const openPopup = () => { + if (buttonRef.current) { + const rect = buttonRef.current.getBoundingClientRect(); // 버튼 위치 계산 + setPopupPosition({ + top: rect.bottom + window.scrollY, // 버튼 아래쪽 + left: rect.left + window.scrollX, // 버튼 왼쪽 + }); + } + setPopupOpen(true); // 팝업 열기 + }; + + const chartForm = CHART_FORM.LINE; + const chartConfig = { + chartData1: { label: '순오수량', color: "red", }, + chartData2: { label: '실측하수량', color: "orange", } + } as ChartConfig; + + useEffect(() => { + if (isModalOpen === false) { + setPopupOpen(true); // 모달이 닫힐 때 조회조건팝업은 열어두기 + setSelectedAplcnYm(""); // 상수사용월 초기화 + setInftwtAnlss([]); // 침입수분석 초기화 + setInftwtAnlsInfos([{ val: 0, text: '선택' }]); // 침입수분석정보 초기화 + setInftwtAnls(null); // 침입수분석정보 초기화 + setSelectedInftwtAnls(0); // 침입수분석 일련번호 초기화 + setChartData({ chartData1: [], chartData2: [], }); // 차트 데이터 초기화 + setInftwtQty("0"); // 침입수량 초기화 + } + }, [isModalOpen]); + + // 상수사용월 변경 핸들러 + const handleAplcnYmChange = async (val: string | number) => { + const selectedValue = String(val); + console.log("선택된 상수사용월:", selectedValue); + + setSelectedAplcnYm(selectedValue); + + // 즉시 관련 상태 초기화 + setInftwtAnls(null); + + // 빈 값이면 초기화만 하고 종료 + if (!selectedValue || selectedValue === "0" || selectedValue === "") { + setInftwtAnlsInfos([{ val: 0, text: '선택' }]); + return; + } + + // API 호출 + try { + const data = await fn.selectInftwtAnls(selectedValue); + setInftwtAnlss(data ?? []); + setInftwtAnlsInfos(fn.getOptionsInftwtAnlsInfo(data)); + } catch (error) { + console.error("selectInftwtAnls 에러:", error); + setInftwtAnlss([]); + setInftwtAnlsInfos([{ val: 0, text: '선택' }]); + } + setSelectedInftwtAnls(0); + }; + + // 분석정보 변경 핸들러 + const handleInftwtAnlsSnChange = (val: string | number) => { + const selectedValue = Number(val); + setSelectedInftwtAnls(selectedValue); + + // 선택된 침입수분석 정보에 대한 처리 로직 + if (selectedValue === 0) { + setInftwtAnls(null); + return; + } + + const selectedAnls = inftwtAnlss.find(anls => anls.inftwtAnlsSn === selectedValue); + if (selectedAnls) { + setInftwtAnls(selectedAnls); + } + }; + + const closePopup = () => { // 팝업 닫기 + setPopupOpen(false); + } + const infilRsltChart = () => { // 그래프 & 팝업 닫기 + + fn.selectInftwtAnlsRslt(selectedInftwtAnls, pplMngNo).then((data) => {// 침입수분석 결과 조회 + const rsltData = fn.getChartDataInftwtAnlsRslt(data ?? []); + setChartData(prev => ({ + ...prev, + chartData1: rsltData // 침입수분석결과 + })); + }); + fn.selectOilq(selectedInftwtAnls, pplMngNo).then((data) => {// 유량 조회 + const rsltData = fn.getChartDataOilq(data ?? []); + setChartData(prev => ({ + ...prev, + chartData2: rsltData // 유량 + })); + }).catch((err) => { + console.error("selectOilq 에러:", err); + setChartData(prev => ({ + ...prev, + chartData2: [] // 유량 데이터 초기화 + })); + }); + fn.selectPplInftwtQty(selectedInftwtAnls, pplMngNo).then((data) => {// 침입수량 조회 + if (data > 0) { + setInftwtQty(data.toLocaleString('ko-KR')); + } + }); + setPopupOpen(false); + }; + + const renderBody = () => ( + <> + <div className="text-black text-3xl font-bold mb-5">침입수분석결과</div> + + <div className='grid grid-cols-[1fr_3fr] gap-4'> + <div className="h-[90%] grid grid-rows-4 gap-4 mb-4"> + <Button className="bg-white text-white w-[130px] font-bold px-4 mt-2 py-2" onClick={openPopup}> + 분석 입력현황 조회 + </Button> + {/* 상수사용량 적용연월 */} + <div className="bg-white px-4 py-2 "> + <span className="text-2xl text-red-600 font-bold">● 상수사용량 적용연월</span> + <div className="flex items-center mt-2"> + <span className="ml-5 text-black text-xl font-bold"> + {(selectedAplcnYm) ? moment(selectedAplcnYm, "YYYYMM").format("YYYY-MM") : ""} + </span> + </div> + </div> + {/* 침입수량 분석결과 */} + <div className="bg-white px-4 py-2 "> + <span className="text-2xl text-red-600 font-bold">● 침입수량 산정결과</span> + <div className="flex items-center mt-2"> + <span className="ml-5 text-black text-xl">침입수량 : </span> + <span className="w-[45px] inline-block text-right text-black text-xl font-bold">{inftwtQty}</span> + <span className="ml-2 text-black text-xl">m³/d</span> + </div> + </div> + </div> + {/* 차트 */} + <div className="h-[90%] bg-white text-3xl"> + <MainChart chartConfig={chartConfig} chartForm={chartForm} chartData={chartData} chartTitle={'침입수량 그래프'} + yFieldText2="" yFieldText="유량(m³/h)"/> + </div> + </div> + {popupOpen && ( + <div + className="absolute bg-gray-100 border border-gray-300 w-[440px] shadow-lg p-6 rounded text-2xl" + style={{ + top: popupPosition.top, + left: popupPosition.left, + zIndex: 1000, + }} + > + <div className="font-bold mb-6 text-3xl">침입수분석 입력현황</div> + <div className='flex flex-col'> + <div className='flex items-center justify-between'> + <Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">상수사용월:</Label> + <SelectBox + className="w-[220px] mr-8 z-[51] mb-2" + selectClass="rounded-lg bg-white" + label='' + selectArray={aplcnYms} + value={selectedAplcnYm} + onSelectValue={handleAplcnYmChange} + /> + </div> + <div className='flex items-center justify-between'> + <Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">분석정보:</Label> + <SelectBox + className="w-[220px] mr-8 z-[51] mb-2" + selectClass="rounded-lg bg-white" + label='' + selectArray={inftwtAnlsInfos} + value={selectedInftwtAnls} + onSelectValue={handleInftwtAnlsSnChange} + /> + </div> + <div className='flex items-center justify-between'> + <Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">오수전환율:</Label> + <Input + className="mr-8 w-[220px] !text-xl placeholder:text-center mb-2" + placeholder="" + value={inftwtAnls?.drtwSwtcRt ?? ""} + readOnly={true} + /> + </div> + <div className='flex items-center justify-between'> + <Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">시간패턴 파일명:</Label> + <Input + className="mr-8 w-[220px] !text-xl placeholder:text-center mb-2" + placeholder="" + value={inftwtAnls?.atflHrPttnNm ?? ""} + readOnly={true} + /> + </div> + <div className='flex items-center justify-between'> + <Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">수용가-맨홀 매칭:</Label> + <Input + className="mr-8 w-[220px] !text-xl placeholder:text-center mb-2" + placeholder="" + value={inftwtAnls?.atflMpngNm ?? ""} + readOnly={true} + /> + </div> + <div className='flex items-center justify-between'> + <Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">관망도 파일명:</Label> + <Input + className="mr-8 w-[220px] !text-xl placeholder:text-center mb-2" + placeholder="" + value={inftwtAnls?.atflInpNm ?? ""} + readOnly={true} + /> + </div> + <div className='flex items-center justify-between'> + <Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">유량 파일명:</Label> + <Input + className="mr-8 w-[220px] !text-xl placeholder:text-center mb-2" + placeholder="" + value={inftwtAnls?.atflOilqNm ?? ""} + readOnly={true} + /> + </div> + </div> + <div className="flex justify-end mt-4 space-x-2"> + <button className="px-4 py-2 bg-gray-600 text-white rounded" onClick={closePopup}> + 닫기 + </button> + <button className="px-4 py-2 bg-gray-600 text-white rounded" onClick={infilRsltChart}> + 선택조건 적용 + </button> + </div> + </div>)} + {/* </Modal > */} + </> + ) + + return ( + <UiModal + open={isModalOpen} + onOpenChange={setIsModalOpen} + headerTitle="" + bodyClass={` + min-h-[50vh] + w-[55vw] + max-w-none + max-h-none + overflow-visible + z-[50] + `} + body={renderBody()} + noFooterYn='Y' + /> + ) +} + +export default React.memo(InfilChart); \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx new file mode 100644 index 000000000..b788b9cf6 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx @@ -0,0 +1,300 @@ +'use client' +import { CHART_FORM, type ChartConfig, MainChart, UiModal } from "@common/business_components"; +import { SelectBox } from '@common/business_components/ui'; +import { Button, Input, Label } from '@common/components'; +import React, { useEffect, useState } from 'react'; +import moment from "moment"; + +import type { TRdiiAnls } from './aplcnYms'; +import * as fn from './aplcnYms'; +import { useRdiiChartModalStore } from '../chartModalStore'; // 모달 상태 관리 스토어 + +type ModalOpenProp = { + pplMngNo: string, // 관로관리번호 +} +// 차트 +type lineBarChartData = { + label : string; + lineVal0 : number; + barVal0 : number; +} + + +const RdiiChart = ({ pplMngNo }: ModalOpenProp) => { + const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useRdiiChartModalStore(); // 모달 상태 관리 스토어 + const [popupOpen, setPopupOpen] = React.useState(false); // 조회조건팝업 + const buttonRef = React.useRef<HTMLButtonElement>(null); // 버튼 참조 + const [popupPosition, setPopupPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 }); // 팝업 위치 + const [aplcnYm, setAplcnYm] = useState(""); + const [rdiiAnlss, setRdiiAnlss] = useState<TRdiiAnls[]>([]); + const [rdiiAnlsInfos, setRdiiAnlsInfos] = useState<{ text: string; val: number; hidden?: boolean }[]>([{ val: 0, text: '선택' }]); // 강우유입수분석정보 options + const [rdiiAnls, setRdiiAnls] = useState<TRdiiAnls | null>(null); // 강우유입수분석정보 + const [rainInfwtAnlsSn, setRainInfwtAnlsSn] = useState<number>(0); // 강우유입수분석 정보 선택시 사용 + const [chartData, setChartData] = useState<lineBarChartData[]>([]); + const [rdiiQty, setRdiiQty] = useState<string>("0"); // 강우유입수량 + const [rainTotal, setRainTotal] = useState<string>("0"); // 적용강우량 + const [totalVolume, setTotalVolume] = useState<string>("0"); // 총체적 m³/mm + const aplcnYms = fn.useAplcnYms('rdiiAnlsRslt'); // 상수사용월 데이터 가져오기 + pplMngNo = "L"+pplMngNo; // "L13101003"; + + const openPopup = () => { + if (buttonRef.current) { + const rect = buttonRef.current.getBoundingClientRect(); // 버튼 위치 계산 + setPopupPosition({ + top: rect.bottom + window.scrollY, // 버튼 아래쪽 + left: rect.left + window.scrollX, // 버튼 왼쪽 + }); + } + setPopupOpen(true); // 팝업 열기 + }; + + const chartForm = CHART_FORM.LINEBAR2; + const chartConfig = { + barVal0: { + label: '강수량', + color: "#33AAFF", + }, + lineVal0: { + label: 'RDII', + color: "#FF0000", + }, + } as ChartConfig; + + useEffect(() => { + if (isModalOpen === false) { + setPopupOpen(true); // 모달이 닫힐 때 팝업은 + setAplcnYm(""); // 상수사용월 초기화 + setRdiiAnlss([]); // 강우유입수분석 초기화 + setRdiiAnlsInfos([{ val: 0, text: '선택' }]); // 강우유입수분석정보 초기화 + setRdiiAnls(null); // 강우유입수분석정보 초기화 + setRainInfwtAnlsSn(0); // 강우유입수분석 일련번호 초기화 + setChartData([]); // 차트 데이터 초기화 + setRdiiQty("0"); // 강우유입수량 초기화 + setRainTotal("0"); // 적용강우량 초기화 + setTotalVolume("0"); // 총체적 m³/mm 초기화 + } + }, [isModalOpen]); + + // 상수사용월 변경시 분석 정보 리스트 가져오기 + useEffect(() => { + // aplcnYm이 비어있거나 "0"이면 API 호출하지 않음 + if (!aplcnYm || aplcnYm === "0" || aplcnYm === "") { + setRainInfwtAnlsSn(0); // 초기화 + setRdiiAnlsInfos([]); + return; + } + fn.selectRdiiAnls(aplcnYm) + .then((data) => { + setRdiiAnlss(data ?? []); // 선택한 월의 분석성공한 RDII분석데이터[] + setRdiiAnlsInfos(fn.getOptionsRdiiAnlsInfo(data)); + setRdiiAnls(null); // 오수전환율 이후 초기화 + setRainInfwtAnlsSn(0); // 초기화 + }) + .catch((error) => { + console.error("selectrdiiAnls 에러:", error); + setRdiiAnlss([]); + setRdiiAnlsInfos([]); + }); + }, [aplcnYm]); + + // 값 표시 + useEffect(() => { + // 선택된 강우유입수분석 정보에 대한 처리 로직 + if (rainInfwtAnlsSn === 0) return; + const selectedAnls = rdiiAnlss.find(anls => anls.rainInfwtAnlsSn === rainInfwtAnlsSn); + if (selectedAnls) { + setRdiiAnls(selectedAnls); + } + }, [rainInfwtAnlsSn, rdiiAnlss]); + + const closePopup = () => { // 팝업 닫기 + setPopupOpen(false); + } + const rdiiRsltChart = () => { // 그래프 & 팝업 닫기 + fetchFilteredData(); + fn.selectPplRdiiQty(rainInfwtAnlsSn, pplMngNo).then((data) => {// 강우유입수량 조회 + setRdiiQty(Math.round(data.rdiih*24).toLocaleString('ko-KR')); // cmd: m³/d 단위로 변환 + if (data.raintotal && data.rainTotal !== 0) { + setRainTotal(data.raintotal.toLocaleString('ko-KR')); // 총강우량mm + setTotalVolume(Math.round(data.rdiih * data.sustaintime / data.raintotal).toLocaleString('ko-KR')); // 총체적 m³/mm + } + }); + setPopupOpen(false); + }; + + const fetchFilteredData: () => Promise<void> = async () => { + const rsltData = await fn.selectRdiiAnlsRslt(rainInfwtAnlsSn, pplMngNo); + const rsltDataformatted = fn.getChartDataRdiiAnlsRslt(rsltData ?? []); // format 변경 + const rainData = await fn.selectRainData(rainInfwtAnlsSn); + const rainDataformatted = fn.getChartDataRainData(rainData ?? []); // format 변경 + const rainMap = new Map(rainDataformatted.map(item => [item.label, item.barVal0])); + + rsltDataformatted.forEach(item => { + const matchedVal = rainMap.get(item.label); + if (matchedVal !== undefined) { + item.barVal0 = matchedVal; + } + }); + setChartData(rsltDataformatted); // 차트 데이터 설정 + } + const renderBody = () => ( + <> + <div className="text-black text-3xl font-bold mb-5">RDII분석결과</div> + + <div className='grid grid-cols-[1fr_3fr] gap-4'> + <div className="grid grid-rows-4 gap-4 mb-4"> + <Button className="bg-white text-white w-[130px] font-bold px-4 mt-2 py-2" onClick={openPopup}> + 분석 입력현황 조회 + </Button> + {/* 상수사용량 적용연월 */} + <div className="bg-white px-4 py-2 "> + <span className="text-2xl text-red-600 font-bold">● 상수사용량 적용연월</span> + <div className="flex items-center mt-2"> + <span className="ml-5 text-black text-xl font-bold"> + {(aplcnYm)? moment(aplcnYm, "YYYYMM").format("YYYY-MM"):""} + </span> + </div> + </div> + + {/* 강우유입수량 분석결과 */} + <div className="bg-white px-4 py-2 "> + <span className="text-2xl text-red-600 font-bold">● 강우유입수량 분석결과</span> + <div className="flex items-center mt-2"> + <span className="w-[95px] inline-block text-right text-black text-xl">평균RDII : </span> + <span className="w-[45px] inline-block text-right text-black text-xl font-bold"> {rdiiQty}</span> + <span className="ml-2 text-black"> m³/d</span> + </div> + <div className="flex items-center mt-2"> + <span className="w-[95px] inline-block text-right text-black text-xl">적용강우량 : </span> + <span className="w-[45px] inline-block text-right text-black text-xl font-bold"> {rainTotal}</span> + <span className="ml-2 text-black"> mm</span> + </div> + <div className="flex items-center mt-2"> + <span className="w-[95px] inline-block text-right text-black text-xl">강우량대비RDII : </span> + <span className="w-[45px] inline-block text-right text-black text-xl font-bold"> {totalVolume}</span> + <span className="ml-2 text-black"> m³/mm</span> + </div> + </div> + </div> + {/* 차트 */} + <div className="h-[90%] bg-white text-2xl"> + <MainChart chartConfig={chartConfig} chartForm={chartForm} chartData={chartData} chartTitle={'RDII 그래프'} + yFieldText2="강수량(mm)" yFieldText="유량(m³/h)" /> + </div> + </div> + {popupOpen && ( + <div + className="absolute bg-gray-100 border border-gray-300 w-[440px] shadow-lg p-6 rounded text-2xl" + style={{ + top: popupPosition.top, + left: popupPosition.left, + zIndex: 1000, + }} + > + <div className="font-bold mb-6 text-3xl">강우유입수분석 입력현황</div> + <div className='flex flex-col'> + <div className='flex items-center justify-between'> + <Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">상수사용월:</Label> + <SelectBox + className="w-[220px] mr-8 z-[51] mb-2" + selectClass="rounded-lg bg-white" + label='' + selectArray={aplcnYms} + value={aplcnYm} + onSelectValue={(val) => { + setAplcnYm(String(val)); + }} + /> + </div> + <div className='flex items-center justify-between'> + <Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">분석정보:</Label> + <SelectBox + className="w-[220px] mr-8 z-[51] mb-2" + selectClass="rounded-lg bg-white" + label='' + selectArray={rdiiAnlsInfos} + value={rainInfwtAnlsSn} + onSelectValue={(val) => { + setRainInfwtAnlsSn(Number(val)); + }} + /> + </div> + <div className='flex items-center justify-between'> + <Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">오수전환율:</Label> + <Input + className="mr-8 w-[220px] !text-xl placeholder:text-center mb-2" + placeholder="" + value={rdiiAnls?.drtwSwtcRt ?? ""} + readOnly={true} + /> + </div> + <div className='flex items-center justify-between'> + <Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">수용가-맨홀 매칭:</Label> + <Input + className="mr-8 w-[220px] !text-xl placeholder:text-center mb-2" + placeholder="" + value={rdiiAnls?.atflMpngNm ?? ""} + readOnly={true} + /> + </div> + <div className='flex items-center justify-between'> + <Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">관망도 파일명:</Label> + <Input + className="mr-8 w-[220px] !text-xl placeholder:text-center mb-2" + placeholder="" + value={rdiiAnls?.atflInpNm ?? ""} + readOnly={true} + /> + </div> + <div className='flex items-center justify-between'> + <Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">강우 파일명:</Label> + <Input + className="mr-8 w-[220px] !text-xl placeholder:text-center mb-2" + placeholder="" + value={rdiiAnls?.atflRainNm ?? ""} + readOnly={true} + /> + </div> + <div className='flex items-center justify-between'> + <Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">RTK계수 출처:</Label> + <Input + className="mr-8 w-[220px] !text-xl placeholder:text-center mb-2" + placeholder="" + value={rdiiAnls?.srcNm ?? ""} + readOnly={true} + /> + </div> + </div> + <div className="flex justify-end mt-4 space-x-2"> + <button className="px-4 py-2 bg-gray-600 text-white rounded" onClick={closePopup}> + 닫기 + </button> + <button className="px-4 py-2 bg-gray-600 text-white rounded" onClick={rdiiRsltChart}> + 선택조건 적용 + </button> + </div> + </div>)} + {/* </Modal > */} + </> + ) + + return ( + <UiModal + open={isModalOpen} + onOpenChange={setIsModalOpen} + headerTitle="" + bodyClass={` + min-h-[50vh] + w-[55vw] + max-w-none + max-h-none + overflow-visible + z-[50] + `} + body={renderBody()} + noFooterYn = 'Y' + /> + ) +} + +export default React.memo(RdiiChart); \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/chartModalStore.ts b/apps/sn/app/(map-layout)/map/components/popupComponent/chartModalStore.ts new file mode 100644 index 000000000..c84fa48a7 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/chartModalStore.ts @@ -0,0 +1,16 @@ +import { create } from 'zustand'; + +interface ModalStore { + isOpen: boolean; + setIsOpen: (open: boolean) => void; +} + +export const useInfilChartModalStore = create<ModalStore>((set) => ({ + isOpen: false, + setIsOpen: (open: boolean) => set({ isOpen: open }), +})); + +export const useRdiiChartModalStore = create<ModalStore>((set) => ({ + isOpen: false, + setIsOpen: (open: boolean) => set({ isOpen: open }), +})); \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/sewagePump.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/sewagePump.tsx new file mode 100644 index 000000000..2cde31b64 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/sewagePump.tsx @@ -0,0 +1,59 @@ +import { VerticalTable } from '@/components/ui/verticalTable'; +import React, { useState, useEffect } from 'react' +// 빗물펌프 자체 입력 정보 +const exclnInputColumn = [ + { id: "swtrmtZoneCd", title: "배수구역", type: "input" }, + { id: "mltvDtdvNm", title: "배수분구", type: "input" }, + { id: "dsgnFrqnc", title: "설계빈도(년)", type: "input" }, +] +// 오수펌프 자체 입력 정보 +const drtwInputColumn = [ + { id: "swtrmt_zone_cd", title: "처리구역", type: "input" }, + { id: "mltv_dtdv_nm", title: "처리분구", type: "input" }, +] + +// 하수펌프장 +const sewagePump = ({ featureData, isDrtwSewagePump, onSaveClick }: { featureData: any, isDrtwSewagePump?: boolean, onSaveClick?: () => void }) => { + const [formData, setFormData] = useState<any>({}); + + // featureData가 변경될 때 formData 초기화 + useEffect(() => { + if (featureData) { + setFormData(featureData); + } + }, [featureData]); + + // 폼 데이터가 변경될 때 호출될 핸들러 + const handleFormDataChange = (newFormData: any) => { + setFormData(newFormData); + }; + + // 저장 버튼 클릭 시 최신 formData를 부모에게 전달 + const handleSaveClick = () => { + if (onSaveClick) { + // 저장 전에 formData를 부모 컴포넌트의 handleSaveClick에 전달 + onSaveClick(); + } + }; + + return ( + <VerticalTable + headName='입력정보' + tableData={featureData ? [formData] : []} + headlist={isDrtwSewagePump ? drtwInputColumn : exclnInputColumn} + pageSize={0} + total={0} + children={undefined} + cellClick={undefined} + isEditable={true} + onPageChange={function (page: number): void { + throw new Error('Function not implemented.'); + }} + showSaveButton={true} + onSaveClick={handleSaveClick} + onFormDataChange={handleFormDataChange} // 폼 데이터 변경 콜백 추가 + /> + ) +} + +export default sewagePump diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/smh.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/smh.tsx new file mode 100644 index 000000000..f1cda25c1 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/smh.tsx @@ -0,0 +1,54 @@ +// 하수맨홀 +import { VerticalTable } from '@/components/ui/verticalTable'; +import { useEffect, useState } from 'react'; + +const inputColumn = [ + { id: "swtrmtZoneCd", title: "처리구역", type: "input" }, + { id: "mltvZoneNm", title: "배수구역", type: "input" }, + { id: "mltvDtdvNm", title: "배수분구", type: "input" }, + { id: "layCd", title: "매설위치", type: "select", options: ["도로", "하상"] }, +]; +const Smh = ({ featureData, onSaveClick }: { featureData: any, onSaveClick?: () => void }) => { + const [formData, setFormData] = useState<any>({}); + + // featureData가 변경될 때 formData 초기화 + useEffect(() => { + if (featureData) { + setFormData(featureData); + } + }, [featureData]); + + // 폼 데이터가 변경될 때 호출될 핸들러 + const handleFormDataChange = (newFormData: any) => { + setFormData(newFormData); + }; + + // 저장 버튼 클릭 시 최신 formData를 부모에게 전달 + const handleSaveClick = () => { + if (onSaveClick) { + // 저장 전에 formData를 부모 컴포넌트의 handleSaveClick에 전달 + onSaveClick(); + } + }; + + return ( + <VerticalTable + headName='입력정보' + tableData={featureData ? [formData] : []} + headlist={inputColumn} + pageSize={20} + total={0} + children={undefined} + cellClick={undefined} + isEditable={true} + onPageChange={function (page: number): void { + throw new Error("Function not implemented."); + }} + showSaveButton={true} + onSaveClick={handleSaveClick} + onFormDataChange={handleFormDataChange} // 폼 데이터 변경 콜백 추가 + /> + ) +} + +export default Smh; \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/stormWaterPump.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/stormWaterPump.tsx new file mode 100644 index 000000000..00ffb3bfe --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/stormWaterPump.tsx @@ -0,0 +1,28 @@ +import { VerticalTable } from '@/components/ui/verticalTable'; +import React from 'react' +const inputColumn = [ + { id: "swtrmt_zone_cd", title: "처리구역", type: "input" }, + { id: "mltv_dtdv_nm", title: "배수분구", type: "input" }, +] +// 빗물펌프 +const StormWaterPump = ({ featureData }: { featureData: Record<string, any> }) => { + const validKey = inputColumn.map((item) => item.id); + const filteredData = featureData && Object.entries(featureData).filter(([key]) => validKey.includes(key)).reduce((obj: Record<string, any>, [key, value]) => { obj[key] = value; return obj; }, {}); + return ( + <VerticalTable + headName='입력정보' + tableData={[filteredData]} + headlist={inputColumn} + pageSize={0} + total={0} + children={undefined} + cellClick={undefined} + isEditable={true} + onPageChange={function (page: number): void { + throw new Error('Function not implemented.'); + }} + /> + ) +} + +export default StormWaterPump \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/featureClickEventPopup.tsx b/apps/sn/app/(map-layout)/map/featureClickEventPopup.tsx new file mode 100644 index 000000000..5259fdab6 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/featureClickEventPopup.tsx @@ -0,0 +1,568 @@ +'use client' +import { getData, postData } from "@/components/lib/api"; +import Modal from "@/components/ui/modal"; +import { VerticalTable } from "@/components/ui/verticalTable"; +import { useGisContext } from "@common/business_components/ui/map/Gis"; +import { Button } from "@common/components/ui"; +import { useEffect, useRef, useState, useCallback, useMemo } from "react"; +import { getLayerContentComponent } from "./components/common"; +import { SwtrmtFcltCol, SwtrmtFcltCol2, SwtrmtPrcsZoneCol, SwtrmtPrcsZoneCol2, untrtSwgRainCol1, untrtSwgRainCol2, MsSwagePplCol, MsSwagePplCol2, ExclnSewagePumpCol, ExclnSewagePumpCol2, DrtwSewagePumpCol, DrtwSewagePumpCol2, MsSwageMhlCol, MsSwageMhlCol2, stofcbrCol, stofcbrCol2, MS_SWAGE_MHL_CODES, DRTW_SEWAGE_PUMP_USE_CODES } from "./components/common/table"; +import InfilChart from './components/popupComponent/chart/infilChart'; +import RdiiChart from './components/popupComponent/chart/rdiiChart'; +import { useInfilChartModalStore, useRdiiChartModalStore } from "./components/popupComponent/chartModalStore"; +import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue'; +import { codeMapper } from "./components/common/codeMapper"; +import { searchUpperPpls } from '@common/business_components/ui/map/Gis'; +import LoadingOverlay from '@/components/ui/LoadingBar'; + + +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 [layerNm, setLayerNm] = useState<string>(""); // 레이어 이름 상태 + const [layerSpecificNm, setLayerSpecificNm] = useState<string>(""); // 레이어 이름 상태 + const [tableData, setTableData] = useState<any>(null); // 피처 데이터 + const [rainMeasureData, setRainMeasureData] = useState<any[]>([]); // 강우/측정 측정 정보 + const [headlist, setHeadlist] = useState<{ id: string; title: string; }[]>([]); // 컬럼리스트 변경상태 + const [headlist2, setHeadlist2] = useState<{ id: string; title: string; }[]>([]); // 컬럼리스트 변경상태 + const [buttonChk, setButtonChk] = useState(true) + const [fetchData, setFetchData] = useState<any>(); // api 호출여부 + const contentRef = useRef<HTMLDivElement>(null); + const [isModalOpen, setIsModalOpen] = useState(false); // 모달 열기 상태 + const [isMainSsc, setIsMainSsc] = useState(false); // 중점우수토실 여부 + const [isDrtwSewagePump, setIsDrtwSewagePump] = useState(false); // 오수펌프장 여부 + // 🎯 InfilChart 및 RdiiChart 관련 상태 추가 + const { setIsOpen: setIsModalInfil } = useInfilChartModalStore(); + const { setIsOpen: setIsModalRdii } = useRdiiChartModalStore(); + const [curPage, setCurPage] = useState(1); // 현재 페이지 상태 + const [isClicked, setIsClicked] = useState(true); // 클릭 여부 상태 + const [isLoading, setIsLoading] = useState(false); // 로딩 상태 + + // 🎯 관로관리번호 추출 + const pplMngNo = featureData?.values_?.fclt_mng_no || ""; + // const featureId = featureData.getId(); // 피처의 id 가져오기 + + const featureId = typeof featureData.getId === 'function' + ? featureData.getId() + : featureData.fid; + console.log('featureId', featureId); + + // 🎯 데이터 전처리 메모이제이션 + const processedData = useMemo(() => { + if (!featureData?.values_) return null; + + return { + ...featureData.values_, + discharge_pipe_spec: `직경: ${featureData.values_.pipe_diameter || '-'} / 높이: ${featureData.values_.pipe_hg || '-'} / 길이: ${featureData.values_.pipe_lt || '-'}` + }; + }, [featureData?.values_]); + + // 피쳐데이터의 현재 레이어 이름 가져오는 함수 + const getSelectedLayers = () => { + let result = null; + const layers = gisMap.getLayers().getArray(); // gisMap에서 모든 레이어 가져오기 + let featureLayer: { get: (key: string) => any }; // 피처가 속한 레이어를 찾기 위한 변수 + layers.some((layer: any) => { + const source = layer.getSource(); + // console.log('실행됨3 source', source); + // console.log('실행됨3 featureData', featureData); + // console.log('실행됨3 featureData.fid', featureData.fid); + // if (source && featureData && featureData.fid) { + // featureLayer = layer; // 피처가 속한 레이어 찾기 + // } + if (source && source.hasFeature && featureData + && typeof featureData.getId === 'function' && source.hasFeature(featureData)) { + featureLayer = layer; // 피처가 속한 레이어 찾기 + return true; // 반복 종료 + } + return false; + }); + + if (featureData['ol_uid']) { + // Gis 레이어 클릭 이벤트 + setIsClicked(true); + console.log('gis data', featureData.values_); + const lyrTyNm = featureData.id_.split('.')[0]; + if (lyrTyNm === 'tb_ms_swage_ppl') { + result = '하수관로'; + setHeadlist(MsSwagePplCol); + setLayerSpecificNm(featureData.values_.fclt_mng_no); + } + else if (lyrTyNm === 'tb_ms_swage_mhl') { + result = '하수맨홀'; + setHeadlist(MsSwageMhlCol); + setLayerSpecificNm(featureData.values_.fclt_mng_no); + } + else if (lyrTyNm === 'tb_ms_swage_pmstn') { // 하수펌프장 + const drtwPumpCode = DRTW_SEWAGE_PUMP_USE_CODES.find(code => code.text === '오수펌프장'); + if (drtwPumpCode && featureData.values_.usg_cd.trim() === drtwPumpCode.val) { + setIsDrtwSewagePump(true); + setHeadlist(DrtwSewagePumpCol); + } + else { + setIsDrtwSewagePump(false); + setHeadlist(ExclnSewagePumpCol); + } + setLayerSpecificNm(featureData.values_.fclt_mng_no); + result = '하수펌프장'; + } + else if (lyrTyNm === 'tb_il_swage_prcs_fclt') { + result = '하수처리시설'; + setHeadlist(SwtrmtFcltCol); + setLayerSpecificNm(featureData.values_.swtrmt_fclt_cd); + } + else if (lyrTyNm === 'tb_ms_swage_prcs_zone') { + result = '하수처리구역'; + setHeadlist(SwtrmtPrcsZoneCol); + setLayerSpecificNm(featureData.values_.swtrmt_fclt_cd); + } + else if (lyrTyNm === 'tb_ms_stofcbr') { + result = '우수토실'; + setHeadlist(stofcbrCol); + setLayerSpecificNm(featureData.values_.fclt_mng_no); + } + else if (lyrTyNm === 'om_dt_untrt_swg_rain') { + result = '중점우수토실'; + setHeadlist(untrtSwgRainCol1); + setHeadlist2(untrtSwgRainCol2); + setLayerSpecificNm(featureData.values_.manage_no); + } + else result = featureLayer.get('lyrNm'); + } + else { + // SearchModal 클릭 이벤트 + setIsClicked(false); + console.log('SearchModal data', featureData); + const lyrTyNm = featureData.lyrTypeNm; + console.log('실행됨4 SearchModal 클릭 이벤트', lyrTyNm); + console.log('실행됨4 SearchModal 클릭 이벤트 featureData', featureData); + if (featureData.lyrTypeNm === 'tb_ms_swage_ppl') { + result = '하수관로'; + setHeadlist(MsSwagePplCol2) + setLayerSpecificNm(featureData.swtrmtFcltCd); + } + else if (featureData.lyrTypeNm === 'tb_ms_swage_mhl') { + result = '하수맨홀'; + setHeadlist(MsSwageMhlCol2) + setLayerSpecificNm(featureData.fcltMngNo); + } + else if (featureData.lyrTypeNm === 'tb_ms_swage_pmstn') { // 하수펌프장 + const drtwPumpCode = DRTW_SEWAGE_PUMP_USE_CODES.find(code => code.text === '오수펌프장'); + if (drtwPumpCode && featureData.usgCd.trim() === drtwPumpCode.val) { + console.log('오수펌프장'); + setIsDrtwSewagePump(false); + setHeadlist(ExclnSewagePumpCol2) + } + else { + console.log('빗물펌프장'); + setIsDrtwSewagePump(true); + setHeadlist(DrtwSewagePumpCol2) + } + setLayerSpecificNm(featureData.fcltMngNo); + result = '하수펌프장'; + } + else if (featureData.lyrTypeNm === 'tb_il_swage_prcs_fclt') { + result = '하수처리시설'; + setHeadlist(SwtrmtFcltCol2) + setLayerSpecificNm(featureData.swtrmtFcltCd); + } + else if (featureData.lyrTypeNm === 'tb_ms_swage_prcs_zone') { + result = '하수처리구역'; + setHeadlist(SwtrmtPrcsZoneCol2); + setLayerSpecificNm(featureData.swtrmtFcltCd); + } + else if (featureData.lyrTypeNm === 'tb_ms_stofcbr') { + result = '우수토실'; + setHeadlist(stofcbrCol2) + setLayerSpecificNm(featureData.fcltMngNo); + } + // else if (featureData.lyrTypeNm === 'om_dt_untrt_swg_rain') result = '중점우수토실'; + } + + return result; + }; + + useEffect(() => { + const fetchData = async () => { + if (!featureId) return; + + try { + const selectedLayerName = getSelectedLayers(); // 레이어 이름 가져오기 + setLayerNm(selectedLayerName); // 상태 업데이트 + + // console.log('gis data', featureData.values_); + let data; + if (featureData['ol_uid']) data = featureData.values_; // Gis 레이어 클릭 이벤트일 때 + else data = featureData; // SearchModal 클릭 이벤트일 때 + + // 하수맨홀인 경우 용도코드를 텍스트로 변환 + if (selectedLayerName === '하수맨홀' && data) { + // mqlt_cd 값 변환 (재질코드) + if (data.mqlt_cd) { + const materialCodeItem = MS_SWAGE_MHL_CODES.find( + code => code.val === data.mqlt_cd.trim() + ); + if (materialCodeItem) data.mqlt_cd = `${materialCodeItem.text}`; + } + } + + // 🎯 기본 데이터 포맷팅 + const formattedData = formatDisplayData(data); + + setFetchData(formattedData); + setTableData(formattedData); + + switch (selectedLayerName) { + case '하수관로': + setButtonChk(true) + + // ✅ codeMapper 사용하여 API 결과도 변환 + setTableData(codeMapper({ + layerName: selectedLayerName, + data: { ...formattedData } + })); + break; + case '하수맨홀': + setButtonChk(true) + + // ✅ codeMapper 사용하여 API 결과도 변환 + setTableData(codeMapper({ + layerName: selectedLayerName, + data: { ...formattedData } + })); + break; + case '하수펌프장': + setButtonChk(true) + + setTableData(codeMapper({ + layerName: selectedLayerName, + data: { ...formattedData } + })); + break; + case '하수처리시설': + setButtonChk(false) + break; + case '하수처리구역': + setButtonChk(false) + break; + case '우수토실': + setButtonChk(true) + setIsMainSsc(false) + break; + case '중점우수토실': + setButtonChk(false) + setIsMainSsc(true) + // 🎯 processedData도 포맷팅하여 설정 + const processedFormattedData = formatDisplayData(processedData); + + setFetchData([processedFormattedData]) + setTableData(processedFormattedData); + // setIsLoading(false); // 기본 렌더링 완료 + + setTableData(codeMapper({ + layerName: selectedLayerName, + data: { ...processedFormattedData } + })); + + // 강우/측정 정보 데이터 호출 + if (tableData?.chmbr_xcnts && tableData?.chmbr_ydnts) { + fetchRainMeasureData(tableData.chmbr_xcnts, tableData.chmbr_ydnts) + .then(response => { + console.log('additionalResponse', response); + setRainMeasureData(response || []); + }) + .catch(error => { + console.error('추가 데이터 로딩 실패:', error); + setRainMeasureData([]); + }); + } + break; + default: + break; + } + + // 자체입력정보 API 엔드포인트 선택 + let apiEndpoint = ''; + const getFeatureId = featureId.toString().split('.').pop() || null; + if (selectedLayerName === '하수관로') apiEndpoint = `v1/fcltppl/${getFeatureId}`; + else if (selectedLayerName === '우수토실') apiEndpoint = `v1/fcltstofcbr/${getFeatureId}`; + else if (selectedLayerName === '오수펌프' || selectedLayerName === '빗물펌프') apiEndpoint = `v1/fcltpump/${getFeatureId}`; + else if (selectedLayerName === '하수맨홀') apiEndpoint = `v1/fcltmhl/${getFeatureId}`; + + // API에서 상세 데이터 가져오기 + if (apiEndpoint) { + try { + console.log(`API 호출: ${apiEndpoint}`); + const result = await getData(apiEndpoint); + + if (result) { + console.log('API 데이터 로드 성공:', result); + + // ✅ codeMapper 사용하여 API 결과도 변환 + const processedApiData = codeMapper({ + layerName: selectedLayerName, + data: { ...result, ...tableData } + }); + + setFetchData(processedApiData); + } + } catch (apiError) { + console.error('API 데이터 로딩 중 오류:', apiError); + } + } + } catch (error) { + console.error('데이터 로딩 중 오류:', error); + } + }; + + fetchData(); + }, [featureId, processedData]); + + // 🎯 추가 데이터 API 호출 함수 + const fetchRainMeasureData = useCallback(async (chmbrXcnts: string, chmbrYdnts: string) => { + if (!chmbrXcnts || !chmbrYdnts) { + console.warn('추가 데이터 호출 실패: 필수 매개변수 누락', { chmbrXcnts, chmbrYdnts }); + return []; + } + + try { + const queryParams = new URLSearchParams({ + chmbrXcnts: chmbrXcnts, + chmbrYdnts: chmbrYdnts, + pageable: `{}`, + }); + + console.log(`추가 데이터 API 호출 시작: v1/onsemiro/getSwgRainsByGeomety?${queryParams.toString()}`); + + const result = await getData(`v1/onsemiro/getSwgRainsByGeomety?${queryParams.toString()}`); + + console.log('추가 데이터 API 호출 성공:', result); + + // 🎯 result가 페이지네이션 객체인 경우 content 배열 추출 + let dataArray: any[] = []; + if (result && typeof result === 'object' && 'content' in result && Array.isArray(result.content)) { + dataArray = result.content; + } else if (Array.isArray(result)) { + dataArray = result; + } else { + console.warn('예상치 못한 API 응답 형식:', result); + return []; + } + + // 🎯 각 아이템을 전처리하여 반환 + const processedDataArray = dataArray.map((data: any) => { + // 🎯 formatDisplayData로 기본 포맷팅 적용 + const formattedItem = formatDisplayData(data); + + // 🎯 추가 데이터 조합 + return { + ...formattedItem, + flwmtrCombined: `${data.flwmtrMesureTpEtc || '-'} / ${data.flwmtrKnd || '-'}`, + }; + }); + + return processedDataArray; + } catch (error) { + console.error('추가 데이터 로딩 실패:', error); + return []; + } + }, []); + + // 🎯 추가 데이터는 별도 useEffect로 비동기 로딩 + useEffect(() => { + if (layerNm === '중점우수토실' && featureData?.values_) { + const data = featureData.values_; + + if (data?.chmbr_xcnts && data?.chmbr_ydnts) { + // 🎯 추가 데이터를 비동기로 로딩 (UI 블로킹 없음) + fetchRainMeasureData(data.chmbr_xcnts, data.chmbr_ydnts) + .then(response => { + console.log('추가 데이터 로딩 완료:', response); + setRainMeasureData(response || []); + }) + .catch(error => { + console.error('추가 데이터 최종 로딩 실패:', error); + setRainMeasureData([]); + }); + } + } + }, [layerNm, featureData?.values_, fetchRainMeasureData]); + + // 저장 시 모달 띄우기 + const handleSaveClick = () => { + setIsModalOpen(true); // 모달 열기 + }; + + const handleModalSave = () => { + setIsModalOpen(false); // 모달 닫기 + btnClickEvent(); // 저장 이벤트 실행 + }; + + const handleModalClose = () => { + setIsModalOpen(false); // 모달 닫기 + }; + + // 저장 버튼 이벤트 + const btnClickEvent = async () => { + const bodyData: Record<string, any> = {} + + if (buttonChk && contentRef.current) { + const inputElements = contentRef.current.querySelectorAll('input'); + const selectElements = contentRef.current.querySelectorAll('select'); + + // 카멜케이스 변환 함수 + const toCamelCase = (str: string) => + str.replace(/_([a-z])/g, (_, char) => char.toUpperCase()); + + // select 태그의 값 가져오기 + selectElements.forEach((select) => { + const camelCaseId = toCamelCase(select.id); + bodyData[camelCaseId] = select.value; + }); + + // // input 태그의 값 가져오기 + inputElements.forEach((input) => { + const camelCaseId = toCamelCase(input.id); + bodyData[camelCaseId] = input.value; + }); + + // 레이어명에 따라 API 엔드포인트 분기처리 + let apiEndpoint = ''; + let result = null; + + if (layerNm === '하수관로') apiEndpoint = 'v1/fcltppl'; + else if (layerNm === '우수토실') apiEndpoint = 'v1/fcltstofcbr'; + else if (layerNm === '하수펌프장') apiEndpoint = 'v1/fcltpump'; + else if (layerNm === '하수맨홀') apiEndpoint = 'v1/fcltmhl'; + + bodyData['fid'] = featureId.toString().split('.').pop() || null; + // 빈 키 제거 + if (bodyData[""] !== undefined) delete bodyData[""]; + + // 🎯 우수토실일 경우 prrtymngYn 값 변환 + if (layerNm === '우수토실' && bodyData.prrtymngYn) { + if (bodyData.prrtymngYn === '중점우수토실') { + bodyData.prrtymngYn = 'Y'; + } else if (bodyData.prrtymngYn === '우수토실') { + bodyData.prrtymngYn = 'N'; + } + } + console.log('bodyData', bodyData); + console.log('apiEndpoint', apiEndpoint); + + try { + result = await postData(apiEndpoint, bodyData); + console.log('result', result); + + if (result) { + alert('저장되었습니다'); + setFetchData(result.data || result); + } else { + alert('저장에 실패했습니다'); + console.error('API 응답 오류:', result); + } + } catch (error) { + alert('저장 중 오류가 발생했습니다'); + console.error('API 요청 오류:', error); + } + } + } + + // 레이어명에 따라 상세페이지 렌더링 + 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-[50]"> + <LoadingOverlay isLoading={isLoading} /> + <div className="card-header bg-primary flex items-center justify-between relative p-4"> + <div className="flex items-center space-x-4 flex-1"> + <h3 className="text-2xl font-medium text-white text-label whitespace-nowrap">{layerNm} + {tableData?.usg_cd && ( + <> + ({tableData.usg_cd}) + </> + )} 정보 <br />관리번호 : {layerSpecificNm} + </h3> + <div className="flex items-center space-x-3"> + {/* 🎯 하수관로일 때만 Infil/RDII 버튼 표시 */} + {layerNm === '하수관로' && ( + <> + <Button onClick={() => setIsModalInfil(true)} + className="py-1 text-xl text-blue-600 border border-blue-600 rounded hover:bg-gray-700" + color="white" + size="sm" + > + 침입수분석 + </Button> + <Button onClick={() => setIsModalRdii(true)} + className="py-1 text-xl text-gray-600 border border-gray-600 rounded hover:bg-gray-700" + color="white" + size="sm" + > + RDII분석 + </Button> + </> + )} + {layerNm === '하수맨홀' && ( + <> + <Button onClick={() => { + setIsLoading(true); + searchUpperPpls(layerSpecificNm).finally(() => setIsLoading(false)); + }} + className="px-2 py-1 text-xl text-gray-600 border border-gray-600 rounded hover:bg-gray-700" + color="white" + > + 상류관로 보기 + </Button> + </> + )} + </div> + </div> + <Button + onClick={onClose} + // className="text-white hover:text-gray-300 focus:outline-none" + className="absolute top-8 right-4 text-white hover:text-gray-300 focus:outline-none" + size="xs" + > + ✕ + </Button> + </div> + <div className="overflow-y-auto overflow-x-hidden flex-1" ref={contentRef}> + <VerticalTable + headName='기본정보' + tableName1={layerNm === '중점우수토실' ? "측정지점" : undefined} + tableData={[tableData]} + headlist={headlist} + pageSize={20} + total={0} + cellClick={undefined} + onPageChange={function (): void { + throw new Error("Function not implemented."); + }} + // 두 번째 테이블 조건부 props + showSecondTable={layerNm === '중점우수토실' ? true : false} + tableName2={layerNm === '중점우수토실' ? "시설현황" : undefined} + tableData2={layerNm === '중점우수토실' && tableData ? [tableData] : []} + headlist2={layerNm === '중점우수토실' ? headlist2 : undefined} + pageSize2={20} + total2={0} + isEditable2={buttonChk} + /> + <Modal isModalOpen={isModalOpen} title="자체입력정보 저장" setIsModalOpen={setIsModalOpen} closeModal={handleModalClose} saveModal={handleModalSave}><div className="text-lg">정보를 저장하시겠습니까?</div></Modal> + {ContentComponent && + <ContentComponent + featureData={fetchData} + isMainSsc={isMainSsc} + isDrtwSewagePump={isDrtwSewagePump} + rainMeasureData={rainMeasureData} + onSaveClick={handleSaveClick} + /> + } + </div> + + {/* 🎯 InfilChart 및 RdiiChart 컴포넌트 추가 */} + <InfilChart pplMngNo={pplMngNo} /> + <RdiiChart pplMngNo={pplMngNo} /> + </div> + ) +} +export default FeatureClickEventPopup; \ No newline at end of file diff --git a/apps/sn/app/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx similarity index 81% rename from apps/sn/app/map/page.tsx rename to apps/sn/app/(map-layout)/map/page.tsx index e1a90d5f1..2dbbcdd1a 100644 --- a/apps/sn/app/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -1,12 +1,36 @@ "use client"; import Gis, { useGisContext } from "@common/business_components/ui/map/Gis"; -import { mapService } from "@service"; +import { getData } from "../../components/lib/api"; import { useCallback, useEffect, useRef, useState } from "react"; import GisApp from "@common/business_components/ui/map/lib/gis.core"; -import SearchModal from "./SearchModal"; +import SearchModal from "./searchModal/searchModal"; import FeatureClickEventPopup from "./featureClickEventPopup"; +// 타입 정의 추가 +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; +}; + const Map = () => { const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 const [selectedFeature, setSelectedFeature] = useState(null); // 선택된 피처 상태 @@ -14,20 +38,19 @@ const Map = () => { const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태 const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 + const fetchData = async () => { try { - const response = await mapService.getLyrStngs(); - setTreeData(response.data); // API 응답 저장 + const data = await getData("v1/map/lyrStngs/SN_NETWORK"); + setTreeData(data); } catch (error) { console.error("데이터 불러오기 실패:", error); } }; - // 컴포넌트 마운트 시 1회 실행 useEffect(() => { fetchData(); }, []); - // SearchModal, GisContent 등에서 호출 const onFeatureSelect = (data: any) => { let map = gisMaps.current; @@ -96,7 +119,7 @@ const Map = () => { treeData={treeData} isSearch={true} gisMaps={gisMaps} - isMiniMap={true} + isMiniMap={false} searchView={SearchModal} detailView={FeatureClickEventPopup} onFeatureSelect={onFeatureSelect} @@ -106,6 +129,7 @@ const Map = () => { setIsSearchModalOpen={setIsSearchModalOpen} isOpen={isOpen} isSearchModalOpen={isSearchModalOpen} + onCheckedLayersChange={undefined} /> </div> ); diff --git a/apps/sn/app/(map-layout)/map/searchModal/basicTab.tsx b/apps/sn/app/(map-layout)/map/searchModal/basicTab.tsx new file mode 100644 index 000000000..89d1db591 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/basicTab.tsx @@ -0,0 +1,199 @@ +import React from 'react'; +import { MS_SWAGE_MHL_USE_CODES, MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES } from '../components/common/table'; +import { SidoSigunguSelectBox } from '@/components/ui/SidoSigunguSelectBox'; +import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox'; +import { SelectBox } from '@common/business_components/ui'; +import { Button, Input, Label } from '@common/components'; +import SwagePpl from './swagePpl'; +import SwagePrcsFclt from './swagePrcsFclt'; +import SwagePrcsZone from './swagePrcsZone'; +import SwageMhl from './swageMhl'; +import SwagePmstn from './swagePmstn'; + +// 기본정보 '구분' 선택 목록 +export const BASIC_TYPES = [ + { val: "tb_ms_swage_ppl", text: "하수관로" }, + { val: "tb_il_swage_prcs_fclt", text: "하수처리시설" }, + { val: "tb_ms_swage_prcs_zone", text: "하수처리구역" }, + { val: "tb_ms_swage_mhl", text: "하수맨홀" }, + { val: "tb_ms_swage_pmstn", text: "하수펌프장" }, + { val: "tb_ms_stofcbr", text: "우수토실" }, +]; + +interface BasicTabProps { + open?: boolean; + basicType: string; + city: number; + sgg: number; + start1: string; + start2: string; + end1: string; + end2: string; + var1: string; + var2: string; + yearFilter: { start: string; end: string; }; + handleSearch: () => void; + // onOpenChange?: (open: boolean) => void; + // onFeatureSelect?: (row: any) => void; + // setTableData?: (data: any[]) => void; + // setTableCount?: (count: number) => void; + setBasicType: (type: string) => void; + setCity: (city: number) => void; + setSgg: (sgg: number) => void; + setYearFilter: (filter: { start: string; end: string }) => void; + setStart1: (value: string) => void; + setStart2: (value: string) => void; + setEnd1: (value: string) => void; + setEnd2: (value: string) => void; + setVar1: (value: string) => void; + setVar2: (value: string) => void; +} + +const BasicTab: React.FC<BasicTabProps> = ({ + // open, + basicType, + city, + sgg, + start1, + start2, + end1, + end2, + var1, + var2, + yearFilter, + handleSearch, + // onOpenChange, + // onFeatureSelect, + // setTableData, + // setTableCount, + setBasicType, + setCity, + setSgg, + setYearFilter, + setStart1, + setStart2, + setEnd1, + setEnd2, + setVar1, + setVar2, +}) => { + return ( + <div className="relative border rounded-lg p-6 w-full h-[10vh]"> + {/* 구분 */} + <div className="mb-3 flex items-center justify-between gap-4"> + <div className="flex items-center"> + <SelectBox + className="w-[150px] mr-3" + selectClass="rounded-lg bg-white" + label="구분" + selectArray={BASIC_TYPES} + onSelectValue={(value: string | number) => + setBasicType(String(value)) + } + value={basicType} + selectContentClass="z-[101]" + /> + </div> + </div> + {/* 분기처리: 하수관로 */} + {basicType === "tb_ms_swage_ppl" && ( + <SwagePpl + var1={var1} + var2={var2} + start1={start1} + end1={end1} + yearFilter={yearFilter} + setVar1={setVar1} + setVar2={setVar2} + setStart1={setStart1} + setEnd1={setEnd1} + setYearFilter={setYearFilter} + handleSearch={handleSearch} + /> + )} + {/* 분기처리: 하수처리시설 */} + {basicType === "tb_il_swage_prcs_fclt" && ( + <SwagePrcsFclt + var1={var1} + start1={start1} + start2={start2} + end1={end1} + end2={end2} + yearFilter={yearFilter} + setVar1={setVar1} + setStart1={setStart1} + setStart2={setStart2} + setEnd1={setEnd1} + setEnd2={setEnd2} + setYearFilter={setYearFilter} + handleSearch={handleSearch} + /> + )} + {/* 분기처리: 하수처리구역 */} + {basicType === "tb_ms_swage_prcs_zone" && ( + <SwagePrcsZone + var1={var1} + var2={var2} + city={city} + sgg={sgg} + setVar1={setVar1} + setVar2={setVar2} + setCity={setCity} + setSgg={setSgg} + handleSearch={handleSearch} + /> + )} + {/* 분기처리: 하수맨홀 */} + {basicType === "tb_ms_swage_mhl" && ( + <SwageMhl + var1={var1} + start1={start1} + end1={end1} + yearFilter={yearFilter} + setVar1={setVar1} + setStart1={setStart1} + setEnd1={setEnd1} + setYearFilter={setYearFilter} + handleSearch={handleSearch} + /> + )} + {/* 분기처리: 하수펌프장 */} + {basicType === "tb_ms_swage_pmstn" && ( + <SwagePmstn + start1={start1} + start2={start2} + end1={end1} + end2={end2} + yearFilter={yearFilter} + setStart1={setStart1} + setStart2={setStart2} + setEnd1={setEnd1} + setEnd2={setEnd2} + setYearFilter={setYearFilter} + handleSearch={handleSearch} + /> + )} + {/* 분기처리: 우수토실 */} + {basicType === "tb_ms_stofcbr" && ( + <div className="mb-3"> + {/* 설치년도 */} + <div className="flex items-center mb-2"> + <YearRangeSelectBox + start={yearFilter.start} + end={yearFilter.end} + onChange={range => setYearFilter({ ...yearFilter, start: range.start, end: range.end })} + startLabel="설치년도" + endLabel="" + /> + {/* 조회 버튼 */} + <div className="flex-1 flex justify-end"> + <Button className="text-base py-2" onClick={handleSearch}>조회</Button> + </div> + </div> + </div> + )} + </div> + ); +}; + +export default React.memo(BasicTab); \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/searchModal/operationTab.tsx b/apps/sn/app/(map-layout)/map/searchModal/operationTab.tsx new file mode 100644 index 000000000..97c0b4c4c --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/operationTab.tsx @@ -0,0 +1,164 @@ +import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox'; +import { SelectBox } from '@common/business_components/ui'; +import { Button, Input, Label } from '@common/components'; +import React from 'react'; +//import { useDcTree, flattenFolders, useSggCd, useCityCd } from './components/searchModalReportApi'; +// import { useBizByFcltStore } from "../../../store/StoreProvider"; + +// 운영정보 '구분' 선택 목록 +// const WORK_TYPES = [ +// { val: "maintenance", text: "보수 및 갱신" }, +// { val: "cleaning", text: "청소 및 준설" }, +// { val: "inspection", text: "점검 및 진단" }, +// ]; +export const WORK_TYPES = [ + { val: "['200', '201', '204']", text: "보수 및 갱신" }, + { val: "['100']", text: "점검 및 진단" }, + { val: "['203']", text: "청소 및 준설" }, +]; +// const BIZ_SE_CD = useBizByFcltStore(state => state.BIZ_SE_CD); +export const BIZ_SE_CD = [ + { lwpoCd: "100", lwpoCdNm: "관로기술진단" }, + { lwpoCd: "200", lwpoCdNm: "개대체" }, + { lwpoCd: "201", lwpoCdNm: "유지보수" }, + { lwpoCd: "203", lwpoCdNm: "청소, 준설" }, + { lwpoCd: "204", lwpoCdNm: "단순보수" }, +]; +// const BIZ_TY_CD = useBizByFcltStore(state => state.BIZ_TY_CD); +export const BIZ_TY_CD = [ + { lwpoCd: "0", lwpoCdNm: "전체" }, + { lwpoCd: "SV", lwpoCdNm: "용역" }, + { lwpoCd: "SF", lwpoCdNm: "자체" }, + { lwpoCd: "SC", lwpoCdNm: "공사" }, +]; +export const BIZ_TY_CD_SELECT_ARRAY = BIZ_TY_CD.map(item => ({ + val: item.lwpoCd, + text: item.lwpoCdNm, +})); +// 운영정보 - 사업 현황 +export const operationTableHeadlist = [ + { id: 'rowNumber', title: '일련번호' }, + { id: "bizNm", title: "사업명" }, + { id: "flfmtBgngYmd", title: "수행시작일자" }, + { id: "flfmtEndYmd", title: "수행종료일자" }, + { id: "flfmtYr", title: "수행년도" }, + { id: "bizTyCd", title: "사업유형" }, + { id: "bizAmt", title: "사업금액" }, +]; +// 운영정보 - 보수 및 갱신 결과표 +export const operationMaintenanceTableHeadlist = [ + { id: 'rowNumber', title: '일련번호' }, + { id: "fcltMngNo", title: "관리번호" }, + { id: "usgCd", title: "용도" }, + { id: "mqltCd", title: "재질" }, + { id: "pdmt", title: "관경(mm)" }, + { id: "prlg", title: "연장(m)" }, + { id: "instlYmd", title: "설치일자" }, + // { id: "instlYmd", title: "사업구분" }, +]; +// 운영정보 - 청소 및 준설 결과표 +export const operationCleaningTableHeadlist = [ + { id: 'rowNumber', title: '일련번호' }, + { id: "fcltMngNo", title: "관리번호" }, + { id: "usgCd", title: "용도" }, + { id: "mqltCd", title: "재질" }, + { id: "pdmt", title: "관경(mm)" }, + { id: "prlg", title: "연장(m)" }, + { id: "instlYmd", title: "설치일자" }, +]; +// 운영정보 - 점검 및 진단 결과표 +export const operationInspectionTableHeadlist = [ + { id: 'rowNumber', title: '일련번호' }, + { id: "fcltMngNo", title: "관리번호" }, + { id: "usgCd", title: "용도" }, + { id: "mqltCd", title: "재질" }, + { id: "pdmt", title: "관경(mm)" }, + { id: "prlg", title: "연장(m)" }, + { id: "instlYmd", title: "설치일자" }, + { id: "strct_stat_grd", title: "구조적 상태등급" }, + { id: "oper_stat_grd", title: "운영적 상태등급" }, +]; + +interface OperationTabProps { + open: boolean; + workType: string; + var1: string; + var2: string; + yearFilter: { start: string; end: string; }; + handleSearch: () => void; + // onOpenChange: (open: boolean) => void; + // setTableData: (data: any[]) => void; + // setTableCount: (count: number) => void; + setWorkType: (type: string) => void; + setYearFilter: (filter: { start: string; end: string }) => void; + setVar1: (value: string) => void; + setVar2: (value: string) => void; +} + +const OperationTab: React.FC<OperationTabProps> = ({ + open, + workType, + var1, + var2, + yearFilter, + handleSearch, + // onOpenChange, + // setTableData, + // setTableCount, + setWorkType, + setVar1, + setVar2, + setYearFilter, +}) => { + return ( + <div className="relative flex flex-col border rounded-lg p-6 w-full h-[10vh]"> + <div className="flex items-center mb-3"> + <SelectBox + className="w-80 mr-8" + selectClass="rounded-lg bg-white text-xl" + label="업무구분" + selectArray={WORK_TYPES.map((w) => ({ + val: w.val, + text: w.text, + }))} + onSelectValue={(val) => setWorkType(String(val))} + value={workType} + selectContentClass="z-[101]" + /> + </div> + <div className="flex items-center mb-6"> + <Label className="mr-2 w-auto text-xl text-gray-600">사업명</Label> + <Input + className="w-[100px] !text-xl placeholder:text-center mr-4" + placeholder="입력" + value={var1} + onChange={(e) => { setVar1(e.target.value); }} + /> + <YearRangeSelectBox + start={yearFilter.start} + end={yearFilter.end} + onChange={range => setYearFilter({ ...yearFilter, start: range.start, end: range.end })} + startLabel="수행기간" + endLabel="" + /> + <SelectBox + className="ml-8 w-60 mr-8" + selectClass="rounded-lg bg-white text-xl" + label="사업 유형" + selectArray={BIZ_TY_CD_SELECT_ARRAY} + value={var2} + onSelectValue={(val) => { + setVar2(String(val)); + }} + selectContentClass="z-[101]" + /> + {/* 조회 버튼 */} + <div className="flex-1 flex justify-end"> + <Button className="text-base py-2" onClick={handleSearch}>조회</Button> + </div> + </div> + </div> + ) +}; + +export default React.memo(OperationTab); \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx b/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx new file mode 100644 index 000000000..48399ac22 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx @@ -0,0 +1,233 @@ +'use client'; +import React, { useEffect, useState } from 'react'; +import { Button } from '@common/components'; +import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox'; +import { SelectBox, UiTable } from '@common/business_components/ui'; +import LoadingOverlay from '@/components/ui/LoadingBar'; + +import { addFileName, flattenFolders, getFileDownload, useCityCd, useDcTree, useSggCd, reportGubuns } from './searchModalReportApi'; +import { type SelectDataType } from '../commonStore'; + + +// 하수도정비 기본계획 보고서 +export const reportTableHeadlist = [ + { id: "no", title: "NO" }, + { id: "sido", title: "시도" }, + { id: "sigungu", title: "시군구" }, + { id: "approvalYear", title: "승인년도" }, + { id: "plantZoneGugun", title: "구분" }, + { id: "reportGubun", title: "보고서구분" }, + { id: "fileName", title: "파일명" }, + { id: "regDate", title: "등록일자" }, +]; + + +interface BasicTabProps { + open?: boolean; +} + +const ReportTab: React.FC<BasicTabProps> = ({ open }) => { + + const [isLoading, setIsLoading] = useState(false); + const [isResetPage, setIsResetPage] = useState<boolean>(false); + const [isResetSelection, setIsResetSelection] = useState<boolean>(false); + const [showTable, setShowTable] = useState(false); + const [tableData, setTableData] = useState<any[]>([]); + const [tableCount, setTableCount] = useState(0); + const [curPage, setCurPage] = useState(1); + const [yearFilter, setYearFilter] = useState({ start: "", end: "" }); + const [ctpvCd, setCtpvCd] = useState('0'); // 하수정비기본계획 report + const [sigungu, setSigungu] = useState('0'); // 하수정비기본계획 report + const [filteredSiguns, setFilteredSiguns] = useState<SelectDataType[]>([{ val: '0', text: '전체', sido: '0' }]); + const [gubun, setGubun] = useState(""); // 하수정비기본계획 report + const [reportGubun, setReportGubun] = useState("0"); // 하수정비기본계획 report + + + // 하수정비기본계획 report + const sidos = useCityCd(); + const siguns = useSggCd(); + const reportOriginData = useDcTree(""); //:Folder[] + + const dynamicHeadlist: { id: string, title: string }[] = reportTableHeadlist; + + useEffect(() => { + // filterParams 초기화 + setCtpvCd('0'); + setSigungu('0'); + setReportGubun('0'); + setYearFilter({ start: "", end: "" }); + setShowTable(false); + setTableData([]); + setTableCount(0); + setCurPage(1); // 페이지를 1로 초기화 + setIsResetPage(true); // 페이지를 초기화 + setIsResetSelection(true); // 선택 초기화 + }, [open]); + + // 페이지 변경 시마다 데이터 fetch + useEffect(() => { + // showTable이 true일 때만 fetch (최초 렌더 방지) + setIsResetPage(false); + setIsResetSelection(true); // 선택 초기화 + if (showTable) fetchFilteredData(); + }, [curPage]); + + const handleCtpvCdChange = (val: string | number) => { // 하수도정비기본계획.시도 + const sidoOption = sidos.find((n) => n.val === val); + setCtpvCd(String(val)); + if (val !== '0') { + const filterSiguns = siguns.filter(item => item.sido === sidoOption?.text); + filterSiguns.unshift({ val: '0', text: '전체', sido: '0' }); + setFilteredSiguns(filterSiguns); + } + setSigungu("0"); + setGubun(""); + setYearFilter({ start: "", end: "", }); + } + + + // 조회 버튼 클릭 시 1페이지로 이동 및 데이터 초기화 + const handleSearch = () => { + setCurPage(1); // 페이지를 1로 초기화 + fetchFilteredData(); + setIsResetPage(true); // 페이지를 초기화 + setIsResetSelection(false); // 선택 초기화 + setShowTable(true); + }; + + // 하수도정비기본계획 테이블 cellClick 핸들러 + const handleReportCellClick = (index: number | undefined ) => { + console.log(index); + if (typeof index === "number" && tableData[index]) { + const reportData = tableData[index]; + const fileName = reportData.fileName; + const filePath = reportData.filePath; + if (reportData.fileSn !== "") { + getFileDownload(reportData.fileSn, fileName, filePath); + } + } + }; + + // 검색 API 호출 함수 + const fetchFilteredData = async () => { + setIsLoading(true); // 로딩 시작 + setIsResetSelection(true); // 선택 초기화 + + try { + //const reportOriginData = await getDcTree('경기도'); + const reportData = flattenFolders(reportOriginData); + let filterReportData = reportData; + if (ctpvCd !== "0") { // 시도검색 + const sidoOption = sidos.find((n) => n.val === ctpvCd); + filterReportData = filterReportData.filter(item => item.sido === sidoOption?.text); + } + if (sigungu !== "0") { // 시군구검색 + const sigunguOption = filteredSiguns.find((n) => n.val === sigungu); + filterReportData = filterReportData.filter(item => item.sigungu === sigunguOption?.text); + } + if (reportGubun !== "0") { // 보고서구분검색 + const reportGubunOption = reportGubuns.find((n) => n.val === reportGubun); + filterReportData = filterReportData.filter(item => item.reportGubun === reportGubunOption?.text); + } + + if (yearFilter.start != "" && yearFilter.end == "") //승인년도검색 + filterReportData = filterReportData.filter(item => (item.approvalYear) ? item.approvalYear >= yearFilter.start + "년" : null); + if (yearFilter.start == "" && yearFilter.end != "") + filterReportData = filterReportData.filter(item => (item.approvalYear) ? item.approvalYear <= yearFilter.end + "년" : null); + if (yearFilter.start != "" && yearFilter.end != "") { + filterReportData = filterReportData.filter(item => { + return (item.approvalYear) ? item.approvalYear >= yearFilter.start + "년" && item.approvalYear <= yearFilter.end + "년" : null; + }); + } + if (gubun !== "") { // 구분검색 + filterReportData = filterReportData.filter(item => (item.plantZoneGugun) ? String(item.plantZoneGugun).includes(String(gubun)) : null); + } + console.log('report count:', filterReportData.length); + setTableCount(filterReportData.length || 0); + filterReportData = filterReportData.slice((curPage - 1) * 10, curPage * 10); // paging + filterReportData = filterReportData?.map((item, index) => ({ ...item, no: (curPage - 1) * 10 + index + 1 })); + //const fileList = await getDcFile("CLS20250530000041"); + addFileName(filterReportData).then(data => { + filterReportData = data; + setTableData(filterReportData || []); + }); + setTableData(filterReportData || []); + + setShowTable(true); + } catch (err) { + console.error('API ERROR:', err); + setTableData([]); + } finally { + setIsLoading(false); // 로딩 종료 + } + }; + + + return ( + <> + <div className="relative flex flex-col border rounded-lg p-6 w-full h-[10vh]"> + <div className="flex items-center mb-3"> + <SelectBox className="w-[150px] mr-8" + selectClass="rounded-lg bg-white" + label="시도" selectArray={sidos} + value={ctpvCd} onSelectValue={handleCtpvCdChange} + selectContentClass="z-[101]" + /> + <SelectBox className="w-48" + selectClass="w-48 rounded-lg bg-white truncate" + label="시군구" selectArray={filteredSiguns} + value={sigungu} onSelectValue={val => setSigungu(String(val))} + selectContentClass="z-[101]" + /> + </div> + <div className="flex items-center mb-6"> + {/* <label className="inline-block w-auto whitespace-nowrap text-xl text-gray-600 mr-2">구분</label> + <Input + className='!text-xl w-[100px] placeholder:text-center mr-8' + placeholder="입력" + value={gubun} + onChange={(e) => { setGubun(e.target.value); }} + /> */} + <SelectBox className="w-[150px] mr-8" + selectClass="rounded-lg bg-white" + label="보고서구분" selectArray={[...reportGubuns]} + value={reportGubun} onSelectValue={val => setReportGubun(String(val))} + selectContentClass="z-[101]" labelClass='whitespace-nowrap text-xl text-gray-600 pr-2' + /> + {/* 승인년도 필터 */} + <YearRangeSelectBox start={yearFilter.start} end={yearFilter.end} + onChange={range => setYearFilter({ ...yearFilter, start: range.start, end: range.end })} + startLabel="승인년도" endLabel="" + /> + {/* 조회 버튼 */} + <div className="flex-1 flex justify-end"> + <Button className="text-base py-2" onClick={handleSearch}>조회</Button> + </div> + </div> + </div> + + <div className="h-[79vh] w-full overflow-hidden rounded-lg border"> + {/* 테이블2 영역 (가로/세로 스크롤 가능) */} + <div className="flex-1 px-2 pb-2"> + <LoadingOverlay isLoading={isLoading} /> + <UiTable + headName={(() => { + return (<>조회 결과 ({tableCount.toLocaleString('ko-KR')})건</>); + })() + } + tableData={tableData} + headlist={dynamicHeadlist} + pageSize={10} + total={tableCount} + cellClick={handleReportCellClick} + onPageChange={setCurPage} + resetPageNo={isResetPage} + resetSelection={isResetSelection} + /> + </div> + </div> + </> + ); +}; + +export default React.memo(ReportTab); \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx b/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx new file mode 100644 index 000000000..ed6cdd608 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx @@ -0,0 +1,706 @@ +import LoadingOverlay from '@/components/ui/LoadingBar'; +import { UiModal, UITab, UiTable } from '@common/business_components/ui'; +import axios from 'axios'; +import React, { useEffect, useMemo, useState } from 'react'; +import { ExclnSewagePumpCol2, MsSwageMhlCol2, MsSwagePplCol2, stofcbrCol2, SwtrmtFcltCol2, SwtrmtPrcsZoneCol2 } from '../components/common/table'; +import { type SelectDataType } from '../commonStore'; +import { MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES, MS_SWAGE_MHL_CODES } from '../components/common/table'; +import { getData } from "@/components/lib/api"; +import BasicTab, { BASIC_TYPES } from './basicTab'; +import OperationTab, { BIZ_TY_CD, operationCleaningTableHeadlist, operationInspectionTableHeadlist, operationMaintenanceTableHeadlist, operationTableHeadlist, WORK_TYPES } from './operationTab'; +import ReportTab, { reportTableHeadlist } from './reportTab'; +import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue'; + +const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}`; + +// 탭 +const TABS = [ + { val: "basic", text: "기본정보" }, + { val: "operation", text: "운영정보" }, + { val: "report", text: "하수도정비기본계획보고서" }, +]; + + +// 날짜 조합 함수 +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}`; +}; + +interface SearchModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onFeatureSelect: (row: any) => void; +} + +const SearchModal: React.FC<SearchModalProps> = ({ + open, + onOpenChange, + onFeatureSelect, +}) => { + const [activeTab, setActiveTab] = useState(TABS[0]?.val ?? "basic"); + const [basicType, setBasicType] = useState(BASIC_TYPES[0]?.val ?? ""); + const [workType, setWorkType] = useState(WORK_TYPES[0]?.val ?? ""); + const [showTable, setShowTable] = useState(false); + const [tableData, setTableData] = useState<any[]>([]); + const [detailTableData, setDetailTableData] = useState<any[]>([]); + const [tableCount, setTableCount] = useState(0); + const [detailTableCount, setDetailTableCount] = useState(0); + const [curPage, setCurPage] = useState(1); + const [detailCurPage, setDetailCurPage] = useState(1); + const [isLoading, setIsLoading] = useState(false); + const [isDetailLoading, setIsDetailLoading] = useState(false); + const [isResetPage, setIsResetPage] = useState<boolean>(false); + const [isResetDetailPage, setIsResetDetailPage] = useState<boolean>(false); + const [isResetSelection, setIsResetSelection] = useState<boolean>(false); + const [isResetDetailSelection, setIsResetDetailSelection] = useState<boolean>(false); + const [detailBizId, setDetailBizId] = useState(0); + const [city, setCity] = useState(0); + const [sgg, setSgg] = useState(0); + const [ctpvCd, setCtpvCd] = useState('0'); // 하수정비기본계획 report + const [sigungu, setSigungu] = useState('0'); // 하수정비기본계획 report + const [gubun, setGubun] = useState(""); // 하수정비기본계획 report + const [reportGubun, setReportGubun] = useState("0"); // 하수정비기본계획 report + const [start1, setStart1] = useState(""); + const [start2, setStart2] = useState(""); + const [end1, setEnd1] = useState(""); + const [end2, setEnd2] = useState(""); + const [var1, setVar1] = useState(""); + const [var2, setVar2] = useState(""); + const [var3, setVar3] = useState(""); + const [var4, setVar4] = useState(""); + // 시작, 종료 + const [yearFilter, setYearFilter] = useState({ start: "", end: "" }); + const [filteredSiguns, setFilteredSiguns] = useState<SelectDataType[]>([{ val: '0', text: '전체', sido: '0' }]); + + + // 오늘 날짜 구하기 + 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"); + + const dynamicHeadlist: { id: string, title: string }[] = useMemo(() => { + if (activeTab === 'basic') { + if (basicType === 'tb_ms_swage_ppl') return MsSwagePplCol2; + if (basicType === 'tb_il_swage_prcs_fclt') return SwtrmtFcltCol2; + if (basicType === 'tb_ms_swage_prcs_zone') return SwtrmtPrcsZoneCol2; + if (basicType === 'tb_ms_swage_mhl') return MsSwageMhlCol2; + if (basicType === 'tb_ms_swage_pmstn') return ExclnSewagePumpCol2; + if (basicType === 'tb_ms_stofcbr') return stofcbrCol2; + + return []; + } + if (activeTab === 'operation') return operationTableHeadlist; + + return []; + }, [activeTab, basicType, workType]); + + const dynamicHeadlist2: { id: string, title: string }[] = useMemo(() => { + if (activeTab === 'operation') { + if (workType === WORK_TYPES[0]?.val) return operationMaintenanceTableHeadlist; + if (workType === WORK_TYPES[1]?.val) return operationCleaningTableHeadlist; + if (workType === WORK_TYPES[2]?.val) return operationInspectionTableHeadlist; + } + + return []; + }, [activeTab, workType]); + + useEffect(() => { + // filterParams 초기화 + setStart1(""); + setStart2(""); + setEnd1(""); + setEnd2(""); + setVar1(""); + setVar2(""); + setVar3(""); + setVar4(""); + setCity(0); + setSgg(0); + setYearFilter({ + start: "", + end: "", + }); + setShowTable(false); + setTableData([]); + setDetailTableData([]); + setTableCount(0); + setDetailTableCount(0); + setCurPage(1); // 페이지를 1로 초기화 + setDetailCurPage(1); // 페이지를 1로 초기화 + setIsResetPage(true); // 페이지를 초기화 + setIsResetDetailPage(true); // 디테일 페이지를 초기화 + setIsResetSelection(true); // 선택 초기화 + setIsResetDetailSelection(true); // 선택 초기화 + // queryParams는 fetchFilteredData 내에서 매번 새로 생성되므로 별도 상태 초기화는 필요 없음 + }, [activeTab, basicType, workType, open]); + + useEffect(() => { + setActiveTab(TABS[0]?.val ?? "basic"); + setBasicType(BASIC_TYPES[0]?.val ?? ""); + setWorkType(WORK_TYPES[0]?.val ?? ""); + setTableCount(0); + setTableData([]); + setDetailTableCount(0); + setDetailTableData([]); + }, [open]); + + // 페이지 변경 시마다 데이터 fetch + useEffect(() => { + // showTable이 true일 때만 fetch (최초 렌더 방지) + setIsResetPage(false); + setIsResetDetailPage(true); + if (showTable) fetchFilteredData(); + }, [curPage]); + + // 페이지 변경 시마다 데이터 fetch + useEffect(() => { + setIsResetDetailSelection(false); + // showTable이 true일 때만 fetch (최초 렌더 방지) + if (showTable) { + handleOperationPageNation(Number(detailBizId)); + } + }, [detailCurPage]); + + + // 검색 API 호출 함수 + const fetchFilteredData = async () => { + setIsLoading(true); // 로딩 시작 + setIsResetSelection(true); // 선택 초기화 + setIsResetDetailSelection(true); // 선택 초기화 + + try { + let cqlFilters: string[] = []; + if (activeTab === 'basic') { + const queryParams = new URLSearchParams({ + lyrTypeNm: basicType, + page: `${curPage - 1}`, + size: '10', + sort: 'string', + }); + console.log('basicType', basicType); + + if (basicType === 'tb_ms_swage_prcs_zone') cqlFilters = zoneCqlFilters(); // 하수처리구역 + else if (basicType === 'tb_il_swage_prcs_fclt') cqlFilters = facilityCqlFilters(); // 하수처리시설 + else if (basicType === 'tb_ms_swage_ppl') cqlFilters = pplCqlFilters(); // 하수관로 + else if (basicType === 'tb_ms_swage_mhl') cqlFilters = mhCqlFilters(); // 하수맨홀 + else if (basicType === 'tb_ms_swage_pmstn') cqlFilters = psmtnCqlFilters(); // 하수펌프장 + else if (basicType === 'tb_ms_stofcbr') cqlFilters = otflCqlFilters(); // 우수토실 + + if (cqlFilters.length > 0) queryParams.append('cqlFiter', cqlFilters.join(' and ')); + + console.log('API URL:', `${API_URL}/v1/fclts?${queryParams.toString()}`); + const res = await axios.get(`${API_URL}/v1/fclts?${queryParams.toString()}`); + console.log('필터링된 API 결과(개수):', res.data.count); + console.log('필터링된 API 결과(데이터):', res.data.data); + + // 🎯 각 아이템을 개별적으로 포맷팅하면서 일련번호 추가 + const dataWithRowNumber = (res.data.data || []).map((item: any, index: number) => { + // 🎯 먼저 각 아이템을 포맷팅 + const formattedItem = formatDisplayData(item); + const matched = MS_SWAGE_MHL_CODES.find(code => code.val === item?.mqltCd?.trim()); + + // 추가 처리 및 일련번호 추가 + return { + ...formattedItem, + rowNumber: (curPage - 1) * 10 + index + 1, // 1부터 시작하는 일련번호 추가 + scsYn: item.scsYn === null || item.scsYn === undefined ? 'N' : item.scsYn, // scsYn이 null이면 'N'으로 변경 + mqltCd: matched ? matched.text : item.mqltCd, + }; + }); + + setTableData(dataWithRowNumber); + setTableCount(res.data.count || []); + console.log('tableCount 상태 업데이트 후:', res.data.count); // tableCount 로그 추가 + + } else if (activeTab === 'operation') { + const RequestDTO: Record<string, string> = { + page: `${curPage - 1}`, + size: '4', + }; + if (var1) RequestDTO.bizNm = var1; + if (var2 != '0' && var2) RequestDTO.bizTyCd = var2; + // 수행기간 범위 + const startDate = getDateString( + yearFilter.start, + '', + '', + "19000101", + ); + const endDate = getDateString( + yearFilter.end, + '', + '', + `${todayYear}${todayMonth}${todayDate}`, + ); + RequestDTO.fromflfmtEndYmd = startDate; + RequestDTO.toflfmtEndYmd = endDate; + + // JSON.parse 사용으로 문자열 -> 배열 변환 + RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"')); + + const params = new URLSearchParams(RequestDTO).toString(); + + console.log('API URL:', `/v1/bizInfo?${params}`); + const res = await getData(`v1/bizInfo?${params}`); + console.log('필터링된 API 결과(개수):', res.totalElements); + console.log('필터링된 API 결과(데이터):', res.content); + + const mappedContent = (res.content || []).map((item: any, index: number) => { + console.log("item:", item); + const formattedItem = formatDisplayData(item); + const matched = BIZ_TY_CD.find(code => code.lwpoCd === item.bizTyCd); + return { + ...formattedItem, + rowNumber: (curPage - 1) * 4 + index + 1, // 1부터 시작하는 일련번호 추가 + bizTyCd: matched ? matched.lwpoCdNm : item.bizTyCd, + }; + }); + + setTableData(mappedContent || []); + setTableCount(res.totalElements || 0); + setDetailTableData([]); + setDetailTableCount(0); + } + + setShowTable(true); + } catch (err) { + console.error('API ERROR:', err); + setTableData([]); + } finally { + setIsLoading(false); // 로딩 종료 + // setIsResetSelection(false); + // setIsResetDetailSelection(false); + } + }; + + // 하수관로용 CQL 필터 생성 + const pplCqlFilters = () => { + const cqlFilters: string[] = []; + // 추후 작성 예정 + if (var1) cqlFilters.push(`usg_cd='${var1}'`); + if (var2) cqlFilters.push(`mqlt_cd='${var2}'`); + if (var3) cqlFilters.push(`pbadms_emd_cd='${var3}'`); + // 설치년도 범위 + const startDate = getDateString( + yearFilter.start, + '', + '', + "19000101", + ); + const endDate = getDateString( + yearFilter.end, + '', + '', + `${todayYear}${todayMonth}${todayDate}`, + ); + cqlFilters.push( + `instl_ymd >= '${startDate}' and instl_ymd <= '${endDate}'`, + ); + // 관경(mm) 범위 + if (start1 && end1) cqlFilters.push(`pdmt BETWEEN ${start1} AND ${end1}`); + else if (start1) cqlFilters.push(`pdmt >= ${start1}`); + else if (end1) cqlFilters.push(`pdmt <= ${end1}`); + // 연장(m) 범위 + if (start2 && end2) cqlFilters.push(`prlg BETWEEN ${start2} AND ${end2}`); + else if (start2) cqlFilters.push(`prlg >= ${start2}`); + else if (end2) cqlFilters.push(`prlg <= ${end2}`); + + return cqlFilters; + }; + // 하수처리시설용 CQL 필터 생성 + const facilityCqlFilters = () => { + const cqlFilters: string[] = []; + // 가동개시일자 범위 + const startDate = getDateString( + yearFilter.start, + '', + '', + "19000101", + ); + const endDate = getDateString( + yearFilter.end, + '', + '', + `${todayYear}${todayMonth}${todayDate}`, + ); + cqlFilters.push( + `oprtng_strt_ymd >= '${startDate}' and oprtng_strt_ymd <= '${endDate}'`, + ); + // 시설용량 범위 + if (start1 && end1) + cqlFilters.push(`fclt_cpct BETWEEN ${start1} AND ${end1}`); + else if (start1) cqlFilters.push(`fclt_cpct >= ${start1}`); + else if (end1) cqlFilters.push(`fclt_cpct <= ${end1}`); + // 처리구역 면적 + if (start2 && end2) + cqlFilters.push(`prcsZoneAreaSum BETWEEN ${start2} AND ${end2}`); + else if (start2) cqlFilters.push(`prcsZoneAreaSum >= ${start2}`); + else if (end2) cqlFilters.push(`prcsZoneAreaSum <= ${end2}`); + // 하수처리시설명 + if (var1) cqlFilters.push(`swtrmt_fclt_nm ILIKE '%${var1}%'`); + return cqlFilters; + }; + // 하수처리구역용 CQL 필터 생성 + const zoneCqlFilters = () => { + const cqlFilters: string[] = []; + if (city) cqlFilters.push(`ctpv_cd='${city}'`); + if (sgg) cqlFilters.push(`sgg_cd='${sgg}'`); + if (var1) cqlFilters.push(`swtrmt_zone_nm ILIKE '%${var1}%'`); + if (var2) cqlFilters.push(`swtrmt_fclt_nm ILIKE '%${var2}%'`); + return cqlFilters; + }; + // 하수맨홀용 CQL 필터 생성 + const mhCqlFilters = () => { + const cqlFilters: string[] = []; + // // 설치년도 범위 + const startDate = getDateString( + yearFilter.start, + '', + '', + "19000101", + ); + const endDate = getDateString( + yearFilter.end, + '', + '', + `${todayYear}${todayMonth}${todayDate}`, + ); + cqlFilters.push( + `instl_ymd >= '${startDate}' and instl_ymd <= '${endDate}'`, + ); + if (var1) cqlFilters.push(`usg_cd='${var1}'`); + // 구경(mm) 범위 + if (start1 && end1) cqlFilters.push(`swage_mh_diam BETWEEN ${start1} AND ${end1}`); + else if (start1) cqlFilters.push(`swage_mh_diam >= ${start1}`); + else if (end1) cqlFilters.push(`swage_mh_diam <= ${end1}`); + + return cqlFilters; + }; + // 하수펌프장용 CQL 필터 생성 + const psmtnCqlFilters = () => { + const cqlFilters: string[] = []; + // 설치년도 범위 + const startDate = getDateString( + yearFilter.start, + '', + '', + "19000101", + ); + const endDate = getDateString( + yearFilter.end, + '', + '', + `${todayYear}${todayMonth}${todayDate}`, + ); + cqlFilters.push( + `instl_ymd >= '${startDate}' and instl_ymd <= '${endDate}'`, + ); + // 일일처리용량 범위 + if (start1 && end1) + cqlFilters.push(`daily_prcs_cpct BETWEEN ${start1} AND ${end1}`); + else if (start1) cqlFilters.push(`daily_prcs_cpct >= ${start1}`); + else if (end1) cqlFilters.push(`daily_prcs_cpct <= ${end1}`); + // 양수능력 범위 + if (start2 && end2) + cqlFilters.push(`excln_wpmp_ablt BETWEEN ${start2} AND ${end2}`); + else if (start2) cqlFilters.push(`excln_wpmp_ablt >= ${start2}`); + else if (end2) cqlFilters.push(`excln_wpmp_ablt <= ${end2}`); + + return cqlFilters; + }; + // 우수토실용 CQL 필터 생성 + const otflCqlFilters = () => { + const cqlFilters: string[] = []; + // 설치년도 범위 + const startDate = getDateString( + yearFilter.start, + '', + '', + "19000101", + ); + const endDate = getDateString( + yearFilter.end, + '', + '', + `${todayYear}${todayMonth}${todayDate}`, + ); + cqlFilters.push( + `instl_ymd >= '${startDate}' and instl_ymd <= '${endDate}'`, + ); + + return cqlFilters; + }; + + // 조회 버튼 클릭 시 1페이지로 이동 및 데이터 초기화 + const handleSearch = () => { + setCurPage(1); // 페이지를 1로 초기화 + fetchFilteredData(); + setIsResetPage(true); // 페이지를 초기화 + setIsResetDetailPage(true); // 디테일 페이지를 초기화 + setIsResetSelection(false); // 선택 초기화 + setIsResetDetailSelection(false); // 선택 초기화 + setShowTable(true); + }; + + // 기본정보 테이블 cellClick 핸들러 + const handleBasicCellClick = (index?: number | undefined, headId?: String) => { + if (typeof index === "number") { + onFeatureSelect(tableData[index % 10]); // 10은 page size 하드 코딩한 숫자 + onOpenChange(false); // SearchModal 닫기 + } + }; + // 운영정보 테이블 cellClick 핸들러 + const handleOperationCellClick = async (index?: number | undefined, headId?: String) => { + setIsResetDetailPage(true); // 페이지를 1로 초기화 + setIsResetDetailSelection(true); // 선택 초기화 + setIsResetDetailPage(true); // 디테일 페이지를 초기화 + + if (typeof index === "number") { + setIsDetailLoading(true); + setDetailBizId(tableData[index].bizId); + setDetailCurPage(1); // 상세 페이지 초기화 + try { + const RequestDTO: Record<string, string> = { + page: `${detailCurPage - 1}`, + size: '4', + bizId: tableData[index % 4].bizId, + }; + + RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"')); + + const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`); + console.log('운영정보 cellClick 데이터:', res); + console.log('운영정보 cellClick.data:', res.data); + console.log('운영정보 length:', res.data.length); + + const mappedData = (res.data || []).map((item: any, detailIndex: number) => { + const formattedItem = formatDisplayData(item); + + const usgCdObj = MS_SWAGE_PPL_USE_CODES.find(code => code.val === item.usgCd.trim()); + const mqltCdObj = MS_SWAGE_PPL_CODES.find(code => code.val === item.mqltCd.trim()); + return { + ...formattedItem, + rowNumber: (detailCurPage - 1) * 4 + detailIndex + 1, + usgCd: usgCdObj ? usgCdObj.text : item.usgCd, + mqltCd: mqltCdObj ? mqltCdObj.text : item.mqltCd, + }; + }); + + console.log('운영정보 cellClick mappedData:', mappedData); + + setDetailTableData(mappedData); + setDetailTableCount(res.count || 0); + } catch (error) { + console.error('운영정보 cellClick 에러:', error); + } finally { + setIsDetailLoading(false); + setIsResetDetailPage(false); + setIsResetDetailSelection(false); + } + } + }; + // 운영정보 디테일 테이블 페이지네이션 핸들러 + const handleOperationPageNation = async (index?: number | undefined) => { + if (typeof index === "number") { + setIsDetailLoading(true); + try { + const RequestDTO: Record<string, string> = { + page: `${detailCurPage - 1}`, + size: '4', + bizId: String(detailBizId), + }; + const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`); + + const mappedData = (res.data || []).map((item: any, detailIndex: number) => { + const formattedItem = formatDisplayData(item); + + const usgCdObj = MS_SWAGE_PPL_USE_CODES.find(code => code.val === item.usgCd.trim()); + const mqltCdObj = MS_SWAGE_PPL_CODES.find(code => code.val === item.mqltCd.trim()); + return { + ...formattedItem, + rowNumber: (detailCurPage - 1) * 4 + detailIndex + 1, + usgCd: usgCdObj ? usgCdObj.text : item.usgCd, + mqltCd: mqltCdObj ? mqltCdObj.text : item.mqltCd, + }; + }); + + setDetailTableData(mappedData); + setDetailTableCount(res.count || 0); + } catch (error) { + console.error('운영정보 페이지네이션 에러:', error); + } finally { + setIsDetailLoading(false); + } + } + }; + + + const renderBody = () => ( + <> + {/* 탭 영역 */} + <UITab + defaultValue={TABS[0]?.val} + tabList={TABS.map((tab) => ({ + value: tab.val, + label: tab.text + }))} + onValueChange={(value: string) => setActiveTab(value)} + > + </UITab> + {/* 각각의 탭 영역 */} + {/* 기본정보 탭 */} + {activeTab === 'basic' && ( + <BasicTab + open={open} + basicType={basicType} + city={city} + sgg={sgg} + start1={start1} + start2={start2} + end1={end1} + end2={end2} + var1={var1} + var2={var2} + yearFilter={yearFilter} + handleSearch={handleSearch} + setBasicType={setBasicType} + setCity={setCity} + setSgg={setSgg} + setYearFilter={setYearFilter} + setStart1={setStart1} + setStart2={setStart2} + setEnd1={setEnd1} + setEnd2={setEnd2} + setVar1={setVar1} + setVar2={setVar2} + /> + )} + {/* 운영정보 탭 */} + {activeTab === 'operation' && ( + <OperationTab + open={open} + workType={workType} + var1={var1} + var2={var2} + yearFilter={yearFilter} + handleSearch={handleSearch} + setWorkType={setWorkType} + setVar1={setVar1} + setVar2={setVar2} + setYearFilter={setYearFilter} + /> + )} + {/* 하수도정비기본계획보고서 탭 */} + {activeTab === 'report' && ( + <ReportTab + open={open} + /> + )} + {/* </UITab2> */} + + + {/* 📋 결과 리스트 */} + {/* 하수도정비기본계획보고서 탭 */} + {activeTab === 'basic' && ( + <div className="h-[79vh] w-full overflow-hidden rounded-lg border"> + <div className="flex-1 px-2 pb-2"> + <LoadingOverlay isLoading={isLoading} /> + <UiTable + headName={(() => { + const label = BASIC_TYPES.find((type) => type.val === basicType)?.text + return (<>{label} 조회 결과 ({tableCount.toLocaleString('ko-KR')})건</>); + })() + } + tableData={tableData} + headlist={dynamicHeadlist} + pageSize={10} + total={tableCount} + cellClick={handleBasicCellClick} + onPageChange={setCurPage} + resetPageNo={isResetPage} + resetSelection={isResetSelection} + /> + </div> + </div> + )} + {/* 운영정보 탭 */} + {activeTab === 'operation' && ( + <> + <div className="h-[39vh] w-full overflow-hidden rounded-lg border"> + <div className="flex-1 px-2 pb-2"> + <LoadingOverlay isLoading={isLoading} /> + <UiTable + headName={ + (() => { + const label = WORK_TYPES.find((type) => type.val === workType)?.text + return (<>{label} 사업 현황 ({tableCount.toLocaleString('ko-KR')})건</>); + })() + } + tableData={tableData} + headlist={dynamicHeadlist} + pageSize={4} + total={tableCount} + cellClick={handleOperationCellClick} + onPageChange={setCurPage} + resetPageNo={isResetPage} + resetSelection={isResetSelection} + /> + </div> + </div> + + <div className="h-[39vh] w-full overflow-hidden rounded-lg border"> + {/* 테이블2 영역 */} + <div className="flex-1 px-2 pb-2"> + <LoadingOverlay isLoading={isDetailLoading} /> + <UiTable + headName={ + (() => { + const label = WORK_TYPES.find((type) => type.val === workType)?.text + return (<>{label} 결과표 ({detailTableCount.toLocaleString('ko-KR')})건</>); + })() + } + tableData={detailTableData} + headlist={dynamicHeadlist2} + pageSize={4} + total={detailTableCount} + cellClick={undefined} + onPageChange={setDetailCurPage} + resetPageNo={isResetDetailPage} + resetSelection={isResetDetailSelection} + /> + </div> + </div> + </> + )} + </> + ); + + return ( + <UiModal + open={open} + onOpenChange={onOpenChange} + bodyClass={` + h-[100vh] + w-[60vw] + max-w-[60vw] + max-h-[100vh] + overflow-hidden + z-[101] + `} + body={renderBody()} + useDirectRender={true} + /> + ); +}; + +export default React.memo(SearchModal); diff --git a/apps/sn/app/(map-layout)/map/searchModal/searchModalReportApi.ts b/apps/sn/app/(map-layout)/map/searchModal/searchModalReportApi.ts new file mode 100644 index 000000000..c12021499 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/searchModalReportApi.ts @@ -0,0 +1,226 @@ +import {useState, useEffect} from 'react'; +import { getDcData } from '@/components/lib/api'; + +const ADDR_URL = process.env.NEXT_PUBLIC_ADDR_URL; + +export type Folder = { + clsfSystmSn: string; + upClsfSystmSn: string; + clsfSystmNm: string; + children: Folder[]; + }; + +export type FlattenedFolder = { + [key: string]: string|number|undefined; +}; + +type SelectDataType = { + text: string; + val: string; + sido?: string; // 시도 코드 (선택 사항) +} + +type ReportDataFile = { + no : number; + fileTitle : string; + regDt : string; + fileSn : string; + filePath : string; +} +const isYear = (value: string): boolean => { + return /^[0-9]{4}년$/.test(value); +}; + +export const reportGubuns = [{val: '0', text: '전체'}, {val: '1', text: '보고서'} + , {val: '2', text: '요약보고서'} , {val: '3', text: '부록'}, {val: '4', text: '기타'}]; + +/** + * Flatten a hierarchical folder structure into a flat array. + * @param data - The hierarchical folder data to flatten. + * @returns An array of flattened folders with their paths and levels. + */ +export function flattenFolders(data: Folder[]): FlattenedFolder[] { + const result: FlattenedFolder[] = []; + const exceptNodeNm = ["부분","통합","전체"]; + const addNodeNm = ["보고서","요약보고서","부록","기타"]; // level5 + const columnNms: { [key: number]: string } = { 0: "root", 1:"sido", 2:"sigungu", 3:"approvalYear" + ,4:"plantZoneGugun", 5:"reportGubun", 7:"fileName", 8:"regDate" }; + + function traverse( + node: Folder, + path: { [key: string]: string| number }, + level: number + ) { + if (level === 2 && isYear(node.clsfSystmNm)) { + level++; + } + if (level === 4 && addNodeNm.includes(node.clsfSystmNm)) { + level++; // 레벨을 증가시켜 하위값에 추가되도록 함. + } + + const newPath = { + ...path, + [`${columnNms[level]}`]: node.clsfSystmNm, + }; + + if (node.children.length === 0) { + newPath.no = result.length + 1; + newPath.clsfSystmSn = node.clsfSystmSn; // 파일 조회를 위한 고유번호 + result.push(newPath); + } else { + if (level === 4) { + if (exceptNodeNm.includes(node.clsfSystmNm)) { + level--; // 제외할 노드 이름인 경우, 현재의 레벨에 하위값이 써지도록 함. + } + } + node.children.forEach((child) => { + traverse(child, newPath, level + 1); + }); + } + } + + data.forEach((root) => traverse(root, {}, 0)); + return result; +} + +export const getDcTree = async (clsfSystmNm:string) => { + const result = await getDcData("/api/v1/dc/clsf?clsfSystmNm=" + clsfSystmNm); + return result; +}; +export const useDcTree = (clsfSystmNm:string) => { + const [data, setData] = useState([]); + + useEffect(() => { + const fetch = async () => { + const result = await getDcTree(clsfSystmNm); + if (result) { + setData(result); + } + }; + fetch(); + console.log("useDcTree호출"); + }, [clsfSystmNm]); + + return data; +}; +export const getDcFile = async (clsfSystmSn:string) => { + const queryString = 'clsfSystmSn='+clsfSystmSn; + const result = await getDcData(`/api/v1/dc/file${queryString ? `?${queryString}` : ''}`); + return result; +}; +export const addFileName = async (data:FlattenedFolder[]) => { + const fileReportData = await Promise.all( + data.map(async (item) => { + const filenameArr:ReportDataFile[] = await getDcFile(String(item.clsfSystmSn)); + const f = filenameArr[0]; + const result = {...item, fileName:f?.fileTitle, regDate:f?.regDt, fileSn:f?.fileSn, filePath:f?.filePath}; + + return result; + }) + ); + return fileReportData; +} +export const getDcFileprcs = async (fileSn:string) => { + const queryString = 'fileSn='+fileSn; + const result = await getDcData(`/api/v1/dc/fileprcs${queryString ? `?${queryString}` : ''}`); + return result; +}; + +export const getDcFileDownload = async (filePath:string) => { + const queryString = 'filePath='+ encodeURIComponent(filePath); + const response = await fetch(`/dc-service/api/v1/dc/file/download?${queryString}`, { + method: 'GET', + }); + if (!response.ok) { + throw new Error(`File download failed: ${response.status}`); + } + const blob = await response.blob(); // PDF, image 등은 blob으로 받아야 함 + return blob; +}; +export const fileDownload = (data:Blob, fileName:string) => { // Blob데이터 임시link로 연결 + const url = window.URL.createObjectURL(data); + const link = document.createElement('a'); + link.href = url; + link.download = fileName; // 원하는 파일명 지정 + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); +} +export const getFileDownload = async (fileSn:string, fileName:string, filePath:string) => { + if (filePath === "") alert("filePath 정보 없으므로 다운로드 불가:filesn"+ fileSn); + const blobData = await getDcFileDownload(filePath); // 실제 다운로드 API + fileDownload(blobData, fileName); +} +// 시도 데이터 +export const getCityCd = async (): Promise<SelectDataType[]> => { + try { + const response = await fetch(`${ADDR_URL}/api/v1/addr/city`, { method: "GET", headers: { accept: "application/json" } }); + if (!response.ok) throw new Error(`시도 데이터 오류: ${response.status}`); + const res = await response.json(); + const city = res.data; + let citys:SelectDataType[] = city.map((item: {adstDstrCd:string, sido:string}) => ({ + val: item.adstDstrCd, + text: item.sido, + })); + citys = citys.sort((a:SelectDataType, b:SelectDataType) => a.text.localeCompare(b.text)); + citys.unshift({ val: '0', text: '전체' }); + return citys; + } catch (error) { + console.error('시도 데이터를 가져오는데 실패했습니다:', error); + return [{ val: '0', text: '전체', sido: '0' }]; + } + }; + +export const useSggCd = () => { + const [data, setData] = useState<SelectDataType[]>([]); + + useEffect(() => { + const fetch = async () => { + const result = await getSggCd(); + if (result) { + setData(result); + } + console.log("useSggCd호출"); + }; + fetch(); + }, []); + return data; +}; + +// 시군구 데이터 +export const getSggCd = async (): Promise<SelectDataType[]> => { + try { + const response = await fetch(`${ADDR_URL}/api/v1/addr/sgg`, { method: "GET", headers: { accept: "application/json" } }); + if (!response.ok) throw new Error(`시군구 데이터 오류: ${response.status}`); + const res = await response.json(); + const sgg = res.data; + const sggCd = sgg.map((item: {adstDstrCd:string, sido:string, gugun:string}) => ({ + val: item.adstDstrCd, + sido: item.sido, + text: item.gugun, + })); + sggCd.unshift({ val: '0', text: '전체' }); // "전체" 옵션 추가 + return sggCd; + } catch (error) { + console.error('시군구 데이터를 가져오는데 실패했습니다:', error); + return [{ val: '0', text: '전체', sido: '0' }]; + } +}; + +export const useCityCd = () => { + const [data, setData] = useState<SelectDataType[]>([]); + + useEffect(() => { + const fetch = async () => { + const result = await getCityCd(); + if (result) { + setData(result); + } + console.log("useCityCd호출"); + }; + fetch(); + }, []); + return data; +}; + diff --git a/apps/sn/app/(map-layout)/map/searchModal/stofcbr.tsx b/apps/sn/app/(map-layout)/map/searchModal/stofcbr.tsx new file mode 100644 index 000000000..700d60cf0 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/stofcbr.tsx @@ -0,0 +1,38 @@ +// 우수토실 + +import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox'; +import { Button } from '@common/components'; +import React from 'react'; + +interface StofcbrProps { + yearFilter: { start: string; end: string }; + setYearFilter: (filter: { start: string; end: string }) => void; + handleSearch: () => void; +} + +const Stofcbr: React.FC<StofcbrProps> = ({ + yearFilter, + setYearFilter, + handleSearch, +}) => { + return ( + <div className="mb-3"> + {/* 설치년도 */} + <div className="flex items-center mb-2"> + <YearRangeSelectBox + start={yearFilter.start} + end={yearFilter.end} + onChange={range => setYearFilter({ ...yearFilter, start: range.start, end: range.end })} + startLabel="설치년도" + endLabel="" + /> + {/* 조회 버튼 */} + <div className="flex-1 flex justify-end"> + <Button className="text-base py-2" onClick={handleSearch}>조회</Button> + </div> + </div> + </div> + ); +}; + +export default React.memo(Stofcbr); \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/searchModal/swageMhl.tsx b/apps/sn/app/(map-layout)/map/searchModal/swageMhl.tsx new file mode 100644 index 000000000..5d678bf1a --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/swageMhl.tsx @@ -0,0 +1,103 @@ +// 하수맨홀 + +import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox'; +import { Button, Input, Label } from '@common/components'; +import React from 'react'; +import { MS_SWAGE_MHL_USE_CODES } from '../components/common/table'; +import { SelectBox } from '@common/business_components/ui'; + + +interface SwageMhlProps { + var1: string; + start1: string; + end1: string; + yearFilter: { start: string; end: string }; + setVar1: (value: string) => void; + setStart1: (value: string) => void; + setEnd1: (value: string) => void; + setYearFilter: (filter: { start: string; end: string }) => void; + handleSearch: () => void; +} + +const SwageMhl: React.FC<SwageMhlProps> = ({ + var1, + start1, + end1, + yearFilter, + setVar1, + setStart1, + setEnd1, + setYearFilter, + handleSearch, +}) => { + + const handleEnterKey = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (e.key === 'Enter') { + handleSearch(); + } + }; + + return ( + <div className="mb-3"> + <div className="mb-2 flex items-center"> + {/* 설치년도 */} + <YearRangeSelectBox + start={yearFilter.start} + end={yearFilter.end} + onChange={(range) => + setYearFilter({ + ...yearFilter, + start: range.start, + end: range.end, + }) + } + startLabel="설치년도" + endLabel="" + /> + {/* 용도 */} + <SelectBox + className="ml-8 w-60 mr-8" + selectClass="rounded-lg bg-white text-xl" + label="용도" + selectArray={MS_SWAGE_MHL_USE_CODES} + value={var1} + onSelectValue={(val) => { + setVar1(String(val)); + }} + selectContentClass="z-[101]" + /> + {/* 하수맨홀 구경 */} + <Label className="mr-2 w-auto text-xl text-gray-600">구경(mm)</Label> + <Input + className="w-[100px] !text-xl placeholder:text-center" + placeholder="입력" + value={start1} + onChange={(e) => { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setStart1(val); + }} + onKeyDown={handleEnterKey} + /> + <span className="mx-2 text-xl font-semibold">~</span> + <Input + className="mr-8 w-[100px] !text-xl placeholder:text-center" + placeholder="입력" + value={end1} + onChange={(e) => { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setEnd1(val); + }} + onKeyDown={handleEnterKey} + /> + {/* 조회 버튼 */} + <div className="flex-1 flex justify-end"> + <Button className="text-base py-2" onClick={handleSearch}>조회</Button> + </div> + </div> + </div> + ); +}; + +export default React.memo(SwageMhl); \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/searchModal/swagePmstn.tsx b/apps/sn/app/(map-layout)/map/searchModal/swagePmstn.tsx new file mode 100644 index 000000000..9543cf218 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/swagePmstn.tsx @@ -0,0 +1,113 @@ +// 하수펌프장 + +import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox'; +import { Button, Input, Label } from '@common/components'; +import React from 'react'; + +interface SwagePmstnProps { + start1: string; + start2: string; + end1: string; + end2: string; + yearFilter: { start: string; end: string }; + setStart1: (value: string) => void; + setStart2: (value: string) => void; + setEnd1: (value: string) => void; + setEnd2: (value: string) => void; + setYearFilter: (filter: { start: string; end: string }) => void; + handleSearch: () => void; +} + +const SwagePmstn: React.FC<SwagePmstnProps> = ({ + start1, + start2, + end1, + end2, + yearFilter, + setStart1, + setStart2, + setEnd1, + setEnd2, + setYearFilter, + handleSearch, +}) => { + + const handleEnterKey = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (e.key === 'Enter') { + handleSearch(); + } + }; + + return ( + <div className="mb-3"> + <div className="flex items-center mb-2"> + {/* 설치년도 */} + <YearRangeSelectBox + start={yearFilter.start} + end={yearFilter.end} + onChange={range => setYearFilter({ ...yearFilter, start: range.start, end: range.end })} + startLabel="설치년도" + endLabel="" + /> + {/* 일일처리용량 - 오수맨홀 */} + <Label className="ml-8 mr-2 w-auto text-xl text-gray-600">일일처리용량(㎥)</Label> + <Input + className="w-[100px] !text-xl placeholder:text-center" + placeholder="입력" + value={start1} + onChange={(e) => { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setStart1(val); + }} + onKeyDown={handleEnterKey} + /> + <span className="mx-2 text-xl font-semibold">~</span> + <Input + className="mr-8 w-[100px] !text-xl placeholder:text-center" + placeholder="입력" + value={end1} + onChange={(e) => { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setEnd1(val); + }} + onKeyDown={handleEnterKey} + /> + {/* 양수능력 - 우수맨홀 */} + <Label className="mr-2 w-auto text-xl text-gray-600"> + 양수능력(m²/d) + </Label> + <Input + className="w-[100px] !text-xl placeholder:text-center" + placeholder="입력" + value={start2} + onChange={(e) => { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setStart2(val); + }} + onKeyDown={handleEnterKey} + /> + <span className="mx-2 text-xl font-semibold">~</span> + <Input + className="mr-8 w-[100px] !text-xl placeholder:text-center" + placeholder="입력" + value={end2} + onChange={(e) => { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setEnd2(val); + }} + onKeyDown={handleEnterKey} + /> + {/* 조회 버튼 */} + <div className="flex-1 flex justify-end"> + <Button className="text-base py-2" onClick={handleSearch}>조회</Button> + </div> + </div> + </div> + ); +}; + +export default React.memo(SwagePmstn); \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/searchModal/swagePpl.tsx b/apps/sn/app/(map-layout)/map/searchModal/swagePpl.tsx new file mode 100644 index 000000000..7a22f3834 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/swagePpl.tsx @@ -0,0 +1,143 @@ +// 하수관로 + +import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox'; +import { SelectBox } from '@common/business_components/ui'; +import { Button, Input, Label } from '@common/components'; +import React from 'react'; +import { MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES } from '../components/common/table'; + +interface SwagePplProps { + var1: string; + var2: string; + start1: string; + end1: string; + yearFilter: { start: string; end: string }; + setVar1: (value: string) => void; + setVar2: (value: string) => void; + setStart1: (value: string) => void; + setEnd1: (value: string) => void; + setYearFilter: (filter: { start: string; end: string }) => void; + handleSearch: () => void; +} + +const SwagePpl: React.FC<SwagePplProps> = ({ + var1, + var2, + start1, + end1, + yearFilter, + setVar1, + setVar2, + setStart1, + setEnd1, + setYearFilter, + handleSearch, +}) => { + + const handleEnterKey = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (e.key === 'Enter') { + handleSearch(); + } + }; + + return ( + <div className="mb-6"> + <div className="mb-2 flex items-center"> + {/* 설치년도 */} + <YearRangeSelectBox + start={yearFilter.start} + end={yearFilter.end} + onChange={(range) => + setYearFilter({ + ...yearFilter, + start: range.start, + end: range.end, + }) + } + startLabel="설치년도" + endLabel="" + /> + {/* 용도 */} + <SelectBox + className="ml-8 w-60 mr-8" + selectClass="rounded-lg bg-white text-xl" + label="용도" + selectArray={MS_SWAGE_PPL_USE_CODES} + value={var1} + onSelectValue={(val) => { + setVar1(String(val)); + }} + selectContentClass="z-[101]" + /> + {/* 재질 */} + <SelectBox + className="w-60 mr-8" + selectClass="rounded-lg bg-white text-xl" + label="재질" + selectArray={MS_SWAGE_PPL_CODES} + value={var2} + onSelectValue={(val) => { + setVar2(String(val)); + }} + selectContentClass="z-[101]" + /> + {/* 관경 */} + <Label className="mr-2 w-auto text-xl text-gray-600">관경(mm)</Label> + <Input + className="w-[60px] !text-xl placeholder:text-center" + placeholder="입력" + value={var1} + onChange={(e) => { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setVar1(val); + }} + onKeyDown={handleEnterKey} + /> + <span className="mx-2 text-xl font-semibold">~</span> + <Input + className="mr-8 w-[60px] !text-xl placeholder:text-center" + placeholder="입력" + value={end1} + onChange={(e) => { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setEnd1(val); + }} + onKeyDown={handleEnterKey} + /> + {/* 연장 */} + <Label className="mr-2 w-auto text-xl text-gray-600">연장(m)</Label> + <Input + className="w-[60px] !text-xl placeholder:text-center" + placeholder="입력" + value={start1} + onChange={(e) => { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setStart1(val); + }} + onKeyDown={handleEnterKey} + /> + <span className="mx-2 text-xl font-semibold">~</span> + <Input + className="mr-8 w-[60px] !text-xl placeholder:text-center" + placeholder="입력" + value={end1} + onChange={(e) => { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setEnd1(val); + }} + onKeyDown={handleEnterKey} + /> + {/* 조회 버튼 */} + <div className="flex-1 flex justify-end"> + <Button className="text-base py-2" onClick={handleSearch}>조회</Button> + </div> + </div> + </div> + ); +}; + +export default React.memo(SwagePpl); \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/searchModal/swagePrcsFclt.tsx b/apps/sn/app/(map-layout)/map/searchModal/swagePrcsFclt.tsx new file mode 100644 index 000000000..4adfeb865 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/swagePrcsFclt.tsx @@ -0,0 +1,133 @@ +// 하수처리시설 + +import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox'; +import { Button, Input, Label } from '@common/components'; +import React from 'react'; + +interface SwagePrcsFcltProps { + var1: string; + start1: string; + start2: string; + end1: string; + end2: string; + yearFilter: { start: string; end: string }; + setVar1: (value: string) => void; + setStart1: (value: string) => void; + setStart2: (value: string) => void; + setEnd1: (value: string) => void; + setEnd2: (value: string) => void; + setYearFilter: (filter: { start: string; end: string }) => void; + handleSearch: () => void; +} + +const SwagePrcsFclt: React.FC<SwagePrcsFcltProps> = ({ + var1, + start1, + start2, + end1, + end2, + yearFilter, + setVar1, + setStart1, + setStart2, + setEnd1, + setEnd2, + setYearFilter, + handleSearch, +}) => { + + const handleEnterKey = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (e.key === 'Enter') { + handleSearch(); + } + }; + + return ( + <div className="mb-3"> + {/* 가동개시일자 */} + <div className="mb-2 flex items-center"> + <Label className="mr-2 w-auto text-xl text-gray-600">가동개시일자</Label> + <YearRangeSelectBox + start={yearFilter.start} + end={yearFilter.end} + onChange={(range) => + setYearFilter({ + ...yearFilter, + start: range.start, + end: range.end, + }) + } + startLabel="" + endLabel="" + /> + {/* 시설용량 */} + <Label className="ml-8 mr-2 w-auto text-xl text-gray-600">시설용량(㎥/d)</Label> + <Input + className="w-[60px] !text-xl placeholder:text-center" + placeholder="입력" + value={start1} + onChange={(e) => { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setStart1(val); + }} + onKeyDown={handleEnterKey} + /> + <span className="mx-2 text-xl font-semibold">~</span> + <Input + className="mr-8 w-[60px] !text-xl placeholder:text-center" + placeholder="입력" + value={end1} + onChange={(e) => { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setEnd1(val); + }} + onKeyDown={handleEnterKey} + /> + {/* 처리구역 면적 */} + <Label className="mr-2 w-auto text-xl text-gray-600">처리구역 면적(m²)</Label> + <Input + className="w-[60px] !text-xl placeholder:text-center" + placeholder="입력" + value={start2} + onChange={(e) => { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setStart2(val); + }} + onKeyDown={handleEnterKey} + /> + <span className="mx-2 text-xl font-semibold">~</span> + <Input + className="mr-8 w-[60px] !text-xl placeholder:text-center" + placeholder="입력" + value={end2} + onChange={(e) => { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setEnd2(val); + }} + onKeyDown={handleEnterKey} + /> + {/* 하수처리시설명 */} + <Label className="mr-2 w-auto text-xl text-gray-600">하수처리시설명</Label> + <Input + className="mr-8 w-[100px] !text-xl placeholder:text-center" + placeholder="입력" + value={var1} + onChange={(e) => { + setVar1(e.target.value); + }} + onKeyDown={handleEnterKey} + /> + {/* 조회 버튼 */} + <div className="flex-1 flex justify-end"> + <Button className="text-base py-2" onClick={handleSearch}>조회</Button> + </div> + </div> + </div> + ); +}; + +export default React.memo(SwagePrcsFclt); \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/searchModal/swagePrcsZone.tsx b/apps/sn/app/(map-layout)/map/searchModal/swagePrcsZone.tsx new file mode 100644 index 000000000..fd49233bb --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/swagePrcsZone.tsx @@ -0,0 +1,78 @@ +// 하수처리구역 + +import { SidoSigunguSelectBox } from '@/components/ui/SidoSigunguSelectBox'; +import { Button, Input, Label } from '@common/components'; +import React from 'react'; + +interface swagePrcsZoneProps { + var1: string; + var2: string; + city: number; + sgg: number; + setVar1: (value: string) => void; + setVar2: (value: string) => void; + setCity: (value: number) => void; + setSgg: (value: number) => void; + handleSearch: () => void; +} + +const swagePrcsZone: React.FC<swagePrcsZoneProps> = ({ + var1, + var2, + city, + sgg, + setVar1, + setVar2, + setCity, + setSgg, + handleSearch, +}) => { + + const handleEnterKey = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (e.key === 'Enter') { + handleSearch(); + } + }; + + return ( + <div className="mb-3"> + <div className="mb-2 flex items-center"> + <SidoSigunguSelectBox + sido={city} + sigungu={sgg} + onSidoChange={(sidoId) => { + setCity(sidoId); + setSgg(0); + }} + onSigunguChange={setSgg} + /> + <Label className="mr-2 w-auto text-xl text-gray-600">하수처리구역명</Label> + <Input + className="mr-8 w-[100px] !text-xl placeholder:text-center" + placeholder="입력" + value={var1} + onChange={(e) => { + setVar1(e.target.value); + }} + onKeyDown={handleEnterKey} + /> + <Label className="mr-2 w-auto text-xl text-gray-600">하수처리시설명</Label> + <Input + className='!text-xl w-[120px] placeholder:text-center mr-8' + placeholder="입력" + value={var2} + onChange={(e) => { + setVar2(e.target.value); + }} + onKeyDown={handleEnterKey} + /> + {/* 조회 버튼 */} + <div className="flex-1 flex justify-end"> + <Button className="text-base py-2" onClick={handleSearch}>조회</Button> + </div> + </div> + </div> + ); +}; + +export default React.memo(swagePrcsZone); \ No newline at end of file diff --git a/apps/sn/app/(with-base-layout)/fileValidation.tsx b/apps/sn/app/(with-base-layout)/fileValidation.tsx new file mode 100644 index 000000000..c0711a7d2 --- /dev/null +++ b/apps/sn/app/(with-base-layout)/fileValidation.tsx @@ -0,0 +1,48 @@ +/** + * 파일 확장자 검증 유틸리티 + */ + +// 파일 확장자 검증 함수 +export const validateFileExtension = (file: File, allowedExtensions: string[]): boolean => { + const fileName = file.name.toLowerCase(); + return allowedExtensions.some(ext => fileName.endsWith(ext.toLowerCase())); +}; + +// 파일 확장자 검증 결과 타입 +export interface FileValidationResult { + isValid: boolean; + errorMessage?: string; +} + +// 파일 검증 설정 타입 +export interface FileValidationConfig { + file: File; + allowedExtensions: string[]; + fileDescription: string; +} + +/** + * 여러 파일의 확장자를 한 번에 검증하는 함수 + * @param validationConfigs 검증할 파일들의 설정 배열 + * @returns 검증 결과. 실패 시 첫 번째 에러 메시지 반환 + */ +export const validateMultipleFiles = (validationConfigs: FileValidationConfig[]): FileValidationResult => { + for (const config of validationConfigs) { + if (!validateFileExtension(config.file, config.allowedExtensions)) { + const allowedExtStr = config.allowedExtensions.join(', '); + return { + isValid: false, + errorMessage: `${config.fileDescription} 파일은 ${allowedExtStr} 확장자만 업로드 가능합니다.` + }; + } + } + + return { isValid: true }; +}; + +// 자주 사용되는 확장자 상수 +export const FILE_EXTENSIONS = { + INP: ['.inp'], + EXCEL: ['.xls', '.xlsx', '.xlsm', '.xltx'], + ALL_OFFICE: ['.xls', '.xlsx', '.xlsm', '.xltx', '.doc', '.docx', '.ppt', '.pptx'] +}; \ No newline at end of file diff --git a/apps/sn/app/(with-base-layout)/ii/BaseLayout.tsx b/apps/sn/app/(with-base-layout)/ii/BaseLayout.tsx new file mode 100644 index 000000000..f0a649ad4 --- /dev/null +++ b/apps/sn/app/(with-base-layout)/ii/BaseLayout.tsx @@ -0,0 +1,305 @@ +'use client' +import { getData } from '@/components/lib/api' +import ContentLayout from '@/components/ui/contentLayout' +import { GovSidoSigunguSearchForm } from '@/components/ui/govSidoSigunguSearchForm' +import { DatePicker, DateMonthPicker, Form, SearchFormRight, SelectBox } from '@common/business_components/ui' +import ListDiv from '@common/business_components/ui/list_div' +import { Button, Label } from '@common/components/ui' +import { useEffect, useState, type ReactNode } from 'react' +import LoadingOverlay from '@/components/ui/LoadingBar' + +type TitleName = { + mainTitle: string + firstCardTitle: string + secondCardTitle?: string +} +type SelectBox = { + labelNm?: Label + searchChk?: boolean + title?: string; + onSearchClick?: () => Promise<void>; +} +type Label = { + first: string + second: string + third: string + fourth: string +} +// API로부터 받아올 데이터 타입을 정의 (SelectBox에 사용될 형태) +interface ApiSelectOption { + text: string; + val: string; +} +type BaseLayout = { + children?: ReactNode[] + titleName: TitleName + form: any + onSubmitEvent: Function + graphBtn?: boolean + analysisBtn?: string + firstDivBtn?: boolean + selectBox?: SelectBox + showDatePicker?: boolean; + enoCd?: string | number + setEnoCd?: (enoCd: string) => void + ctpvCd?: string | number + setCtpvCd?: (ctpvCd: string) => void + sigungu?: ApiSelectOption + setSigungu?: (sigungu: ApiSelectOption) => void + selectedSwageTreatmentPlant?: string | number; + onSwageTreatmentPlantChange?: (value: string | number) => void; + selectedSwageProcessingZone?: string | number; + onSwageProcessingZoneChange?: (value: string | number) => void; + aplcnYmFrom?: string + setAplcnYmFrom?: (aplcnYmFrom: string) => void; + aplcnYmTo?: string + setAplcnYmTo?: (aplcnYmTo: string) => void; + isLoading?: boolean; + // Excel 다운로드 관련 props 추가 + onExcelDownload?: () => void; + onDeleteFile?: () => void; + onAnalysis?: () => void; + selectedRowData?: any; +} +// TODO: 나중에 이벤트 추가되면 파라미터 추가 필요 ? +/** + * + * @param children + * @param titleName : 메인 타이틀, 첫번째 요소 타이틀, 두번째 요소 타이틀 + * @param form : zod 스키마 + * @param onSubmitEvent : 폼 이벤트 처리 + * @param graphBtn : 하단에 그래프 버튼 유무 처리 + * @returns + */ +const BaseLayout = ({ + children, + titleName, + form, + onSubmitEvent, + graphBtn = false, + analysisBtn, + selectBox, + showDatePicker = false, + enoCd, + setEnoCd, + ctpvCd, + setCtpvCd, + sigungu, + setSigungu, + selectedSwageTreatmentPlant, + onSwageTreatmentPlantChange, + selectedSwageProcessingZone, + onSwageProcessingZoneChange, + aplcnYmFrom, + setAplcnYmFrom, + aplcnYmTo, + setAplcnYmTo, + isLoading, + // Excel 다운로드 관련 props 추가 + onExcelDownload, + onDeleteFile, + onAnalysis, + selectedRowData +}: BaseLayout) => { + + const defaultOption: ApiSelectOption = { val: '0', text: '전체' }; + + const [processingZoneOptions, setProcessingZoneOptions] = useState<ApiSelectOption[]>([]); + const [treatmentFacilityOptions, setTreatmentFacilityOptions] = useState<ApiSelectOption[]>([]); + const [isLoadingAreaData, setIsLoadingAreaData] = useState<boolean>(isLoading || false); + const [atflId, setAtflId] = useState('') + const [fileNm, setFileNm] = useState('') + const [dcrpPplMtncBizSn, setDcrpPplMtncBizSn] = useState('') + + useEffect(() => { + const fetchData = async () => { + if (!sigungu || sigungu.val === '0' || !sigungu.val) { + console.log("BaseLayout: Skipping fetchData because sigungu.val is:", sigungu?.val); + setProcessingZoneOptions([defaultOption]); + setTreatmentFacilityOptions([defaultOption]); + setIsLoadingAreaData(false); + return; + } + + setIsLoadingAreaData(true); + try { + // const queryParamsZone = new URLSearchParams({ + // lyrTypeNm: 'tb_ms_swage_prcs_zone', + // page: '0', + // size: '100000', + // sort: 'string', + // }); + // const cqlFiltersZone: string[] = []; + // if (sigungu.val && sigungu.val !== '0') { + // // console.log("시군구 코드", sigungu.val); + // cqlFiltersZone.push(`sgg_cd='${sigungu.val}'`); + // } + // if (cqlFiltersZone.length > 0) { + // queryParamsZone.append('cqlFiter', cqlFiltersZone.join(' and ')); + // } + + const queryParamsFacility = new URLSearchParams({ + lyrTypeNm: 'tb_il_swage_prcs_fclt', + page: '0', + size: '100000', // 요청하신 URL에 맞게 50으로 변경 가능 + sort: 'string', + }); + const cqlFiltersFacility: string[] = []; + if (sigungu.text && sigungu.val !== '0') { + cqlFiltersFacility.push(`sgg_nm ILIKE '%${sigungu.text}%'`); + } + if (cqlFiltersFacility.length > 0) { + queryParamsFacility.append('cqlFiter', cqlFiltersFacility.join(' and ')); // API 명세에 따라 cqlFilter 또는 cqlFiter + } + + // console.log("BaseLayout API URL for Zones:", `v1/fclts?${queryParamsZone.toString()}`); + console.log("BaseLayout API URL for Facilities:", `v1/fclts?${queryParamsFacility.toString()}`); + + // const [zoneRes, facilityRes] = await Promise.all([ + // getData(`v1/fclts?${queryParamsZone.toString()}`), + // getData(`v1/fclts?${queryParamsFacility.toString()}`) + // ]); + const facilityRes = await getData(`v1/fclts?${queryParamsFacility.toString()}`); + // const zones = (zoneRes.data || []).map((item: any) => ({ + // text: item.swtrmtZoneNm || item.name || `구역 ${item.id || item.gid || item.objectid}`, + // val: item.fid || item.id || item.gid || item.objectid + // })); + // setProcessingZoneOptions([defaultOption, ...zones]); + // console.log("BaseLayout Fetched Processing Zones:", [defaultOption, ...zones]); + + + const facilities = (facilityRes.data || []).map((item: any) => ({ + text: item.swtrmtFcltNm || item.name || `시설 ${item.id || item.gid || item.objectid}`, + val: item.fid || item.id || item.gid || item.objectid + })); + setTreatmentFacilityOptions([defaultOption, ...facilities]); + console.log("BaseLayout Fetched Treatment Facilities:", [defaultOption, ...facilities]); + + } catch (error) { + console.error("BaseLayout: 지역/시설 데이터 조회 중 오류 발생:", error); + setProcessingZoneOptions([defaultOption]); + setTreatmentFacilityOptions([defaultOption]); + } finally { + setIsLoadingAreaData(false); + } + }; + + fetchData(); + }, [sigungu]); // sigungu 상태가 변경될 때마다 실행 + + // 파일 다운로드 + const isChckFile = () => { + // Excel 다운로드 함수가 있으면 Excel 다운로드 실행 + if (onExcelDownload) { + onExcelDownload(); + return; + } + return true + } + // 파일 삭제 + const isDeleteFile = async () => { + // Excel 다운로드 함수가 있으면 Excel 다운로드 실행 + if (onDeleteFile) { + onDeleteFile(); + return; + } + } + + return ( + <Form {...form} className="min-h-screen flex flex-col"> + <LoadingOverlay isLoading={isLoading ?? false} /> + <ListDiv> + {/* searchBox.title이 있으면 h3 태그로 제목을 표시합니다. */} + {selectBox?.title && ( + <div className="card-header"> + <h3 className="text-2xl font-medium">{selectBox.title}</h3> + </div> + )} + <div className="flex items-center w-full"> + {showDatePicker && ( // showDatePicker 값에 따라 DatePicker 렌더링 + <> + <Label className="mr-4 w-20 text-xl text-gray-600 whitespace-nowrap">상수사용기간</Label> + <DateMonthPicker + field={{ + value: aplcnYmFrom || (() => { // aplcnYm이 없으면 현재 연월을 기본값으로 설정 + const now = new Date(); + const year = now.getFullYear(); + // const month = String(now.getMonth() + 1).padStart(2, '0'); + // return `${year}${month}`; + return `${year}01`; + })(), // yyyyMM 형식 + onChange: (value: string) => { + setAplcnYmFrom?.(value); // string 값을 직접 전달 + } + }} + selectClass="ml-5 w-[100px]" + startYear={1900} // 시작 연도 설정 + /> + <span className="font-semibold text-xl mx-2">~</span> + <DateMonthPicker + field={{ + value: aplcnYmTo || (() => { // aplcnYm이 없으면 현재 연월을 기본값으로 설정 + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + return `${year}${month}`; + })(), // yyyyMM 형식 + onChange: (value: string) => { + setAplcnYmTo?.(value); // string 값을 직접 전달 + } + }} + selectClass="w-[100px]" + startYear={1900} // 시작 연도 설정 + /> + </> + )} + <GovSidoSigunguSearchForm + isShowPart={true} + setRtnEnvPartValue={(val) => setEnoCd?.(val)} + setRtnEnvPartSidoValue={(val) => setCtpvCd?.(val)} + setRtnEnvPartSigunValue={setSigungu} // 선택된 시군구 값을 받을 콜백 함수 전달 + > + <SelectBox label={"하수처리장"} value={selectedSwageTreatmentPlant} selectArray={treatmentFacilityOptions} className="col-span-3" selectClass="ml-5 rounded-lg bg-white truncate" onSelectValue={(val) => onSwageTreatmentPlantChange?.(val)} selectContentClass="z-[101]" /> + {/* <SelectBox label={"하수처리구역"} value={selectedSwageProcessingZone} selectArray={processingZoneOptions} className="col-span-3" selectClass="rounded-lg bg-white truncate" onSelectValue={(val) => onSwageProcessingZoneChange?.(val)} selectContentClass="z-[101]" /> */} + </GovSidoSigunguSearchForm> + {selectBox?.searchChk ? <SearchFormRight><Button size="sm" onClick={() => selectBox.onSearchClick?.()}>조회</Button></SearchFormRight> : null} + </div> + </ListDiv> + <ListDiv> + <div className="card-header"> + <h3 className="text-2xl font-medium">{titleName.firstCardTitle}</h3> + <div> + {graphBtn ? <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" onClick={isChckFile}>파일 다운로드</Button> : null} + {/* 그래프 있을 시 */} + {graphBtn ? <Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size="sm" onClick={isDeleteFile}>삭제</Button> : null} + {/* 분석 버튼 */} + {analysisBtn ? <Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size="sm" onClick={onAnalysis}>{analysisBtn}</Button> : null} + </div> + </div> + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmitEvent)} className="flex-grow flex flex-col"> + <div className="card !m-[1.2rem] flex-grow flex flex-col"> + {/* 폼 정보 입력 칸 */} + {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> + {graphBtn ? <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> : null} + {/* 그래프 있을 시 */} + {graphBtn ? <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> : null} + </div> + </div> + <div className="card !m-[1.2rem]">{children?.[1]}</div> + </ListDiv> : null + } + </Form> + ) +} + +export default BaseLayout \ No newline at end of file diff --git a/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx b/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx new file mode 100644 index 000000000..4e8492f47 --- /dev/null +++ b/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx @@ -0,0 +1,466 @@ +'use client' +import { postData } from '@/components/lib/api' +import FileUploadForm from '@/components/ui/fileUploadForm' +import FormSection from '@/components/ui/formSection' +import { DateMonthPicker, filePicked } from '@common/business_components/ui' +import ListDiv from '@common/business_components/ui/list_div' +import { Input } from '@common/components/ui' +import { zodResolver } from '@hookform/resolvers/zod' +import { useState } from 'react' +import { useForm } from 'react-hook-form' +import { z } from 'zod' +import { excelToFlowRateData, excelToMatchingResultData, excelToTimePatternData } from '@/components/lib/utils' +import BaseLayout from '../BaseLayout' +import { validateMultipleFiles, FILE_EXTENSIONS, type FileValidationConfig } from '../../fileValidation'; +import { validateBasicSelectionsWithAlert } from '../../selectionValidation'; +import { tokenManager } from '@/entities/auth/lib/token-manager'; +import { AUTH_USER_ID } from "@/shared/config"; +import { useQuery } from "@tanstack/react-query"; +import { ENDPOINT } from "@common/components/config"; +import { getUserInfo } from "@common/components/api"; + + +const formSchema = z.object({ + managenm: z.string(), + region: z.string(), + suyeonga: z.number(), + inputdate: z.coerce.date(), + rtkSource: z.string(), + suyeongaPlace: z.string(), + pattern: z.string() +}) + +// 각 파일 업로드 폼의 데이터를 저장할 인터페이스 정의 +interface UploadedFileInfo { + file: File | null; + atflCd: string | null; + atflId: string | null; + // 필요하다면 파일 이름 등 추가 정보 저장 가능 + // fileName: string; +} +// 초기 파일 정보 상태 +const initialFileInfo: UploadedFileInfo = { file: null, atflCd: null, atflId: null }; + +interface ApiSelectOption { + text: string; + val: string; +} + +const IrSetting = () => { + const [input1, setInput1] = useState(''); + const [input2, setInput2] = useState(''); + const [select1, setSelect1] = useState('0'); // 환경청코드 + const [select2, setSelect2] = useState('0'); // 시도코드 + const [select3, setSelect3] = useState<ApiSelectOption>({ val: '0', text: '전체' }); // 시군구 상태를 객체로 변경 + const [select4, setSelect4] = useState<string | number>('0'); // 하수처리장코드 + const [select5, setSelect5] = useState<string | number>('0'); // 하수처리구역코드 + const [formResetKey, setFormResetKey] = useState(0); // FileUploadForm 리셋을 위한 key 상태 + const [isLoadingResult, setIsLoadingResult] = useState<boolean>(false); // 로딩 상태 + + const { data: userInfo } = useQuery({ + queryKey: [ENDPOINT.USER_SERVICE.USERS], + queryFn: () => getUserInfo(), + enabled: true, + }); + + const form = useForm<z.infer<typeof formSchema>>({ + resolver: zodResolver(formSchema), + defaultValues: { + managenm: "", + region: "", + suyeonga: 0, + inputdate: new Date(), + rtkSource: "", + suyeongaPlace: "", + pattern: "" + } + }) + + const [uploadedFilesData, setUploadedFilesData] = useState<UploadedFileInfo[]>([ + initialFileInfo, // 0: 오수관망도 + initialFileInfo, // 1: 수용가-맨홀 매칭결과 + initialFileInfo, // 2: 오수량 시간패턴 + initialFileInfo // 3: 유량자료 등록 + ]); + + // selectedDate를 yyyyMM 문자열 형식으로 변경 + const [selectedDate, setSelectedDate] = useState(() => { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + return `${year}${month}`; + }); + + // 폼 초기화 함수 + const resetAllForms = () => { + form.reset({ // react-hook-form 초기화 + managenm: "", + region: "", + suyeonga: 0, + inputdate: new Date(), + rtkSource: "", + suyeongaPlace: "", + pattern: "" + }); + setUploadedFilesData([ // 파일 업로드 상태 초기화 + initialFileInfo, + initialFileInfo, + initialFileInfo, + initialFileInfo + ]); + // selectedDate를 현재 연월로 초기화 + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + setSelectedDate(`${year}${month}`); + setInput1(''); // 입력 필드 초기화 + setInput2(''); // 입력 필드 초기화 + setSelect1('0'); // 선택 필드 초기화 + setSelect2('0'); + setSelect3({ val: '0', text: '전체' }); + setSelect4(0); + setSelect5(0); + setFormResetKey(prevKey => prevKey + 1); // FileUploadForm 재마운트를 위해 key 변경 + }; + + // FileUploadForm의 onFileSelect prop으로 전달될 콜백 함수 + const handleFileSelection = ( + index: number, // FileUploadForm의 fileIdx 값 + selectedFile: File | null, + cd: string | null, + id: string | null + ) => { + setUploadedFilesData(prevData => { + const newData = [...prevData]; + newData[index] = { + file: selectedFile, + atflCd: cd, + atflId: id, + }; + return newData; + }); + console.log(`Page: File data received for form index ${index}`, { fileName: selectedFile?.name, cd, id }); + }; + + + const registFile = async (analysisData: any) => { + setIsLoadingResult(true); // 로딩 상태 시작 + console.log("Submitting combined analysis data to backend:", analysisData); + // 추후 API 엔드포인트에 맞게 URL 수정 필요 + const data = await postData('v1/inftwtAnls', analysisData); + console.log("Response from backend:", data); + if (data) { + setIsLoadingResult(false); // 로딩 상태 시작 + alert("분석 데이터가 성공적으로 제출되었습니다."); + resetAllForms(); // 성공 시 폼 초기화 함수 호출 + } else { + setIsLoadingResult(false); // 로딩 상태 시작 + alert("분석 데이터 제출 중 오류가 발생했습니다."); + } + }; + + // 침입수 분석 실행 버튼 클릭 핸들러 + const handleAnalysisClick = async () => { + setIsLoadingResult(true); // 로딩 상태 시작 + + // 기본 선택 항목들 검증 (환경청, 시도, 시군구, 하수처리장) + if (!validateBasicSelectionsWithAlert(select1, select2, select3, select4, { + requireEnv: true, + requireSido: true, + requireSigungu: true, + requireTreatmentPlant: true + })) { + setIsLoadingResult(false); + return; + } + + console.log(uploadedFilesData); + const file1 = uploadedFilesData[0]; // 오수관망도 + const file2 = uploadedFilesData[1]; // 수용가-맨홀 매칭결과 + const file3 = uploadedFilesData[2]; // 오수량 시간패턴 + const file4 = uploadedFilesData[3]; // 유량자료 + + console.log("오수관망도 정보:", file1?.file); + console.log("수용가-맨홀 매칭결과 정보:", file2); + console.log("오수량 시간패턴 정보:", file3); + console.log("유량자료 정보:", file4); + + // 모든 필수 파일이 선택되었는지 확인 (UploadedFileInfo 객체와 그 안의 file 객체 모두 확인) + // 유량자료는 선택값 + if (!file1?.file || !file2?.file || !file3?.file) { + setIsLoadingResult(false); + alert('모든 분석용 파일을 선택해주세요.'); + return; + } + + // 파일 확장자 검증 (분리된 유틸리티 사용) + const validationConfigs: FileValidationConfig[] = [ + { + file: file1.file, + allowedExtensions: FILE_EXTENSIONS.INP, + fileDescription: '오수관망도' + }, + { + file: file2.file, + allowedExtensions: FILE_EXTENSIONS.EXCEL, + fileDescription: '수용가-맨홀 매칭결과' + }, + { + file: file3.file, + allowedExtensions: FILE_EXTENSIONS.EXCEL, + fileDescription: '오수량 시간패턴' + } + ]; + + // 유량자료 파일이 있는 경우 검증 목록에 추가 + if (file4?.file) { + validationConfigs.push({ + file: file4.file, + allowedExtensions: FILE_EXTENSIONS.EXCEL, + fileDescription: '유량자료' + }); + } + + // 파일 확장자 검증 실행 + const validationResult = validateMultipleFiles(validationConfigs); + if (!validationResult.isValid) { + setIsLoadingResult(false); + alert(validationResult.errorMessage); + return; + } + + // 오수전환율 값 0 ~ 1 사이인지 검증 + if (input1 === '' || input1 === null || input1 === undefined) { + setIsLoadingResult(false); + console.error('오수전환율은 0에서 1 사이의 값으로 입력해야 합니다.'); + alert('오수전환율을 입력해주세요.'); + return; + } + + const numericInput1 = parseFloat(input1); + if (isNaN(numericInput1) || numericInput1 < 0 || numericInput1 > 1) { + setIsLoadingResult(false); + console.error('오수전환율은 0에서 1 사이의 값으로 입력해야 합니다.'); + alert('오수전환율은 0에서 1 사이의 값으로 입력해야 합니다.'); + return; + } + + type MatchingResult = { anlsSn: number; mhlMngNo: string; rcptrNo: string; }; + type TimePattern = { hrId: number; rt?: number | null | undefined; }; + type FlowRate = { pplMngNo: string; mtrngDt: string; oilq?: number | null | undefined; }; + + const filePickers: { + file: UploadedFileInfo; + parser: (excelData: any) => MatchingResult[] | TimePattern[] | FlowRate[]; + }[] = [ + { file: file2, parser: excelToMatchingResultData }, + { file: file3, parser: excelToTimePatternData } + ]; + + if (file4?.file) { + filePickers.push({ file: file4, parser: excelToFlowRateData }); + } + + try { + const parsedResults = await Promise.all( + filePickers.map(async ({ file, parser }) => { + if (!file || !file.file) return null; + + const jsonData = await filePicked(file.file, 0); + + if (jsonData === undefined) { + console.warn(`Warning: filePicked returned undefined for ${file.file.name}`); + return { + fileName: file.file.name, + atflCd: file.atflCd, + atflId: file.atflId, + parsedData: null + }; + } + + // jsonData를 각 parser 함수로 처리 + const parsedDataBySpecificFunction = parser(jsonData); + + return { + fileName: file.file.name, + atflCd: file.atflCd, + atflId: file.atflId, + parsedData: parsedDataBySpecificFunction + }; + }) + ); + + // 각 파싱 결과 접근 + const matchingResultData = parsedResults.find(r => r && r.fileName === file2.file?.name)?.parsedData; + const timePatternData = parsedResults.find(r => r && r.fileName === file3.file?.name)?.parsedData; + const flowRateData = file4?.file + ? parsedResults.find(r => r && r.fileName === file4.file?.name)?.parsedData + : null; + + // 새로운 JSON 데이터 생성 + const combinedAnalysisData = { + enoCd: (select1 && select1 !== '0') ? select1.toString() : null, // 환경청코드 (선택값) + ctpvCd: (select2 && select2 !== '0') ? select2.toString() : null, // 시도코드 (선택값) + sggCd: (select3.val && select3.val !== '0') ? select3.val.toString() : null, // 시군구코드 (선택값) + swageTmplntCd: (select4 && select4 !== '0' && select4 !== 0) + ? select4.toString().split('.').pop() || null // '.'으로 split해서 마지막 부분만 사용 + : null, // 하수처리장코드 (선택값) + swtrmtZoneCd: (select5 && select5 !== '0' && select5 !== 0) + ? select5.toString().split('.').pop() || null // '.'으로 split해서 마지막 부분만 사용 + : null, // 하수처리구역코드 (선택값) + aplcnYm: selectedDate, // 적용연월 + drtwSwtcRt: parseFloat(input1) || null, // 오수전환율 (숫자로 변환) + anlsInfo: input2, // 분석정보 + rgtr: tokenManager.getToken(AUTH_USER_ID) || null, // 등록자 (사용자 ID) + rgtrNm: userInfo?.userName, // 등록자 (사용자 ID) + atflCd: file1?.atflCd, // 첨부파일CD (오수관망도) + atflId: file1?.atflId, // 첨부파일ID (오수관망도) + atflInpNm: file1?.file?.name, // 오수관망도 파일명 + atflHrPttnNm: file3?.file?.name, // 오수량 시간패턴 파일명 + atflMpngNm: file2?.file?.name, // 수용가-맨홀 매칭결과 파일명 + atflOilqNm: file4?.file?.name || null, // 유량자료 파일명 (없을 수 있음) + tbSnRcptrMhlMpng: matchingResultData, // 수용가-맨홀 매칭결과 + tbSnHrPttn: timePatternData, // 오수량 시간패턴 + tbSnOilqData: flowRateData, // 유량자료 (없을 수 있음) + }; + + console.log("통합 분석 데이터 (JSON):", combinedAnalysisData); + + // 수정된 registFile 함수를 호출하여 combinedAnalysisData를 전송합니다. + await registFile(combinedAnalysisData); + setIsLoadingResult(false); + } catch (error) { + setIsLoadingResult(false); + console.error("파일 파싱 중 오류 발생:", error); + alert("파일 처리 중 오류가 발생했습니다."); + return; + } + } + + return ( + <> + <BaseLayout + titleName={{ mainTitle: '침입수 분석', firstCardTitle: '침입수 분석 자료 입력' }} + form={form} + onSubmitEvent={() => { }} + graphBtn={false} + analysisBtn={'침입수분석 실행'} + selectBox={{ title: '분석 대상지역 선택' }} + enoCd={select1} + setEnoCd={setSelect1} + ctpvCd={select2} + setCtpvCd={setSelect2} + sigungu={select3} + setSigungu={setSelect3} + selectedSwageTreatmentPlant={select4} + onSwageTreatmentPlantChange={setSelect4} + selectedSwageProcessingZone={select5} + onSwageProcessingZoneChange={setSelect5} + isLoading={isLoadingResult} + onAnalysis={handleAnalysisClick} + > + {[<div key="irsetting-layout-key" className="grid gap-4"> + <FormSection name='suyeongaPlace' form={form} titleNm='▷ 오수관망도'> + <FileUploadForm + key={`file-upload-0-${formResetKey}`} // 폼 초기화를 위한 key + onFileSelect={(file, cd, id) => handleFileSelection(0, file, cd, id)} + showButton={false} + showPreview={true} + accept={".inp"} + fileIdx={0} /> + </FormSection> + <FormSection name='suyeonga' form={form} titleNm='▷ 수용가-맨홀 매칭결과'> + <FileUploadForm + key={`file-upload-1-${formResetKey}`} // 폼 초기화를 위한 key + onFileSelect={(file, cd, id) => handleFileSelection(1, file, cd, id)} + showButton={false} + showPreview={true} + accept={".xls, .xlsx, .xlsm, .xltx"} + fileIdx={1} /> + </FormSection> + <FormSection name='pattern' form={form} titleNm='▷ 오수량 시간패턴'> + <FileUploadForm + key={`file-upload-2-${formResetKey}`} // 폼 초기화를 위한 key + onFileSelect={(file, cd, id) => handleFileSelection(2, file, cd, id)} + showButton={false} + showPreview={true} + accept={".xls, .xlsx, .xlsm, .xltx"} + fileIdx={2} /> + </FormSection> + <FormSection + name='inputdate' + form={form} + titleNm='▷ 상수사용량 적용월'> + {/* 연월 선택 라이브러리로 수정 필요 */} + <DateMonthPicker + field={{ + value: selectedDate, // yyyyMM 형식 + onChange: (value: string) => { + setSelectedDate(value); + } + }} + selectClass="ml-5 w-[150px]" + startYear={1900} // 시작 연도 설정 + /> + </FormSection> + <FormSection + name='rtkSource' + form={form} + titleNm='▷ 오수전환율'> + <Input + value={input1} + onChange={(e) => { + const newValue = e.target.value; + + if (newValue === "") { + setInput1(newValue); + return; + } + + // 숫자와 '.'만 허용하고, '.'은 최대 하나인지 확인 + const isValidChars = /^[0-9]*\.?[0-9]*$/.test(newValue); + if (!isValidChars) return; // 유효하지 않은 문자 또는 여러 개의 '.' 포함 + + // '.' 기호가 포함된 경우, 처음이나 끝에 오면 안 됨 + if (newValue.includes('.') && newValue.startsWith('.')) return; // '.'이 처음에는 올 수 없음 + + // 숫자 값으로 변환하여 범위 체크 (0 이상 1 이하) + const numericValue = parseFloat(newValue); + if (isNaN(numericValue) || numericValue < 0 || numericValue > 1) return; + + setInput1(newValue); + }} + className="ml-5 h-[3.2rem] text-center text-xl w-[150px]" + placeholder="예: 0.75" + /> + </FormSection> + <FormSection + name='analysis information' + form={form} + titleNm='▷ 분석 정보'> + <Input + value={input2} + onChange={(e) => setInput2(e.target.value)} + className="ml-5 h-[3.2rem] text-center text-xl w-[150px]" /> + </FormSection> + <ListDiv> + <div className="card !m-[1.2rem]"> + <FormSection name='region' form={form} titleNm='▷ 유량자료 등록'> + <FileUploadForm + key={`file-upload-3-${formResetKey}`} // 폼 초기화를 위한 key + onFileSelect={(file, cd, id) => handleFileSelection(3, file, cd, id)} + showButton={false} + showPreview={true} + accept={".xls, .xlsx, .xlsm, .xltx"} + fileIdx={3} + /> + </FormSection> + </div> + </ListDiv> + </div>]} + </BaseLayout> + </> + ) +} + +export default IrSetting \ No newline at end of file diff --git a/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx b/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx new file mode 100644 index 000000000..08aa84e34 --- /dev/null +++ b/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx @@ -0,0 +1,530 @@ +"use client" + +import { postData } from '@/components/lib/api' +import FileUploadForm from "@/components/ui/fileUploadForm" +import FormSection from "@/components/ui/formSection" +import BaseLayout from "../BaseLayout" +import { DateMonthPicker, filePicked } from "@common/business_components/ui" +import { Input } from "@common/components/ui" +import { zodResolver } from "@hookform/resolvers/zod" +import { Label } from '@radix-ui/react-label' +import { useState } from "react" +import { useFieldArray, useForm } from "react-hook-form" +import { z } from "zod" +import { excelToMatchingResultData, excelToRainData } from '@/components/lib/utils' +import { EditableFormTable, type EditableTableHeadItem } from '../../../components/ui/EditableFormTable' +import { validateMultipleFiles, FILE_EXTENSIONS, type FileValidationConfig } from '../../fileValidation'; +import { validateBasicSelectionsWithAlert } from '../../selectionValidation'; +import { tokenManager } from '@/entities/auth/lib/token-manager'; +import { AUTH_USER_ID } from "@/shared/config"; +import { useQuery } from "@tanstack/react-query"; +import { ENDPOINT } from "@common/components/config"; +import { getUserInfo } from "@common/components/api"; + + +// zod 스키마 정의 +const formSchema = z.object({ + enoCd: z.string(), // 환경청코드 + ctpvCd: z.string(), // 시도코드 + sggCd: z.string(), // 시군구코드 + swageTmplntCd: z.string(), // 하수처리장코드 + swtrmtZoneCd: z.string(), // 하수처리구역코드 + rtkSource: z.string(), // RTK계수 출처 + rdiiArea: z.string(), // RDII 기여면적 + 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(), + })), + // osumap: z.instanceof(File).optional(), // 오수관망도 + // matching: z.instanceof(File).optional(), // 수용가-맨홀 매칭결과 + aplcnYm: z.string(), // 상수사용량 적용월 (YYYYMM 형식) + rate: z.string(), // 오수전환율 + // rain: z.instanceof(File).optional(), // 강우자료 + analysisInfo: z.string().optional(), // 분석 정보 + date: z.string(), // Date 객체에서 string(yyyyMM 형식)으로 변경 + // date: z.coerce.date(), +}) +// zod 스키마 타입 추론 +type FormSchemaType = z.infer<typeof formSchema>; +// 기본 폼 값 정의 (테스트 편의를 위해 초기값 설정, 추후 삭제 필요) +const defaultFormValues: FormSchemaType = { + enoCd: "", + ctpvCd: "", + sggCd: "", + swageTmplntCd: "", + swtrmtZoneCd: "", + rtkSource: "출처", + rdiiArea: "123", // 문자열로 기본값 설정 + unitFactors: [ + { response: "Short-Term", R: "0.040", T: "0.5", K: "1" }, // 문자열로 기본값 설정 + { response: "Medium-Term", R: "0.005", T: "1", K: "2" }, + { response: "Long-Term", R: "0.005", T: "2", K: "3" }, + ], + initialDepths: [ + { response: "Short-Term", Dmax: "0", Drec: "0", Do: "0" }, // 문자열로 기본값 설정 + { response: "Medium-Term", Dmax: "0", Drec: "0", Do: "0" }, + { response: "Long-Term", Dmax: "0", Drec: "0", Do: "0" }, + ], + // osumap: undefined, + // matchingResultFile: undefined, + aplcnYm: "", + rate: "0.8", // 문자열로 기본값 설정 + // rainFile: undefined, + analysisInfo: "test 분석정보", + date: (() => { // 현재 연월을 yyyyMM 형식으로 설정 + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + return `${year}${month}`; + })(), +}; + +// 각 파일 업로드 폼의 데이터를 저장할 인터페이스 정의 +interface UploadedFileInfo { + file: File | null; + atflCd: string | null; + atflId: string | null; +} + +// 초기 파일 정보 상태 +const initialFileInfo: UploadedFileInfo = { file: null, atflCd: null, atflId: null }; + +interface ApiSelectOption { + text: string; + val: string; +} + +export default function RTKForm() { + const [formResetKey, setFormResetKey] = useState(0); // FileUploadForm 리셋을 위한 key 상태 + const [isLoadingResult, setIsLoadingResult] = useState<boolean>(false); // 로딩 상태 + + // FileUploadForm에서 전달받은 파일 정보를 저장할 상태 배열 + // 각 인덱스는 FileUploadForm의 fileIdx와 매칭됨 + const [uploadedFilesData, setUploadedFilesData] = useState<UploadedFileInfo[]>([ + initialFileInfo, // 0: 오수관망도 + initialFileInfo, // 1: 수용가-맨홀 매칭결과 + initialFileInfo, // 2: 강우자료 + ]); + + const [select1, setSelect1] = useState('0'); // 환경청코드 (enoCd) + const [select2, setSelect2] = useState('0'); // 시도코드 (ctpvCd) + const [select3, setSelect3] = useState<ApiSelectOption>({ val: '0', text: '전체' }); // 시군구 (sggCd) + const [select4, setSelect4] = useState<string | number>('0'); // 하수처리장 (swageTmplntCd) + const [select5, setSelect5] = useState<string | number>('0'); // 하수처리구역 (swtrmtZoneCd) + + const { data: userInfo } = useQuery({ + queryKey: [ENDPOINT.USER_SERVICE.USERS], + queryFn: () => getUserInfo(), + enabled: true, + }); + + // form 인스턴스 생성 + const form = useForm<FormSchemaType>({ + resolver: zodResolver(formSchema), + defaultValues: defaultFormValues + }); + + // 폼 초기화 함수 + const resetAllForms = () => { + form.reset(defaultFormValues); // 폼 값 초기화 + setUploadedFilesData([ // 파일 업로드 상태 초기화 (3개 파일) + initialFileInfo, + initialFileInfo, + initialFileInfo, + ]); + setFormResetKey(prevKey => prevKey + 1); // FileUploadForm 재마운트를 위해 key 변경 + setSelect1('0'); + setSelect2('0'); + setSelect3({ val: '0', text: '전체' }); + setSelect4('0'); + setSelect5('0'); + }; + + // FileUploadForm의 onFileSelect prop으로 전달될 콜백 함수 + const handleFileSelection = ( + index: number, // FileUploadForm의 fileIdx 값 + selectedFile: File | null, + cd: string | null, + id: string | null + ) => { + setUploadedFilesData(prevData => { + const newData = [...prevData]; + newData[index] = { + file: selectedFile, + atflCd: cd, + atflId: id, + }; + return newData; + }); + console.log(`Page: File data received for form index ${index}`, { fileName: selectedFile?.name, cd, id }); + }; + + const registFile = async (analysisData: any) => { + console.log("Submitting combined analysis data to backend"); + // 추후 API 엔드포인트에 맞게 URL 수정 필요 + const data = await postData('v1/rdiiAnls', analysisData); + if (data) { + alert("분석 데이터가 성공적으로 제출되었습니다."); + resetAllForms(); // 성공 시 폼 초기화 함수 호출 + } else alert("분석 데이터 제출 중 오류가 발생했습니다."); + }; + + // 침입수 분석 실행 버튼 클릭 핸들러 + const handleAnalysisClick = async () => { + setIsLoadingResult(true); // 로딩 상태 시작 + + // 기본 선택 항목들 검증 (환경청, 시도, 시군구, 하수처리장) + if (!validateBasicSelectionsWithAlert(select1, select2, select3, select4, { + requireEnv: true, + requireSido: true, + requireSigungu: true, + requireTreatmentPlant: true + })) { + setIsLoadingResult(false); + return; + } + + const formData = form.getValues(); + console.log(uploadedFilesData); + const file1 = uploadedFilesData[0]; // 오수관망도 + const file2 = uploadedFilesData[1]; // 수용가-맨홀 매칭결과 + const file3 = uploadedFilesData[2]; // 강우자료 + + console.log("오수관망도 정보:", file1?.file); + console.log("수용가-맨홀 매칭결과 정보:", file2); + console.log("강우자료:", file3); + + // 모든 필수 파일이 선택되었는지 확인 + if (!file1?.file || !file2?.file || !file3?.file) { + setIsLoadingResult(false); + alert('모든 분석용 파일을 선택해주세요.'); + return; + } + + // 파일 확장자 검증 (분리된 유틸리티 사용) + const validationConfigs: FileValidationConfig[] = [ + { + file: file1.file, + allowedExtensions: FILE_EXTENSIONS.INP, + fileDescription: '오수관망도' + }, + { + file: file2.file, + allowedExtensions: FILE_EXTENSIONS.EXCEL, + fileDescription: '수용가-맨홀 매칭결과' + }, + { + file: file3.file, + allowedExtensions: FILE_EXTENSIONS.EXCEL, + fileDescription: '오수량 시간패턴' + } + ]; + + // 파일 확장자 검증 실행 + const validationResult = validateMultipleFiles(validationConfigs); + if (!validationResult.isValid) { + setIsLoadingResult(false); + alert(validationResult.errorMessage); + return; + } + + // 오수전환율 값 0 ~ 1 사이인지 검증 + const rateValue = form.getValues().rate; + if (rateValue === '' || rateValue === null || rateValue === undefined) { + setIsLoadingResult(false); + console.error('오수전환율은 0에서 1 사이의 값으로 입력해야 합니다.'); + alert('오수전환율을 입력해주세요.'); + return; + } + + const numericInput1 = parseFloat(rateValue); + if (isNaN(numericInput1) || numericInput1 < 0 || numericInput1 > 1) { + setIsLoadingResult(false); + console.error('오수전환율은 0에서 1 사이의 값으로 입력해야 합니다.'); + alert('오수전환율은 0에서 1 사이의 값으로 입력해야 합니다.'); + return; + } + + const filePickers = [ + { file: file2, parser: excelToMatchingResultData }, + { file: file3, parser: excelToRainData }, + ]; + + try { + const parsedResults = await Promise.all( + filePickers.map(async ({ file, parser }) => { + if (!file || !file.file) return null; // fileInfo 객체 및 file 객체 null 체크 + + // filePicked 함수에 File 객체와 headerNum 전달 + const jsonData = await filePicked(file.file, 0); + + if (jsonData === undefined) { + console.warn(`Warning: filePicked returned undefined for ${file.file.name}`); + return { + fileName: file.file.name, + atflCd: file.atflCd, + atflId: file.atflId, + parsedData: null // 또는 적절한 기본값 + }; + } + + // jsonData를 각 parser 함수로 처리 + // parser 함수가 filePicked가 반환하는 JSON 형식을 직접 처리한다고 가정 + const parsedDataBySpecificFunction = parser(jsonData); + + return { + fileName: file.file.name, + atflCd: file.atflCd, + atflId: file.atflId, + parsedData: parsedDataBySpecificFunction + }; + }) + ); + + // 각 파싱 결과 접근 + const matchingResultData = parsedResults.find(r => r && r.fileName === file2.file?.name)?.parsedData; + const rainData = parsedResults.find(r => r && r.fileName === file3.file?.name)?.parsedData; + + // 새로운 JSON 데이터 생성 + const combinedAnalysisData = { + enoCd: (select1 && select1 !== '0') ? select1.toString() : null, // 환경청코드 (선택값) + aplcnYm: formData.date, // 적용연월 (YYYYMM 형식) + ctpvCd: (select2 && select2 !== '0') ? select2.toString() : null, // 시도코드 (선택값) + sggCd: (select3.val && select3.val !== '0') ? select3.val.toString() : null, // 시군구코드 (선택값) + swageTmplntCd: (select4 && select4 !== '0' && select4 !== 0) + ? select4.toString().split('.').pop() || null // '.'으로 split해서 마지막 부분만 사용 + : null, // 하수처리장코드 (선택값) + swtrmtZoneCd: (select5 && select5 !== '0' && select5 !== 0) + ? select5.toString().split('.').pop() || null // '.'으로 split해서 마지막 부분만 사용 + : null, // 하수처리구역코드 (선택값) + atflCd: file1?.atflCd, // 첨부파일CD (오수관망도) + atflId: file1?.atflId, // 첨부파일ID (오수관망도) + atflInpNm: file1?.file?.name || "", // 오수관망도 파일명 + atflMpngNm: file2?.file?.name || "", // 수용가-맨홀 매칭결과 파일명 + atflRainNm: file3?.file?.name || "", // 강우자료 파일명 + cntbArea: parseFloat(formData.rdiiArea) || null, // RDII 기여면적(ha) + srcNm: formData.rtkSource, // 출처이름 (RTK계수 출처) + + shortPrdR: parseFloat(formData.unitFactors[0]?.R || "0"), + shortPrdT: parseFloat(formData.unitFactors[0]?.T || "0"), + shortPrdK: parseFloat(formData.unitFactors[0]?.K || "0"), + + mdPrdR: parseFloat(formData.unitFactors[1]?.R || "0"), + mdPrdT: parseFloat(formData.unitFactors[1]?.T || "0"), + mdPrdK: parseFloat(formData.unitFactors[1]?.K || "0"), + + longPrdR: parseFloat(formData.unitFactors[2]?.R || "0"), + longPrdT: parseFloat(formData.unitFactors[2]?.T || "0"), + longPrdK: parseFloat(formData.unitFactors[2]?.K || "0"), + + shortPrdMaxDpth: parseFloat(formData.initialDepths[0]?.Dmax || "0"), + shortPrdRcvryRt: parseFloat(formData.initialDepths[0]?.Drec || "0"), + shortPrdBgngDpth: parseFloat(formData.initialDepths[0]?.Do || "0"), + + mdPrdMaxDpth: parseFloat(formData.initialDepths[1]?.Dmax || "0"), + mdPrdRcvryRt: parseFloat(formData.initialDepths[1]?.Drec || "0"), + mdPrdBgngDpth: parseFloat(formData.initialDepths[1]?.Do || "0"), + + longTermMaxDpth: parseFloat(formData.initialDepths[2]?.Dmax || "0"), + longTermRcvryRt: parseFloat(formData.initialDepths[2]?.Drec || "0"), + longTermBgngDpth: parseFloat(formData.initialDepths[2]?.Do || "0"), + + drtwSwtcRt: parseFloat(formData.rate || "0"), // 오수전환율 + anlsInfo: formData.analysisInfo, // 분석정보 + rgtr: tokenManager.getToken(AUTH_USER_ID) || null, // 등록자 (사용자 ID) + rgtrNm: userInfo?.userName, // 등록자 (사용자 ID) + + tbSnRcptrMhlMpng: matchingResultData, // 수용가-맨홀 매칭결과 + tbSnRainData: rainData, // 강우자료 + }; + + console.log("통합 분석 데이터 (JSON):", combinedAnalysisData); + + // 수정된 registFile 함수를 호출하여 combinedAnalysisData를 전송합니다. + await registFile(combinedAnalysisData); + setIsLoadingResult(false); + } catch (error) { + setIsLoadingResult(false); + console.error("파일 파싱 중 오류 발생:", error); + alert("파일 처리 중 오류가 발생했습니다."); + return; + } + } + + 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) + } + + // 단위도 계수 테이블 설정 (EditableTableHeadItem 타입 사용 및 id 수정) + const unitFactorHeadlist: EditableTableHeadItem<FormSchemaType['unitFactors'][number]>[] = [ + { id: 'response', title: 'Response', editable: false, width: '150px' }, // width 속성 추가 + { id: 'R', title: 'R', editable: true, inputProps: { type: 'text' } }, + { id: 'T', title: 'T', editable: true, inputProps: { type: 'text' } }, + { id: 'K', title: 'K', editable: true, inputProps: { type: 'text' } } + ]; + + // 초기 차단 깊이 테이블 설정 (EditableTableHeadItem 타입 사용) + const initialDepthHeadlist: EditableTableHeadItem<FormSchemaType['initialDepths'][number]>[] = [ + { id: 'response', title: 'Response', editable: false, width: '150px' }, // width 속성 추가 + { id: 'Dmax', title: 'Dmax', editable: true, inputProps: { type: 'text' } }, + { id: 'Drec', title: 'Drec', editable: true, inputProps: { type: 'text' } }, + { id: 'Do', title: 'Do', editable: true, inputProps: { type: 'text' } } + ]; + + // 테이블 페이지 변경 핸들러 + const handlePageChange = (page: number) => { + console.log('Page changed:', page) + } + + return ( + // <BaseLayout searchBox={{ title: '분석 대상지역 선택' }} titleName={{ mainTitle: 'RDII 분석', firstCardTitle: 'RDII 분석자료 입력', secondCardTitle: '실측자료 등록' }} form={form} onSubmitEvent={() => { }} graphBtn={false} > + <BaseLayout + selectBox={{ title: '분석 대상지역 선택' }} + titleName={{ mainTitle: 'RDII 분석', firstCardTitle: 'RDII 분석자료 입력', secondCardTitle: '실측자료 등록' }} + form={form} // react-hook-form 인스턴스 전달 + onSubmitEvent={() => { }} // BaseLayout의 form 태그에 연결될 onSubmit + graphBtn={false} + analysisBtn={'RDII분석 실행'} + enoCd={select1} + setEnoCd={setSelect1} + ctpvCd={select2} + setCtpvCd={setSelect2} + sigungu={select3} + setSigungu={setSelect3} + selectedSwageTreatmentPlant={select4} + onSwageTreatmentPlantChange={setSelect4} + selectedSwageProcessingZone={select5} + onSwageProcessingZoneChange={setSelect5} + isLoading={isLoadingResult} + onAnalysis={handleAnalysisClick} + > + {[<div className="space-y-[1.2rem] overflow-x-auto pt-4 pb-4 px-2" key="form-section"> + <div className="grid grid-cols-2 gap-4"> + <FormSection name="rtkSource" form={form} titleNm="○ RTK계수 출처"> + <Input {...form.register("rtkSource")} className="h-12 text-center text-xl w-[225px]" /> + </FormSection> + <FormSection name="rdiiArea" form={form} titleNm="○ RDII 기여면적(ha)"> + <Input {...form.register("rdiiArea")} className="h-12 text-center text-xl w-[225px]" /> + </FormSection></div> + <div className="grid grid-cols-2 gap-4"> + <div> + <Label className="mb-2 text-xl font-medium">○ 단위도 계수</Label> + <EditableFormTable + form={form} + name="unitFactors" + fields={unitFactorFields} + headlist={unitFactorHeadlist} + tableClassName="w-[400px]" + /> + </div> + <div> + <Label className="mb-[1.2rem] text-xl font-medium">○ 초기 차단 깊이</Label> + <EditableFormTable + form={form} + name="initialDepths" + fields={initialDepthFields} + headlist={initialDepthHeadlist} + tableClassName="w-[400px]" + /> + </div> + </div> + <div className="grid grid-cols-2 gap-4"> + <FormSection name="suyeonga" form={form} titleNm="○ 오수관망도"><div className="w-[350px]"> + <FileUploadForm + key={`file-upload-0-${formResetKey}`} // 폼 초기화를 위한 key + onFileSelect={(file, cd, id) => handleFileSelection(0, file, cd, id)} + showButton={false} + showPreview={true} + accept={".inp"} + fileIdx={0} /> + </div></FormSection> + <FormSection name="regopm" form={form} titleNm="○ 수용가-맨홀 매칭결과"><div className="w-[350px]"> + <FileUploadForm + key={`file-upload-1-${formResetKey}`} // 폼 초기화를 위한 key + onFileSelect={(file, cd, id) => handleFileSelection(1, file, cd, id)} + showButton={false} + showPreview={true} + accept={".xls, .xlsx, .xlsm, .xltx"} + fileIdx={1} /> + </div></FormSection> + <FormSection name="rain" form={form} titleNm="○ 강우자료"><div className="w-[350px]"> + <FileUploadForm + key={`file-upload-2-${formResetKey}`} // 폼 초기화를 위한 key + onFileSelect={(file, cd, id) => handleFileSelection(2, file, cd, id)} + showButton={false} + showPreview={true} + accept={".xls, .xlsx, .xlsm, .xltx"} + fileIdx={2} /> + </div></FormSection> + <FormSection name='rate' form={form} titleNm='○ 오수전환율'> + <Input + value={form.watch("rate")} // form의 rate 값을 가져옴 + onChange={(e) => { + const newValue = e.target.value; + + if (newValue === "") { + form.setValue("rate", newValue); // form의 rate 필드에 설정 + return; + } + + // 숫자와 '.'만 허용하고, '.'은 최대 하나인지 확인 + const isValidChars = /^[0-9]*\.?[0-9]*$/.test(newValue); + if (!isValidChars) return; // 유효하지 않은 문자 또는 여러 개의 '.' 포함 + + // '.' 기호가 처음에 오면 안 됨 + if (newValue.startsWith('.')) return; + + // 완성된 숫자인 경우에만 범위 체크 (입력 중인 경우 허용) + if (!newValue.endsWith('.')) { + const numericValue = parseFloat(newValue); + if (isNaN(numericValue) || numericValue < 0 || numericValue > 1) return; + } + + form.setValue("rate", newValue, { shouldValidate: true }); // form의 rate 필드에 설정 + }} + className="h-12 text-center text-xl w-[225px] ml-5" + placeholder="예: 0.75" + /> + </FormSection> + <FormSection name='date' form={form} titleNm='○ 상수사용량 적용월'> + <DateMonthPicker + field={{ + value: form.watch("date"), // form의 date 값을 직접 사용 + onChange: (value: string) => { + form.setValue("date", value, { shouldValidate: true }); // form의 date 필드에 직접 설정 + } + }} + selectClass="ml-5 w-[200px]" + startYear={1900} // 시작 연도 설정 + /> + </FormSection> + <FormSection name='rate' form={form} titleNm='○ 분석 정보'> + <Input {...form.register("analysisInfo")} className="h-12 text-center text-xl w-[225px] ml-5" /> + </FormSection> + </div> + {/* <div className="flex justify-end mt-1"> + <Button size="sm" type="submit" onClick={handleAnalysisClick}>RDII분석 실행</Button> + </div> */} + </div> + ]} + </BaseLayout > + ) +} diff --git a/apps/sn/app/(with-base-layout)/ii/rdiiResult/page.tsx b/apps/sn/app/(with-base-layout)/ii/rdiiResult/page.tsx new file mode 100644 index 000000000..08c7a8146 --- /dev/null +++ b/apps/sn/app/(with-base-layout)/ii/rdiiResult/page.tsx @@ -0,0 +1,333 @@ +'use client' +import { deleteData, getData, useCookieAuth } from '@/components/lib/api' +import { UiTable, downloadExcelWithKoreanHeaders } from '@common/business_components/ui' +import { zodResolver } from '@hookform/resolvers/zod' +import { useState, useEffect } from 'react' +import { useForm } from 'react-hook-form' +import { z } from 'zod' +import BaseLayout from '../BaseLayout' +import { handleDelete } from '@/components/lib/utils' +import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue'; + + +const formSchema = z.object({ + enoCd: z.string().default('0'), // 환경청코드 + ctpvCd: z.string().default('0'), // 시도코드 + sggCd: z.object({ // 시군구 + val: z.string().default('0'), + text: z.string().default('전체'), + }).default({ val: '0', text: '전체' }), + swageTmplntCd: z.string().default('0'), // 하수처리장 + swtrmtZoneCd: z.string().default('0'), // 하수처리구역 + aplcnYmFrom: z.string().default(() => { // Date에서 string(yyyyMM 형식)으로 변경 + const now = new Date(); + const year = now.getFullYear(); + // const month = String(now.getMonth() + 1).padStart(2, '0'); + // return `${year}${month}`; + return `${year}01`; // 기본값을 1월로 설정 + }), // 분석기간 시작연월 + aplcnYmTo: z.string().default(() => { // Date에서 string(yyyyMM 형식)으로 변경 + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + return `${year}${month}`; + }), // 분석기간 종료연월 +}) + +type ResultFormValues = z.infer<typeof formSchema> + +const tableColumn = [ + { id: 'rowNumber', title: '일련번호' }, + { id: 'enoNm', title: '환경청' }, + { id: 'ctpvNm', title: '시도' }, + { id: 'sggNm', title: '시군구' }, + { id: 'swageTmplntNm', title: '하수처리장' }, + // { id: 'swtrmtZoneNm', title: '처리구역' }, + { id: 'aplcnYm', title: '상수사용연월' }, + { id: 'drtwSwtcRt', title: '오수 전환율' }, + { id: 'srcNm', title: 'RTK 계수 출처' }, + { id: 'cntbArea', title: 'RDII 기여면적' }, + { id: 'anlsInfo', title: 'RDII 분석정보' }, + { id: 'regDt', title: '분석일자' }, + { id: 'rgtrNm', title: '분석자' }, + { id: 'scsYn', title: '분석성공여부' } +] + +const Result = () => { + const AUTH = useCookieAuth(); + + const form = useForm<ResultFormValues>({ + resolver: zodResolver(formSchema), + defaultValues: { + enoCd: '0', + ctpvCd: '0', + sggCd: { val: '0', text: '전체' }, + swageTmplntCd: '0', + swtrmtZoneCd: '0', + aplcnYmFrom: (() => { // 현재 연월을 yyyyMM 형식으로 설정 + const now = new Date(); + const year = now.getFullYear(); + // const month = String(now.getMonth() + 1).padStart(2, '0'); + // return `${year}${month}`; + return `${year}01`; + })(), + aplcnYmTo: (() => { // 현재 연월을 yyyyMM 형식으로 설정 + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + return `${year}${month}`; + })(), + } + }) + + + const [tableData, setTableData] = useState<any[]>([]); // API 조회 결과 저장 상태 + const [totalData, setTotalData] = useState(0) //총 데이터 수 + const [isLoadingResult, setIsLoadingResult] = useState<boolean>(false); // 로딩 상태 + const [selectedRowData, setSelectedRowData] = useState<any>(null); + const [resetSelection, setResetSelection] = useState<boolean>(false); + const [isResetPage, setIsResetPage] = useState<boolean>(false); + const [curPage, setCurPage] = useState(1); // 현재 페이지 + + useEffect(() => { + // setCurPage(1); + handleSearchButtonClick(); + }, []) + + // 페이지 변경 시마다 데이터 fetch + useEffect(() => { + setIsResetPage(false); + handleSearchButtonClick(); + }, [curPage]); + + // 조회 버튼 클릭 시 1페이지로 이동 및 데이터 초기화 + const handleSearch: () => Promise<void> = async () => { + setCurPage(1); // 페이지를 1로 초기화 + setIsResetPage(true); // 페이지를 초기화 + await handleSearchButtonClick(); + }; + + + const handleSearchButtonClick = async () => { + setIsLoadingResult(true); + setResetSelection(true); + + const formValues = form.getValues(); // react-hook-form에서 현재 값 가져오기 + console.log("formValues", formValues); + + // swageTmplntCd 처리 로직 추가 + const processedSwageTmplntCd = formValues.swageTmplntCd && formValues.swageTmplntCd !== '0' + ? (() => { + const valString = String(formValues.swageTmplntCd); + const parts = valString.split('.'); + return parts.length > 1 ? parts[1] : valString; + })() + : null; + + let inftwtAnlsSearchDto = { + "aplcnYmFrom": formValues.aplcnYmFrom && formValues.aplcnYmFrom !== '' ? formValues.aplcnYmFrom : null, + "aplcnYmTo": formValues.aplcnYmTo && formValues.aplcnYmTo !== '' ? formValues.aplcnYmTo : null, + "enoCd": formValues.enoCd === '0' ? null : formValues.enoCd, + "ctpvCd": formValues.ctpvCd === '0' ? null : formValues.ctpvCd, + "sggCd": formValues.sggCd?.val === '0' ? null : formValues.sggCd?.val, + "swageTmplntCd": processedSwageTmplntCd, + "auth": AUTH, + }; + + console.log("Search DTO:", inftwtAnlsSearchDto); + + const queryParams = new URLSearchParams({ + page: `${curPage - 1}`, + size: '10', + sort: 'rainInfwtAnlsSn,desc' + }); + Object.entries(inftwtAnlsSearchDto).forEach(([key, value]) => { + if (value !== undefined && value !== null && String(value).trim() !== '') { // undefined, null, 빈 문자열이 아닌 경우에만 추가 + queryParams.append(key, String(value)); + } + }); + const queryString = queryParams.toString(); + + console.log("API: ", `v1/rdiiAnls${queryString ? `?${queryString}` : ''}`); + try { + const data = await getData(`v1/rdiiAnls${queryString ? `?${queryString}` : ''}`); + + // 🎯 각 아이템을 개별적으로 포맷팅하면서 일련번호 추가 + const dataWithRowNumber = (data.content || []).map((item: any, index: number) => { + // 🎯 먼저 각 아이템을 포맷팅 + const formattedItem = formatDisplayData(item); + + // 추가 처리 및 일련번호 추가 + return { + ...formattedItem, + rowNumber: (curPage - 1) * 10 + index + 1, // 1부터 시작하는 일련번호 추가 + scsYn: item.scsYn === null || item.scsYn === undefined ? 'N' : item.scsYn // scsYn이 null이면 'N'으로 변경 + }; + }); + + // 선택된 행 초기화 + setSelectedRowData(null); + + setTableData(dataWithRowNumber); + setTotalData(dataWithRowNumber.length); + console.log("response:", data); + console.log("response data:", dataWithRowNumber); + setIsLoadingResult(false); + } catch (error) { + setIsLoadingResult(false); + console.error("Error fetching analysis result data:", error); + alert("분석결과 조회 중 오류가 발생했습니다."); + setTableData([]); + setTotalData(0); + setSelectedRowData(null); + } + setResetSelection(false); + }; + + // 테이블 셀 클릭 핸들러 추가 + const handleCellClick = (index: number | undefined, headId: string) => { + if (index !== undefined && tableData[index]) { + setSelectedRowData(tableData[index]); + console.log("Selected row data:", tableData[index]); + } + }; + + // Excel 다운로드 핸들러 추가 + const handleExcelDownload = async () => { + setIsLoadingResult(true); + console.log("Selected row data for download:", selectedRowData); + if (!selectedRowData || selectedRowData.rainInfwtAnlsSn === undefined) { + setIsLoadingResult(false); + alert('다운로드할 데이터를 선택해주세요.'); + return; + } + + try { + const data = await getData(`v1/rdiiAnlsRslt/ppl/${selectedRowData.rainInfwtAnlsSn}`); + + // 선택된 행 데이터를 Excel 다운로드 형식으로 변환 + const excelData = data.content; // 배열로 감싸서 전달 + + // 헤더 매핑 정의 + const headerMapping: { [key: string]: string } = { + 'rainInfwtAnlsSn': 'RDII분석\n일련번호', + 'pplMngNo': '관로\n관리번호', + 'anlsTm': '분석시각', + 'predcOilq': '예측유량\n(m³/h)' + }; + + // 파일명 생성 (분석번호 기반) + const fileName = `RDII분석결과_${selectedRowData.swageTmplntNm}_${selectedRowData.aplcnYm}_${selectedRowData.rainInfwtAnlsSn}`; + + await downloadExcelWithKoreanHeaders({ + data: excelData as JSON[], + fileName: fileName, + sheetNameArray: ["RDII 분석결과"], + headerRange: "A1:D1", + headerMapping: headerMapping + }); + setIsLoadingResult(false); + } catch (error) { + setIsLoadingResult(false); + console.error("Excel 다운로드 중 오류 발생:", error); + alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요."); + } + }; + + // 파일 삭제 핸들러 추가 + const handleFileDelete = async () => { + setIsLoadingResult(true); + console.log("Selected row data for deletion:", selectedRowData); + if (!selectedRowData || selectedRowData.rainInfwtAnlsSn === undefined) { + alert('삭제할 파일을 선택해주세요.'); + return; + } + // 삭제 확인 대화상자 + const confirmDelete = window.confirm('선택된 분석 결과를 삭제하시겠습니까?'); + if (!confirmDelete) { + return; + } + + try { + // 삭제 API 호출 + const response1 = await deleteData(`v1/rdiiAnlsRslt/${selectedRowData.rainInfwtAnlsSn}`); + const response2 = await deleteData(`v1/rdiiAnls/${selectedRowData.rainInfwtAnlsSn}`); + // 첨부파일(inp파일) 삭제 API 호출 + handleDelete(selectedRowData.atflCd, { + handleDeleteStart: () => console.log("파일 삭제 시작"), + handleDeleteSuccess: (response) => console.log("파일 삭제 성공:", response), + handleDeleteError: (error) => console.error("파일 삭제 실패:", error), + }); + console.log("File delete response1:", response1); + console.log("File delete response2:", response2); + + // 테이블 선택 상태 초기화 트리거 + setResetSelection(true); + + // 검색 버튼 클릭 이벤트 트리거 + await handleSearchButtonClick(); + + setIsLoadingResult(false); + + alert('삭제가 완료되었습니다.'); + + setTimeout(() => { + setResetSelection(false); + }, 100); + } catch (error) { + console.error("분석결과 파일 삭제 중 오류 발생:", error); + alert("분석결과 파일 삭제 중 오류가 발생했습니다. 다시 시도해주세요."); + } + setIsLoadingResult(false); + }; + + return ( + <BaseLayout + selectBox={{ title: '분석 대상지역 선택', searchChk: true, onSearchClick: handleSearch }} + titleName={{ mainTitle: '분석결과', firstCardTitle: 'RDII 산정결과 현황' }} + form={form} // form 인스턴스 전달 + onSubmitEvent={() => { }} // BaseLayout의 form 태그에 연결될 onSubmit + graphBtn={true} + showDatePicker={true} + enoCd={form.watch("enoCd")} + setEnoCd={(val) => form.setValue("enoCd", val)} + ctpvCd={form.watch("ctpvCd")} + setCtpvCd={(val) => form.setValue("ctpvCd", val)} + sigungu={form.watch("sggCd")} + setSigungu={(val) => form.setValue("sggCd", val)} + selectedSwageTreatmentPlant={form.watch("swageTmplntCd")} + onSwageTreatmentPlantChange={(val) => form.setValue("swageTmplntCd", String(val))} + selectedSwageProcessingZone={form.watch("swtrmtZoneCd")} + onSwageProcessingZoneChange={(val) => form.setValue("swtrmtZoneCd", String(val))} + aplcnYmFrom={form.watch("aplcnYmFrom")} + setAplcnYmFrom={(val: string) => { form.setValue("aplcnYmFrom", val) }} + aplcnYmTo={form.watch("aplcnYmTo")} + setAplcnYmTo={(val: string) => { form.setValue("aplcnYmTo", val) }} + isLoading={isLoadingResult} + // Excel 다운로드 핸들러 추가 + onExcelDownload={handleExcelDownload} + onDeleteFile={handleFileDelete} + selectedRowData={selectedRowData} + > + {[<div key='table1' className="h-[58vh] flex flex-col border rounded-lg bg-white shadow-md"> + {/* <div className="mb-[1.2rem] text-xl font-medium">○ RDII 분석결과 현황</div> */} + <div className="flex-grow overflow-auto"> + <UiTable + headName='' + headlist={tableColumn} + tableData={tableData} + total={totalData} + pageSize={10} + cellClick={handleCellClick} + onPageChange={setCurPage} + resetSelection={resetSelection} + resetPageNo={isResetPage} + /> + </div> + </div> + ]} + </BaseLayout> + ) +} + +export default Result \ No newline at end of file diff --git a/apps/sn/app/(with-base-layout)/ii/result/page.tsx b/apps/sn/app/(with-base-layout)/ii/result/page.tsx new file mode 100644 index 000000000..974f6fa5f --- /dev/null +++ b/apps/sn/app/(with-base-layout)/ii/result/page.tsx @@ -0,0 +1,333 @@ +'use client' +import { deleteData, getData, useCookieAuth } from '@/components/lib/api' +import { UiTable, downloadExcel, downloadExcelWithKoreanHeaders } from '@common/business_components/ui' +import { zodResolver } from '@hookform/resolvers/zod' +import { useState, useEffect } from 'react' +import { useForm } from 'react-hook-form' +import { z } from 'zod' +import BaseLayout from '../BaseLayout' +import { handleDelete } from '@/components/lib/utils' +import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue'; + + +const formSchema = z.object({ + enoCd: z.string().default('0'), // 환경청코드 + ctpvCd: z.string().default('0'), // 시도코드 + sggCd: z.object({ // 시군구 + val: z.string().default('0'), + text: z.string().default('전체'), + }).default({ val: '0', text: '전체' }), + swageTmplntCd: z.string().default('0'), // 하수처리장 + swtrmtZoneCd: z.string().default('0'), // 하수처리구역 + aplcnYmFrom: z.string().default(() => { // Date에서 string(yyyyMM 형식)으로 변경 + const now = new Date(); + const year = now.getFullYear(); + // const month = String(now.getMonth() + 1).padStart(2, '0'); + // return `${year}${month}`; + return `${year}01`; + }), // 분석기간 시작연월 + aplcnYmTo: z.string().default(() => { // Date에서 string(yyyyMM 형식)으로 변경 + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + return `${year}${month}`; + }), // 분석기간 종료연월 +}) + +type ResultFormValues = z.infer<typeof formSchema> + +const tableColumn = [ + { id: 'rowNumber', title: '일련번호' }, + { id: 'enoNm', title: '환경청' }, + { id: 'ctpvNm', title: '시도' }, + { id: 'sggNm', title: '시군구' }, + { id: 'swageTmplntNm', title: '하수처리장' }, + // { id: 'swtrmtZoneNm', title: '처리구역' }, + { id: 'aplcnYm', title: '적용연월' }, + { id: 'drtwSwtcRt', title: '오수 전환율' }, + { id: 'anlsInfo', title: '침입수 분석정보' }, + { id: 'regDt', title: '분석일자' }, + { id: 'rgtrNm', title: '분석자' }, + { id: 'scsYn', title: '분석성공여부' } +] + +const Result = () => { + const form = useForm<ResultFormValues>({ + resolver: zodResolver(formSchema), + defaultValues: { + enoCd: '0', + ctpvCd: '0', + sggCd: { val: '0', text: '전체' }, + swageTmplntCd: '0', + swtrmtZoneCd: '0', + aplcnYmFrom: (() => { // 현재 연월을 yyyyMM 형식으로 설정 + const now = new Date(); + const year = now.getFullYear(); + // const month = String(now.getMonth() + 1).padStart(2, '0'); + // return `${year}${month}`; + return `${year}01`; + })(), + aplcnYmTo: (() => { // 현재 연월을 yyyyMM 형식으로 설정 + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + return `${year}${month}`; + })(), + } + }) + + const AUTH = useCookieAuth(); + + const [tableData, setTableData] = useState<any[]>([]); // API 조회 결과 저장 상태 + const [totalData, setTotalData] = useState(0) //총 데이터 수l + const [isLoadingResult, setIsLoadingResult] = useState<boolean>(false); // 로딩 상태 + const [selectedRowData, setSelectedRowData] = useState<any>(null); + const [resetSelection, setResetSelection] = useState<boolean>(false); + const [isResetPage, setIsResetPage] = useState<boolean>(false); + const [curPage, setCurPage] = useState(1); // 현재 페이지 + + useEffect(() => { + // setCurPage(1); + handleSearchButtonClick(); + }, []) + + // 페이지 변경 시마다 데이터 fetch + useEffect(() => { + setIsResetPage(false); + handleSearchButtonClick(); + }, [curPage]); + + // 조회 버튼 클릭 시 1페이지로 이동 및 데이터 초기화 + const handleSearch: () => Promise<void> = async () => { + setCurPage(1); // 페이지를 1로 초기화 + setIsResetPage(true); // 페이지를 초기화 + await handleSearchButtonClick(); + }; + + const handleSearchButtonClick = async () => { + setIsLoadingResult(true); + setResetSelection(true); + const formValues = form.getValues(); // react-hook-form에서 현재 값 가져오기 + console.log("formValues", formValues); + + + + // swageTmplntCd 처리 로직 추가 + const processedSwageTmplntCd = formValues.swageTmplntCd && formValues.swageTmplntCd !== '0' + ? (() => { + const valString = String(formValues.swageTmplntCd); + const parts = valString.split('.'); + return parts.length > 1 ? parts[1] : valString; + })() + : null; + + let inftwtAnlsSearchDto = { + "aplcnYmFrom": formValues.aplcnYmFrom && formValues.aplcnYmFrom !== '' ? formValues.aplcnYmFrom : null, + "aplcnYmTo": formValues.aplcnYmTo && formValues.aplcnYmTo !== '' ? formValues.aplcnYmTo : null, + "enoCd": formValues.enoCd === '0' ? null : formValues.enoCd, + "ctpvCd": formValues.ctpvCd === '0' ? null : formValues.ctpvCd, + "sggCd": formValues.sggCd?.val === '0' ? null : formValues.sggCd?.val, + "swageTmplntCd": processedSwageTmplntCd, + // "swtrmtZoneCd": formValues.swtrmtZoneCd === '0' ? null : formValues.swtrmtZoneCd, + "auth": AUTH, + }; + + console.log("Search DTO:", inftwtAnlsSearchDto); + + const queryParams = new URLSearchParams({ + page: `${curPage - 1}`, + size: '10', + sort: 'inftwtAnlsSn,desc' + }); + Object.entries(inftwtAnlsSearchDto).forEach(([key, value]) => { + if (value !== undefined && value !== null && String(value).trim() !== '') { // undefined, null, 빈 문자열이 아닌 경우에만 추가 + queryParams.append(key, String(value)); + } + }); + const queryString = queryParams.toString(); + + console.log("API: ", `v1/inftwtAnls${queryString ? `?${queryString}` : ''}`); + try { + const data = await getData(`v1/inftwtAnls${queryString ? `?${queryString}` : ''}`); + console.log("API response:", data); + + // 🎯 각 아이템을 개별적으로 포맷팅하면서 일련번호 추가 + const dataWithRowNumber = (data.content || []).map((item: any, index: number) => { + // 🎯 먼저 각 아이템을 포맷팅 + const formattedItem = formatDisplayData(item); + + // 추가 처리 및 일련번호 추가 + return { + ...formattedItem, + rowNumber: (curPage - 1) * 10 + index + 1, // 1부터 시작하는 일련번호 추가 + scsYn: item.scsYn === null || item.scsYn === undefined ? 'N' : item.scsYn // scsYn이 null이면 'N'으로 변경 + }; + }); + + // 선택된 행 초기화 + setSelectedRowData(null); + + setTableData(dataWithRowNumber); + setTotalData(data?.totalElements || 0); // totalElements가 없을 경우 0으로 설정 + console.log("response:", data); + console.log("response data:", dataWithRowNumber); + setIsLoadingResult(false); + } catch (error) { + setIsLoadingResult(false); + console.error("Error fetching analysis result data:", error); + alert("분석결과 조회 중 오류가 발생했습니다."); + setTableData([]); + setTotalData(0); + setSelectedRowData(null); + } + setResetSelection(false); + }; + + // 테이블 셀 클릭 핸들러 추가 + const handleCellClick = (index: number | undefined, headId: string) => { + if (index !== undefined && tableData[index]) { + setSelectedRowData(tableData[index]); + console.log("Selected row data:", tableData[index]); + } + }; + + // Excel 다운로드 핸들러 추가 + const handleExcelDownload = async () => { + setIsLoadingResult(true); + console.log("Selected row data for download:", selectedRowData); + if (!selectedRowData || selectedRowData.inftwtAnlsSn === undefined) { + setIsLoadingResult(false); + alert('다운로드할 데이터를 선택해주세요.'); + return; + } + + try { + const data = await getData(`v1/inftwtAnlsRslt/ppl/${selectedRowData.inftwtAnlsSn}`); + + // 선택된 행 데이터를 Excel 다운로드 형식으로 변환 + const excelData = data.content; // 배열로 감싸서 전달 + + // 헤더 매핑 정의 + const headerMapping: { [key: string]: string } = { + 'inftwtAnlsSn': '침입수분석\n일련번호', + 'pplMngNo': '관로\n관리번호', + 'anlsTm': '분석시각', + 'predcOilq': '예측유량\n(m³/h)' + }; + + // 파일명 생성 (분석번호 기반) + const fileName = `침입수분석결과_${selectedRowData.swageTmplntNm}_${selectedRowData.aplcnYm}_${selectedRowData.inftwtAnlsSn}`; + + await downloadExcelWithKoreanHeaders({ + data: excelData as JSON[], + fileName: fileName, + sheetNameArray: ["침입수 분석결과"], + headerRange: "A1:D1", + headerMapping: headerMapping + }); + setIsLoadingResult(false); + } catch (error) { + setIsLoadingResult(false); + console.error("Excel 다운로드 중 오류 발생:", error); + alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요."); + } + }; + + // 파일 삭제 핸들러 추가 + const handleFileDelete = async () => { + setIsLoadingResult(true); + console.log("Selected row data for deletion:", selectedRowData); + if (!selectedRowData || selectedRowData.inftwtAnlsSn === undefined) { + setIsLoadingResult(false); + alert('삭제할 파일을 선택해주세요.'); + return; + } + // 삭제 확인 대화상자 + const confirmDelete = window.confirm('선택된 분석 결과를 삭제하시겠습니까?'); + if (!confirmDelete) { + return; + } + + try { + // // 삭제 API 호출 + console.log("Deleting analysis result with inftwtAnlsSn:", selectedRowData.inftwtAnlsSn); + const response1 = await deleteData(`v1/inftwtAnlsRslt/${selectedRowData.inftwtAnlsSn}`); + const response2 = await deleteData(`v1/inftwtAnls/${selectedRowData.inftwtAnlsSn}`); + // 첨부파일(inp파일) 삭제 API 호출 + handleDelete(selectedRowData.atflCd, { + handleDeleteStart: () => console.log("파일 삭제 시작"), + handleDeleteSuccess: (response) => console.log("파일 삭제 성공:", response), + handleDeleteError: (error) => console.error("파일 삭제 실패:", error), + }); + console.log("File delete response1:", response1); + console.log("File delete response2:", response2); + + // 테이블 선택 상태 초기화 트리거 + setResetSelection(true); + + // 검색 버튼 클릭 이벤트 트리거 + await handleSearchButtonClick(); + + setIsLoadingResult(false); + + alert('삭제가 완료되었습니다.'); + + setResetSelection(false); + } catch (error) { + setIsLoadingResult(false); + console.error("분석결과 파일 삭제 중 오류 발생:", error); + alert("분석결과 파일 삭제 중 오류가 발생했습니다. 다시 시도해주세요."); + } + }; + + + return ( + <BaseLayout + selectBox={{ title: '분석 대상지역 선택', searchChk: true, onSearchClick: handleSearch }} + titleName={{ mainTitle: '분석결과', firstCardTitle: '분석이력' }} + form={form} // form 인스턴스 전달 + onSubmitEvent={() => { }} // BaseLayout의 form 태그에 연결될 onSubmit + graphBtn={true} + showDatePicker={true} + enoCd={form.watch("enoCd")} + setEnoCd={(val) => form.setValue("enoCd", val)} + ctpvCd={form.watch("ctpvCd")} + setCtpvCd={(val) => form.setValue("ctpvCd", val)} + sigungu={form.watch("sggCd")} + setSigungu={(val) => form.setValue("sggCd", val)} + selectedSwageTreatmentPlant={form.watch("swageTmplntCd")} + onSwageTreatmentPlantChange={(val) => form.setValue("swageTmplntCd", String(val))} + selectedSwageProcessingZone={form.watch("swtrmtZoneCd")} + onSwageProcessingZoneChange={(val) => form.setValue("swtrmtZoneCd", String(val))} + aplcnYmFrom={form.watch("aplcnYmFrom")} + setAplcnYmFrom={(val: string) => { form.setValue("aplcnYmFrom", val) }} + aplcnYmTo={form.watch("aplcnYmTo")} + setAplcnYmTo={(val: string) => { form.setValue("aplcnYmTo", val) }} + isLoading={isLoadingResult} // 로딩 상태 전달 + // Excel 다운로드 핸들러 추가 + onExcelDownload={handleExcelDownload} + onDeleteFile={handleFileDelete} + selectedRowData={selectedRowData} + > + {[<div key='table1' className="h-[58vh] flex flex-col border rounded-lg bg-white shadow-md"> + {/* <div className="mb-[1.2rem] text-xl font-medium">○ 침입수 산정결과 현황</div> */} + <div className="flex-grow overflow-auto"> + <UiTable + headName='' + headlist={tableColumn} + tableData={tableData} + total={totalData} + pageSize={10} + cellClick={handleCellClick} + onPageChange={setCurPage} + resetSelection={resetSelection} + resetPageNo={isResetPage} + /> + </div> + </div> + ]} + </BaseLayout> + ) +} + +export default Result \ No newline at end of file diff --git a/apps/sn/app/(with-base-layout)/layout.tsx b/apps/sn/app/(with-base-layout)/layout.tsx new file mode 100644 index 000000000..3c1a527d7 --- /dev/null +++ b/apps/sn/app/(with-base-layout)/layout.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { usePathname, useRouter } from "next/navigation"; +import { useQueryClient } from "@tanstack/react-query"; +import { BaseLayout } from "@common/components"; +import type { MenuItem } from "@/entities/menu"; +import { useMenuStore } from "@/shared/lib"; +import { PageLayout } from "@/shared/ui/page-layout"; + +export default function WithBaseLayout({ + children, +}: { + children: React.ReactNode; +}) { + const router = useRouter(); + const pathname = usePathname(); + const queryClient = useQueryClient(); + + const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo); + + const handleMenuClick = (item: MenuItem) => { + setCurrentMenuInfo(item); + router.push(item.urlPath); + }; + + return ( + <BaseLayout + queryClient={queryClient} + currentPath={pathname ?? ""} + handleMenuClick={handleMenuClick} + > + <PageLayout>{children}</PageLayout> + </BaseLayout> + ); +} diff --git a/apps/sn/app/(with-base-layout)/oper/floodPre/page.tsx b/apps/sn/app/(with-base-layout)/oper/floodPre/page.tsx new file mode 100644 index 000000000..4d1c0fbe8 --- /dev/null +++ b/apps/sn/app/(with-base-layout)/oper/floodPre/page.tsx @@ -0,0 +1,230 @@ +'use client' +import { deleteData, postData } from '@/components/lib/api' +import { searchData } from '@/components/lib/common/apiHelpers' +import { formatDate, handleDownload } from '@/components/lib/utils' +import LoadingOverlay from '@/components/ui/LoadingBar' +import { SearchDiv, SearchFormLeft, SearchFormRight, UiTable, UiModal } from '@common/business_components/ui' +import ListDiv from '@common/business_components/ui/list_div' +import { Button, DatePicker, Input, Label } from '@common/components/ui' +import { useEffect, useState } from 'react' +import { tableHeadlist } from '..' +import OperFileUploadForm from '../../../components/ui/operFileUploadForm' + + +const Page = () => { + // 조회조건 + const [fromDate, setFromDate] = useState(() => { const date = new Date(); date.setDate(date.getDate() - 7); return date }) + const [toDate, setToDate] = useState(new Date()) + const [usr, setUsr] = useState('') + const [pjNm, setpjNm] = useState('') + // 파일업로드 관련 상태 + const [fileNm, setFileNm] = useState('') //파일명 + const [atflId, setAtflId] = useState('') //파일 ID + const [sbmrgPrevntBizSn, setSbmrgPrevntBizSn] = useState('') //파일 일련번호 + const [tableData, setTableData] = useState<any>([{}]) //테이블 데이터 + const [curPage, setCurPage] = useState(1) //현재 페이지 + const [totalData, setTotalData] = useState(0) //총 데이터 수 + const [isLoading, setIsLoading] = useState(true) //로딩 상태 + const [isModalOpen, setIsModalOpen] = useState(false) // 모달 상태 + const [resetSelection, setResetSelection] = useState<boolean>(false); // 선택 초기화 상태 + const [isResetPage, setIsResetPage] = useState<boolean>(false); + + useEffect(() => { + setCurPage(1) + fetchData() + }, []) + + // 페이지 변경 시마다 데이터 fetch + useEffect(() => { + setIsResetPage(false); + fetchData(); + }, [curPage]); + + // 조회 버튼 클릭 시 1페이지로 이동 및 데이터 초기화 + const handleSearch: () => Promise<void> = async () => { + setCurPage(1); // 페이지를 1로 초기화 + setIsResetPage(true); // 페이지를 초기화 + await fetchData(); + }; + + // 조회 + const fetchData = async () => { + setIsLoading(true) + setResetSelection(true); + // searchCmmnCd('SITEKNDCD') + try { + const { data, total } = await searchData('v1/sbmrgPrevnt', { + fromRegDt: formatDate(fromDate), + toRegDt: formatDate(toDate), + sbmrgPrevntBizNm: pjNm, + rgtrNm: usr, + page: curPage - 1, + size: '10' + }) + // 일련번호를 추가한 데이터로 변환 (전체 데이터 기준으로 일련번호 계산) + const dataWithRowNumber = (data || []).map((item: any, index: number) => ({ + ...item, + rowNumber: (curPage - 1) * 10 + index + 1, // 페이지 기준 일련번호 계산 + })); + setTableData(dataWithRowNumber) + setTotalData(total) + } + catch (error) { console.error('Error fetching data:', error) } + finally { + setFileNm(''); + setAtflId(''); + setSbmrgPrevntBizSn(''); + setIsLoading(false); + setResetSelection(false); + } + } + + // 파일 다운로드 + const isChckFile = () => { + if (fileNm === '' || fileNm === undefined) { + alert('파일을 선택해주세요') + return false + } + handleDownload(`/attach-service/api/v1/download/${atflId}`, fileNm) + } + + // 선택 파일 삭제 + const isDeleteFile = async () => { + if (sbmrgPrevntBizSn === '' || sbmrgPrevntBizSn === undefined) { + alert('삭제할 파일을 선택해주세요') + return false + } + const data = await deleteData(`/attach-service/api/v1/atfl/attachments/${atflId}`) + if (data) { + const delData = await deleteData(`v1/sbmrgPrevnt/${sbmrgPrevntBizSn}`) + if (delData.status === 200) { + alert('삭제되었습니다.') + fetchData() + } + } + else { + alert('삭제에 실패했습니다.') + } + } + + // 현황 등록 + const registFile = async (fileName: string, year: string, quater: string, cd: string, id: string) => { + const data = await postData('v1/sbmrgPrevnt', { + atflNm: fileName, + mngYr: year, + mngQu: quater, + atflCd: cd, + atflId: id, + sbmrgPrevntBizNm: fileName + }) + if (data) { + alert('파일이 등록되었습니다.'); + setIsModalOpen(false); + handleSearch(); + } + } + + // 등록 버튼 클릭 핸들러 + const handleRegisterClick = () => { + setIsModalOpen(true); + } + + + return ( + <> + <LoadingOverlay isLoading={isLoading ?? false} /> + <SearchDiv> + <SearchFormLeft> + <div className='flex items-center gap-5'> + <div className='flex items-center gap-2'> + <Label className="w-20 text-xl text-gray-600">등록기간</Label> + <DatePicker mode={'single'} + selected={fromDate} + onSelect={(date: Date | undefined) => { if (date) setFromDate(date) }} + modifiers={{ + disabled: { after: new Date() } + }} + /> + <span>~</span> + <DatePicker mode={'single'} + selected={toDate} + onSelect={(date: Date | undefined) => { if (date) setToDate(date) }} + modifiers={{ + disabled: { after: new Date() } + }} + /> + </div> + <div className='flex items-center'> + <Label className="w-20 text-xl text-gray-600">등록자</Label> + <Input + className='w-[100px]' + value={usr} + onChange={(e) => setUsr(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> + <div className='flex items-center gap-5'> + <Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size="sm" onClick={async () => await handleSearch()} >조회</Button> + <Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size="sm" onClick={handleRegisterClick} >등록</Button> + </div> + </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" onClick={isChckFile} >파일 다운로드</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" onClick={isDeleteFile} >삭제</Button> + </div> + </div> + <UiTable + headName='' + tableData={tableData} + headlist={tableHeadlist} + pageSize={10} + total={totalData} + cellClick={(index) => { + if (index != null) { + const fileName = tableData[index]?.sbmrgPrevntBizNm ?? ""; + const id = tableData[index]?.atflId ?? ''; + setFileNm(fileName); + setAtflId(id); + setSbmrgPrevntBizSn(tableData[index]?.sbmrgPrevntBizSn); + } + }} + onPageChange={setCurPage} + resetSelection={resetSelection} + resetPageNo={isResetPage} + /> + </ListDiv> + {/* 파일 업로드 모달 */} + <UiModal + open={isModalOpen} + onOpenChange={setIsModalOpen} + bodyClass="w-auto max-w-[90vw] min-w-[600px] h-auto" + noHeaderYn="N" + noFooterYn="Y" + body={ + <OperFileUploadForm + title='도시침수 예방사업 추진현황 등록' + onSubmit={async (file, year, quater, cd, id) => { + await registFile(file, year, quater, cd, id) + }} + /> + } + /> + </> + ) +} + +export default Page \ No newline at end of file diff --git a/apps/sn/app/(with-base-layout)/oper/index.ts b/apps/sn/app/(with-base-layout)/oper/index.ts new file mode 100644 index 000000000..b9dd98685 --- /dev/null +++ b/apps/sn/app/(with-base-layout)/oper/index.ts @@ -0,0 +1,19 @@ +export const tableHeadlist = [ + { id: 'rowNumber', title: '일련번호' }, + // { id: 'sbmrgPrevntBizSn', title: '일련번호' }, + { id: 'sbmrgPrevntBizNm', title: '파일명' }, + { id: 'mngYr', title: '년도' }, + { id: 'mngQu', title: '분기' }, + { id: 'regDt', title: '등록 일자' }, + { id: 'rgtr', title: '등록자' } + +] +export const tableHeadlistOldPp = [ + { id: 'rowNumber', title: '일련번호' }, + // { id: 'dcrpPplMtncBizSn', title: '일련번호' }, + { id: 'dcrpPplMtncBizNm', title: '파일명' }, + { id: 'mngYr', title: '년도' }, + { id: 'mngQu', title: '분기' }, + { id: 'regDt', title: '등록 일자' }, + { id: 'rgtr', title: '등록자' } +] \ No newline at end of file diff --git a/apps/sn/app/(with-base-layout)/oper/oldPp/page.tsx b/apps/sn/app/(with-base-layout)/oper/oldPp/page.tsx new file mode 100644 index 000000000..4f46aee91 --- /dev/null +++ b/apps/sn/app/(with-base-layout)/oper/oldPp/page.tsx @@ -0,0 +1,225 @@ +'use client' +import { deleteData, postData } from '@/components/lib/api' +import { searchData } from '@/components/lib/common/apiHelpers' +import { formatDate, handleDownload, yearOptions } from '@/components/lib/utils' +import LoadingOverlay from '@/components/ui/LoadingBar' +import { SearchDiv, SearchFormLeft, SearchFormRight, UiTable, UiModal } from '@common/business_components/ui' +import ListDiv from '@common/business_components/ui/list_div' +import { Button, DatePicker, Input, Label } from '@common/components/ui' +import { useEffect, useState } from 'react' +import { tableHeadlistOldPp } from '..' +import OperFileUploadForm from '../../../components/ui/operFileUploadForm' + + +const Page = () => { + const [fromDate, setFromDate] = useState<Date>(() => { const date = new Date(); date.setDate(date.getDate() - 7); return date }); + const [toDate, setToDate] = useState<Date>(new Date()); + const [usr, setUsr] = useState('') + const [pjNm, setpjNm] = useState('') + const [fileNm, setFileNm] = useState('') + const [atflId, setAtflId] = useState('') + const [dcrpPplMtncBizSn, setDcrpPplMtncBizSn] = useState('') + const [tableData, setTableData] = useState<any>([{}]) + const [curPage, setCurPage] = useState(1) + const [totalData, setTotalData] = useState(0) //총 데이터 수 + const [isLoading, setIsLoading] = useState(true) //로딩 상태 + const [isModalOpen, setIsModalOpen] = useState(false) // 모달 상태 + const [resetSelection, setResetSelection] = useState<boolean>(false); // 선택 초기화 상태 + const [isResetPage, setIsResetPage] = useState<boolean>(false); + + useEffect(() => { + setCurPage(1) + fetchData() + }, []) + + // 페이지 변경 시마다 데이터 fetch + useEffect(() => { + setIsResetPage(false); + fetchData(); + }, [curPage]); + + // 조회 버튼 클릭 시 1페이지로 이동 및 데이터 초기화 + const handleSearch: () => Promise<void> = async () => { + setCurPage(1); // 페이지를 1로 초기화 + setIsResetPage(true); // 페이지를 초기화 + await fetchData(); + }; + + // 데이터 조회 + const fetchData = async () => { + setIsLoading(true) + setResetSelection(true); + try { + const { data, total } = await searchData('v1/dcrpPpl', { + fromRegDt: formatDate(fromDate), + toRegDt: formatDate(toDate), + dcrpPplMtncBizNm: pjNm, + rgtrNm: usr, + page: curPage - 1, + size: '10' + }) + // 일련번호를 추가한 데이터로 변환 (전체 데이터 기준으로 일련번호 계산) + const dataWithRowNumber = (data || []).map((item: any, index: number) => ({ + ...item, + rowNumber: (curPage - 1) * 10 + index + 1, // 페이지 기준 일련번호 계산 + })); + setTableData(dataWithRowNumber) + setTotalData(total) + } catch (error) { + console.error('Error fetching data:', error) + } finally { + setFileNm(''); + setAtflId(''); + setDcrpPplMtncBizSn(''); + setIsLoading(false) + setResetSelection(false); + } + } + + // 파일 다운로드 + const isChckFile = () => { + if (fileNm === '' || fileNm === undefined) { + alert('파일을 선택해주세요') + return false + } + handleDownload(`/attach-service/api/v1/download/${atflId}`, fileNm) + return true + } + + // 파일 삭제 + const isDeleteFile = async () => { + if (dcrpPplMtncBizSn === '' || dcrpPplMtncBizSn === undefined) { + alert('삭제할 파일을 선택해주세요') + return false + } + const data = await deleteData(`/attach-service/api/v1/atfl/attachments/${atflId}`) + if (data) { + const delData = await deleteData(`v1/dcrpPpl/${dcrpPplMtncBizSn}`) + if (delData.status == 200) { + alert('삭제되었습니다.') + fetchData() + } else { + alert('삭제에 실패했습니다.') + } + } + } + + // 파일 등록 + const registFile = async (fileNm: string, year: string, quater: string, cd: string, id: string) => { + const data = await postData('v1/dcrpPpl', { + atflNm: fileNm, + mngYr: year, + mngQu: quater, + atflCd: cd, + atflId: id, + dcrpPplMtncBizNm: fileNm + }) + if (data) { + alert('파일이 등록되었습니다.'); + setIsModalOpen(false); + handleSearch(); + } + } + + // 등록 버튼 클릭 핸들러 + const handleRegisterClick = () => { + setIsModalOpen(true); + } + + + return ( + <> + <LoadingOverlay isLoading={isLoading ?? false} /> + <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={fromDate} + onSelect={(date: Date | undefined) => { if (date) setFromDate(date) }} + modifiers={{ + disabled: { after: new Date() } + }} /> + <span>~</span> + <DatePicker mode={'single'} + selected={toDate} + onSelect={(date: Date | undefined) => { if (date) setToDate(date) }} + modifiers={{ + disabled: { after: new Date() } + }} /> + <div className='flex items-center'> + <Label className="w-20 text-xl text-gray-600">등록자</Label> + <Input + className='w-[100px] !text-2xl' + value={usr} + onChange={(e) => setUsr(e.target.value)} + /> + </div> + <div className='flex items-center'> + <Label className="w-20 text-xl text-gray-600">파일명</Label> + <Input + className='w-[100px] !text-2xl' + value={pjNm} + onChange={(e) => { setpjNm(e.target.value) }} + /> + </div> + </div> + </div> + </SearchFormLeft> + <SearchFormRight> + <div className='flex items-center gap-5'> + <Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size="sm" onClick={async () => await handleSearch()} >조회</Button> + <Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size="sm" onClick={handleRegisterClick} >등록</Button> + </div> + </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" onClick={isChckFile}>파일 다운로드</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" onClick={isDeleteFile}>삭제</Button> + </div> + </div> + <UiTable + headName='' + tableData={tableData} + headlist={tableHeadlistOldPp} + pageSize={10} + total={totalData} + children={undefined} + cellClick={(index) => { + if (index != null) { + const fileName = tableData[index]?.dcrpPplMtncBizNm ?? ""; + const id = tableData[index]?.atflId ?? ''; + setFileNm(fileName); + setAtflId(id); + setDcrpPplMtncBizSn(tableData[index]?.dcrpPplMtncBizSn); + } + }} + onPageChange={setCurPage} + resetSelection={resetSelection} + /> + </ListDiv> + {/* 파일 업로드 모달 */} + <UiModal + open={isModalOpen} + onOpenChange={setIsModalOpen} + bodyClass="w-auto max-w-[90vw] min-w-[600px] h-auto" + noHeaderYn="N" + noFooterYn="Y" + body={ + <OperFileUploadForm + title='노후 하수관로 정비사업 추진현황 등록' + onSubmit={async (file, year, quater, cd, id) => { + await registFile(file, year, quater, cd, id) + }} + /> + } + /> + </> + ) +} + +export default Page \ No newline at end of file diff --git a/apps/sn/app/(with-base-layout)/selectionValidation.tsx b/apps/sn/app/(with-base-layout)/selectionValidation.tsx new file mode 100644 index 000000000..8cc506d0e --- /dev/null +++ b/apps/sn/app/(with-base-layout)/selectionValidation.tsx @@ -0,0 +1,161 @@ +/** + * 선택 항목 검증 유틸리티 + */ + +export interface ApiSelectOption { + text: string; + val: string; +} + +// 선택 항목 검증 설정 타입 +export interface SelectionValidationConfig { + value: string | number | ApiSelectOption; + description: string; + required: boolean; +} + +// 선택 항목 검증 결과 타입 +export interface SelectionValidationResult { + isValid: boolean; + errorMessage?: string; +} + +/** + * 단일 선택 항목이 유효한지 검증하는 함수 + * @param value 검증할 값 + * @param isRequired 필수 선택 여부 + * @returns 유효한지 여부 + */ +export const isValidSelection = ( + value: string | number | ApiSelectOption, + isRequired: boolean = true +): boolean => { + if (!isRequired) return true; + + if (typeof value === 'object' && value !== null) { + // ApiSelectOption 객체인 경우 + return value.val !== '0' && value.val !== '' && value.val !== null && value.val !== undefined; + } + + // 문자열 또는 숫자인 경우 + return value !== '0' && value !== 0 && value !== '' && value !== null && value !== undefined; +}; + +/** + * 여러 선택 항목을 한 번에 검증하는 함수 + * @param validationConfigs 검증할 선택 항목들의 설정 배열 + * @returns 검증 결과. 실패 시 첫 번째 에러 메시지 반환 + */ +export const validateMultipleSelections = ( + validationConfigs: SelectionValidationConfig[] +): SelectionValidationResult => { + for (const config of validationConfigs) { + if (!isValidSelection(config.value, config.required)) { + return { + isValid: false, + errorMessage: `${config.description}을(를) 선택해주세요.` + }; + } + } + + return { isValid: true }; +}; + +/** + * 여러 선택 항목을 한 번에 검증하고 alert 표시하는 함수 + * @param validationConfigs 검증할 선택 항목들의 설정 배열 + * @returns 검증 결과 + */ +export const validateMultipleSelectionsWithAlert = ( + validationConfigs: SelectionValidationConfig[] +): boolean => { + const result = validateMultipleSelections(validationConfigs); + if (!result.isValid && result.errorMessage) { + alert(result.errorMessage); + return false; + } + return true; +}; + +/** + * 기본 선택 항목들 검증 (환경청, 시도, 시군구, 하수처리장) + * @param select1 환경청코드 + * @param select2 시도코드 + * @param select3 시군구 객체 + * @param select4 하수처리장코드 + * @param options 각 항목의 필수 여부 설정 + * @returns 검증 결과 + */ +export const validateBasicSelections = ( + select1: string, + select2: string, + select3: ApiSelectOption, + select4: string | number, + options: { + requireEnv?: boolean; + requireSido?: boolean; + requireSigungu?: boolean; + requireTreatmentPlant?: boolean; + } = {} +): SelectionValidationResult => { + const { + requireEnv = true, + requireSido = true, + requireSigungu = true, + requireTreatmentPlant = true + } = options; + + const validationConfigs: SelectionValidationConfig[] = [ + { + value: select1, + description: '환경청', + required: requireEnv + }, + { + value: select2, + description: '시도', + required: requireSido + }, + { + value: select3, + description: '시군구', + required: requireSigungu + }, + { + value: select4, + description: '하수처리장', + required: requireTreatmentPlant + } + ]; + + return validateMultipleSelections(validationConfigs); +}; + +/** + * 기본 선택 항목들 검증 및 alert 표시 + * @param select1 환경청코드 + * @param select2 시도코드 + * @param select3 시군구 객체 + * @param select4 하수처리장코드 + * @param options 각 항목의 필수 여부 설정 + * @returns 검증 결과 + */ +export const validateBasicSelectionsWithAlert = ( + select1: string, + select2: string, + select3: ApiSelectOption, + select4: string | number, + options: { + requireEnv?: boolean; + requireSido?: boolean; + requireSigungu?: boolean; + requireTreatmentPlant?: boolean; + } = {} +): boolean => { + const result = validateBasicSelections(select1, select2, select3, select4, options); + if (!result.isValid && result.errorMessage) { + alert(result.errorMessage); + return false; + } + return true; +}; \ No newline at end of file diff --git a/apps/sn/app/clientLayout.tsx b/apps/sn/app/clientLayout.tsx deleted file mode 100644 index e8fdbe032..000000000 --- a/apps/sn/app/clientLayout.tsx +++ /dev/null @@ -1,7 +0,0 @@ -'use client' - -import { BaseLayout } from "@common/components"; - -export function ClientLayout({ children }: { children: React.ReactNode }) { - return <BaseLayout>{children}</BaseLayout>; -} \ 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 index d7db93f77..ab8936329 100644 --- a/apps/sn/app/components/lib/api/index.ts +++ b/apps/sn/app/components/lib/api/index.ts @@ -1,68 +1,136 @@ +import { useEffect, useState } from "react"; + const BASE_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ''; -export const postData = async (url: string, data: object): Promise<any> => { - const options: RequestInit = { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(data), - }; - const res = await fetch(`${BASE_URL}/${url}`, options); - if (res.ok) { - const resData = await res.json(); - return resData; - // const text = await res.text(); - // if (text) { - // return JSON.parse(text); // JSON 데이터가 있으면 파싱하여 반환 - // } - // return true; // 응답 본문이 비어 있으면 true 반환 +export const getData = async (url: string, data?: object): Promise<any> => { + const res = await fetch(url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`, { + method: "GET", + }); + if (res.ok) { + try { + // 응답이 비어있는지 확인 + const contentLength = res.headers.get('content-length'); + if (contentLength === '0') { + return null; + } + + // 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인 + const text = await res.text(); + if (!text || text.trim() === '') { + return null; + } + + // JSON 파싱 시도 + const data = JSON.parse(text); + return data; + } catch (error) { + console.error('JSON parsing error:', error); + // JSON 파싱에 실패한 경우, 원본 텍스트 또는 null 반환 + throw new Error('Invalid JSON response from server'); } + } + throw new Error('Failed to fetch data'); +}; + +export const postData = async (url: string, data: object): Promise<any> => { + const options: RequestInit = { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }; + const res = await fetch(`${BASE_URL}/${url}`, options); + console.log('res', res); + console.log('res.status', res.status); + if (res.ok) { + const resData = await res.json(); + return resData; + } }; export const postMutiPart = async (url: string, data: FormData): Promise<any> => { - const options: RequestInit = { - method: "POST", - body: data, - }; - const res = await fetch(`${BASE_URL}/${url}`, options); - const resData = await res.json(); - return resData; + const options: RequestInit = { + method: "POST", + body: data, + }; + const res = await fetch(`${BASE_URL}/${url}`, options); + const resData = await res.json(); + return resData; }; export const putData = async (url: string, data: object): Promise<any> => { - const options: RequestInit = { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(data), - }; - const res = await fetch(`${BASE_URL}/${url}`, options); - const resData = await res.json(); - return resData; -}; - -export const getData = async (url: string, data?: object): Promise<any> => { - const res = await fetch(url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`, { - method: "GET", - }); - if (res.ok) { - const data = await res.json(); - return data; - } - throw new Error('Failed to fetch data'); + 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 deleteData = async (url: string, data?: object): Promise<any> => { - const options: RequestInit = { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - body: data ? JSON.stringify(data) : undefined, + const options: RequestInit = { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: data ? JSON.stringify(data) : undefined, + }; + const res = await fetch(url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`, options); + if (res.status === 204) return true + return res; +}; + +// RSONE DC API 호출 +export const getDcData = async (url: string, data?: object): Promise<any> => { + const res = await fetch('/dc-service' + url, { + method: "GET", + }); + if (res.ok) { + const data = await res.json(); + return data; + } + throw new Error('Failed to fetch data'); +}; + +// RSONE DC API 호출 +export const getCmsData = async (url: string, data?: object): Promise<any> => { + console.log(url); + const res = await fetch('/cms-service' + url, { + method: "GET", + }); + if (res.ok) { + const data = await res.json(); + return data; + } + throw new Error('Failed to fetch data'); +}; + +export function getCookie(name: string) { + const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); + if (match && match[2]) { + return decodeURIComponent(match[2]); + } + return null; +} + +export const useCookieAuth = () => { + const [data, setData] = useState(""); + + useEffect(() => { + const fetch = async () => { + const result = getCookie("Authorization"); + if (result) { + setData(result); + } }; - const res = await fetch(url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`, options); - if (res.status === 204) return true - return res; + fetch(); + console.log("useCookieAuth 호출"); + }, []); + + return data; }; \ No newline at end of file diff --git a/apps/sn/app/components/lib/auth/lib/auth-wrapper.tsx b/apps/sn/app/components/lib/auth/lib/auth-wrapper.tsx deleted file mode 100644 index 0b72cc768..000000000 --- a/apps/sn/app/components/lib/auth/lib/auth-wrapper.tsx +++ /dev/null @@ -1,87 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -import { useRouter, usePathname } from "next/navigation"; -import axios from "axios"; -import Cookies from "js-cookie"; -import { useUserStore, type IUserInfo } from "@store"; -import { getCookie } from "@utils"; -import { LoginApi } from "../api/login-service"; -import { tokenManager } from "../lib/token-manager"; - -const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; -const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; - -const ENDPOINT = { - USER_SERVICE: { - LOGIN: "/user-service/login", - USERS: "/user-service/api/v1/users", - SILENT_REFRESH: "/user-service/api/v1/users/token/refresh", - AUTHORIZATIONS: "/user-service/api/v1/authorizations", - ROLE_AUTHORIZATIONS: "/user-service/api/v1/role-authorizations", - }, -}; - -const AuthWrapper = ({ children }: { children: React.ReactNode }) => { - const router = useRouter(); - const pathname = usePathname(); // 현재 경로 - const { userInfo, setUserInfo } = useUserStore((state) => state); - - async function authCheck() { - const checkToken = getCookie(AUTH_USER_ID); - - - if (!checkToken) { - //TODO 나중에 수정해야.... - const userId = "admin1gmail"; - const password = "test1234!"; - await LoginApi.login({ userId, password }); - // window.location.href = "http://dev.onasys.co.kr/auth/login"; - // throw new Error("Missing required authentication tokens"); - } - - const accessToken = tokenManager.getToken(CLAIM_NAME); - const tokenId = tokenManager.getToken(AUTH_USER_ID); - - const response = await axios.get<IUserInfo>( - `${ENDPOINT.USER_SERVICE.USERS}/info/me`, - { - withCredentials: true, - headers: { - [CLAIM_NAME]: accessToken, - [AUTH_USER_ID]: tokenId ?? "", - }, - }, - ); - - if (!userInfo) setUserInfo(response.data); - } - - useEffect(() => { - (async () => { - try { - await authCheck(); - } catch (error) { - console.error("Auth check failed:", error); - - try { - await LoginApi.silentRefresh(); - await authCheck(); - } catch (refreshError) { - console.error("Token refresh failed:", refreshError); - setUserInfo(null); - - // TODO 운영에 반영 할때 수정해야..... - // 현재 경로가 로그인 페이지가 아닐 때만 redirect - // if (!pathname.startsWith("/auth/login")) { - // window.location.href = "http://dev.onasys.co.kr/auth/login"; - // } - } - } - })(); - }, [pathname]); // pathname도 의존성에 포함 - - return <>{children}</>; -}; - -export { AuthWrapper }; diff --git a/apps/sn/app/components/lib/common/apiHelpers.ts b/apps/sn/app/components/lib/common/apiHelpers.ts index caaa3b050..4963516f7 100644 --- a/apps/sn/app/components/lib/common/apiHelpers.ts +++ b/apps/sn/app/components/lib/common/apiHelpers.ts @@ -1,24 +1,24 @@ import { getData } from "../api"; export const searchData = async<T extends Record<string, any>>(url: string, paramObj: T) => { - const params = new URLSearchParams( - Object.entries(paramObj) - .filter(([_, value]) => value !== undefined && value !== '') // 값이 있는 항목만 포함 - .reduce((acc, [key, value]) => { - acc[key] = String(value); - return acc; - }, {} as Record<string, string>) - ); - const data = await getData(`${url}?${params.toString()}`); - return data && { data: data.content, total: data.totalElements } + const params = new URLSearchParams( + Object.entries(paramObj) + .filter(([_, value]) => value !== undefined && value !== '') // 값이 있는 항목만 포함 + .reduce((acc, [key, value]) => { + acc[key] = String(value); + return acc; + }, {} as Record<string, string>) + ); + const data = await getData(`${url}?${params.toString()}`); + return data && { data: data.content, total: data.totalElements } } // 상위코드로 하위코드 조회 export const searchCmmnCd = async (upCd: string) => { - const data = await getData(`/cms-service/api/v1/code/cache/${upCd}`); - const filteredData = data.map((item: any) => ({ - lwpoCd: item.lwpoCd, - lwpoCdNm: item.lwpoCdNm, - })); - return filteredData; + const data = await getData(`/cms-service/api/v1/code/cache/${upCd}`); + const filteredData = data.map((item: any) => ({ + lwpoCd: item.lwpoCd, + lwpoCdNm: item.lwpoCdNm, + })); + return filteredData; } \ No newline at end of file diff --git a/apps/sn/app/components/lib/utils/date.ts b/apps/sn/app/components/lib/utils/date.ts index 75e3cbf32..a401ad738 100644 --- a/apps/sn/app/components/lib/utils/date.ts +++ b/apps/sn/app/components/lib/utils/date.ts @@ -1,13 +1,110 @@ export const formatDate = (date: Date): string => { - if (!date) return ''; - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); // 월은 0부터 시작하므로 +1 - const day = String(date.getDate()).padStart(2, '0'); - return `${year}-${month}-${day}`; + if (!date) return ''; + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); // 월은 0부터 시작하므로 +1 + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; }; - // 현재 년도부터 10년 전까지의 년도 옵션 생성 - export const yearOptions = Array.from({ length: 10 }, (_, i) => { - const year = new Date().getFullYear() - i; - return { text: `${year}년`, val: year.toString() }; - }); +// 현재 년도부터 10년 전까지의 년도 옵션 생성 +export const yearOptions = Array.from({ length: 10 }, (_, i) => { + const year = new Date().getFullYear() - i; + return { text: `${year}년`, val: year.toString() }; +}); + +// 현재 날짜를 기준으로 현재 분기까지의 분기 옵션 생성 +export const getQuarterOptions = (selectedYear?: string): { text: string; val: string }[] => { + const currentDate = new Date(); + const currentYear = currentDate.getFullYear().toString(); + const currentMonth = currentDate.getMonth() + 1; // 0부터 시작하므로 +1 + + // 현재 년도인 경우에만 현재 날짜까지의 분기 표시 + if (selectedYear === currentYear) { + // 현재 월을 기준으로 현재 분기 계산 + const currentQuarter = Math.ceil(currentMonth / 3); + + // 현재 분기까지의 분기 옵션 생성 + const quarterOptions = []; + for (let i = 1; i <= currentQuarter; i++) { + quarterOptions.push({ + text: `${i}분기`, + val: i.toString() + }); + } + + return quarterOptions; + } else { + // 이전 년도인 경우 모든 분기 표시 + return [ + { text: "1분기", val: "1" }, + { text: "2분기", val: "2" }, + { text: "3분기", val: "3" }, + { text: "4분기", val: "4" } + ]; + } +}; + +// ["202506", "202505", "202502", "202405"] 을 SelectBox의 옵션으로 변환 +export const convertToTextValArray = (yyyymms: string[]): { text: string; val: string }[] => { + return yyyymms.map(dateStr => { + const year = dateStr.slice(0, 4); + const month = dateStr.slice(4, 6); + return { + text: `${year}-${month}`, + val: dateStr + }; + }); +}; + +// 🎯 날짜 유효성 검사 함수 (8자리 또는 6자리 지원) +export const isValidDateString = (dateStr: string): boolean => { + // 숫자만 추출 + const cleanStr = dateStr.replace(/\D/g, ''); + + // 8자리 (yyyymmdd) 또는 6자리 (yyyymm) 확인 + if (cleanStr.length === 8) { + // 연도, 월, 일 추출 + const year = parseInt(cleanStr.substring(0, 4)); + const month = parseInt(cleanStr.substring(4, 6)); + const day = parseInt(cleanStr.substring(6, 8)); + + // 기본 범위 검사 + if (year < 1900 || year > 2200) return false; + if (month < 1 || month > 12) return false; + if (day < 1 || day > 31) return false; + + // Date 객체로 실제 유효한 날짜인지 검사 + const date = new Date(year, month - 1, day); + return date.getFullYear() === year && + date.getMonth() === month - 1 && + date.getDate() === day; + } + else if (cleanStr.length === 6) { + // 연도, 월 추출 + const year = parseInt(cleanStr.substring(0, 4)); + const month = parseInt(cleanStr.substring(4, 6)); + + // 기본 범위 검사 + if (year < 1900 || year > 2200) return false; + if (month < 1 || month > 12) return false; + + return true; + } + + return false; +}; + +// 🎯 날짜 포맷팅 함수 +export const formatDateString = (dateStr: string): string => { + const cleanStr = dateStr.replace(/\D/g, ''); // 숫자만 추출 + + if (!isValidDateString(cleanStr)) { + return dateStr; // 유효하지 않은 날짜면 원본 반환 + } + + const year = cleanStr.substring(0, 4); + const month = parseInt(cleanStr.substring(4, 6)); // parseInt로 앞의 0 제거 + const day = parseInt(cleanStr.substring(6, 8)); // parseInt로 앞의 0 제거 + + return `${year}년 ${month}월 ${day}일`; +}; \ No newline at end of file diff --git a/apps/sn/app/components/lib/utils/fileDeleteUtil.tsx b/apps/sn/app/components/lib/utils/fileDeleteUtil.tsx index bf8a70130..4cadd12e8 100644 --- a/apps/sn/app/components/lib/utils/fileDeleteUtil.tsx +++ b/apps/sn/app/components/lib/utils/fileDeleteUtil.tsx @@ -1,4 +1,4 @@ -import { getCookie } from "@utils"; +import { getCookie } from "@/shared/lib"; import { ENDPOINT } from "../api/attachment/attachment-service"; const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; diff --git a/apps/sn/app/components/lib/utils/fileUploadUtil.tsx b/apps/sn/app/components/lib/utils/fileUploadUtil.tsx index 9b3721cd0..3bfb35f00 100644 --- a/apps/sn/app/components/lib/utils/fileUploadUtil.tsx +++ b/apps/sn/app/components/lib/utils/fileUploadUtil.tsx @@ -1,5 +1,5 @@ import { Upload, type HttpResponse } from "tus-js-client"; -import { getCookie, setCookie } from "@utils"; +import { getCookie, setCookie } from "@/shared/lib"; import { ENDPOINT } from "../api/attachment/attachment-service"; const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; diff --git a/apps/sn/app/components/lib/utils/formatDisplayValue.tsx b/apps/sn/app/components/lib/utils/formatDisplayValue.tsx new file mode 100644 index 000000000..a5bd847cf --- /dev/null +++ b/apps/sn/app/components/lib/utils/formatDisplayValue.tsx @@ -0,0 +1,96 @@ +import { dateFields, numberFields, yearMonthFields, dateTimeFields } from "../../../(map-layout)/map/components/common/table"; +import { formatDateString } from "./date"; + +// 🎯 데이터 객체 전체를 포맷팅하는 함수 +export const formatDisplayData = (data: { [key: string]: any }): { [key: string]: string } => { + if (!data) return {}; + + const formattedData: { [key: string]: string } = {}; + + Object.keys(data).forEach(fieldId => { + const value = data[fieldId]; + + if (value === null || value === undefined || value === "") { + formattedData[fieldId] = "-"; + return; + } + + // 🎯 연월 포맷팅 (yyyymm -> yyyy년 m월) + if (yearMonthFields.includes(fieldId)) { + const dateStr = String(value).trim(); + if (/^\d{6}$/.test(dateStr)) { + const year = dateStr.substring(0, 4); + const month = parseInt(dateStr.substring(4, 6)); + formattedData[fieldId] = `${year}년 ${month}월`; + } else { + formattedData[fieldId] = String(value); + } + return; + } + + // 🎯 날짜시간 포맷팅 (yyyy-mm-dd hh:mm:ss -> yyyy년 m월 d일 hh:mm) + if (dateTimeFields.includes(fieldId)) { + const dateTimeStr = String(value).trim(); + // yyyy-mm-dd hh:mm:ss 형식 확인 + const dateTimeRegex = /^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})$/; + const match = dateTimeStr.match(dateTimeRegex); + + if (match && match.length >= 6) { + const year = match[1]; + const month = match[2]; + const day = match[3]; + const hour = match[4]; + const minute = match[5]; + const second = match[6]; + + // 🎯 undefined 체크 후 parseInt 적용 + if (year && month && day && hour && minute) { + const formattedMonth = parseInt(month); + const formattedDay = parseInt(day); + formattedData[fieldId] = `${year}년 ${formattedMonth}월 ${formattedDay}일 ${hour}시 ${minute}분 ${second}초`; + } else { + formattedData[fieldId] = String(value); + } + } else { + formattedData[fieldId] = String(value); + } + return; + } + + // 🎯 기존 날짜 포맷팅 (yyyymmdd -> yyyy년 m월 d일) + if (dateFields.includes(fieldId)) { + const dateStr = String(value).trim(); + + // 숫자 타입이거나 숫자로만 구성된 문자열인지 확인 + if (/^\d+$/.test(dateStr)) { + formattedData[fieldId] = formatDateString(dateStr); + } else { + formattedData[fieldId] = String(value); + } + return; + } + + // 🎯 숫자 필드는 천 단위 구분자 추가 + if (numberFields.includes(fieldId)) { + const numValue = Number(value); + if (!isNaN(numValue) && isFinite(numValue)) { + formattedData[fieldId] = numValue.toLocaleString('ko-KR'); + } else { + formattedData[fieldId] = String(value); + } + return; + } + + // 나머지는 문자열로 처리 + formattedData[fieldId] = String(value); + }); + + return formattedData; +}; + +// 🎯 단일 값만 포맷팅하는 함수 (기존 호환성을 위해 유지) +export const formatDisplayValue = (value: any, fieldId: string): string => { + const tempData = { [fieldId]: value }; + const formatted = formatDisplayData(tempData); + return formatted[fieldId] || String(value); +}; \ No newline at end of file diff --git a/apps/sn/app/components/lib/utils/index.tsx b/apps/sn/app/components/lib/utils/index.tsx index 0b9999c87..8bc8cf9be 100644 --- a/apps/sn/app/components/lib/utils/index.tsx +++ b/apps/sn/app/components/lib/utils/index.tsx @@ -1,4 +1,7 @@ -export { getCookie, setCookie, removeCookie } from "./cookie"; -export { handleUpload } from "./fileUploadUtil"; +export * from "./fileUploadUtil"; export * from "./fileDownload"; export * from './date' +export * from './excelToJson' +export * from './fileDeleteUtil' +export * from './fileDownload' +export * from './formatDisplayValue' diff --git a/apps/sn/app/components/ui/SidoSigunguSelectBox.tsx b/apps/sn/app/components/ui/SidoSigunguSelectBox.tsx index 0ea8bb510..7414185a6 100644 --- a/apps/sn/app/components/ui/SidoSigunguSelectBox.tsx +++ b/apps/sn/app/components/ui/SidoSigunguSelectBox.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from "react"; -import { SelectBox2 } from "@common/business_components/ui/select/index-2"; +import { SelectBox } from '@common/business_components/ui' interface Option { id: number; @@ -138,21 +138,23 @@ export function SidoSigunguSelectBox({ return ( <div className={`flex items-center justify-center ${className} mr-8`}> - <SelectBox2 + <SelectBox className="w-[150px] mr-8" selectClass="rounded-lg bg-white" label="시도" selectArray={sidoList.map(opt => ({ val: opt.id, text: opt.name }))} value={sido} onSelectValue={val => onSidoChange(Number(val))} + selectContentClass="z-[101]" /> - <SelectBox2 + <SelectBox className="w-48" selectClass="w-48 rounded-lg bg-white truncate" label="시군구" selectArray={sigunguList.map(opt => ({ val: opt.id, text: opt.name }))} value={sigungu} onSelectValue={val => onSigunguChange(Number(val))} + selectContentClass="z-[101]" /> </div> ); diff --git a/apps/sn/app/components/ui/YearRangeSelectBox.tsx b/apps/sn/app/components/ui/YearRangeSelectBox.tsx index 3dc8818fc..3e0318dac 100644 --- a/apps/sn/app/components/ui/YearRangeSelectBox.tsx +++ b/apps/sn/app/components/ui/YearRangeSelectBox.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { SelectBox2 } from "@common/business_components/ui/select/index-2"; +import { SelectBox } from '@common/business_components/ui' interface YearRangeSelectBoxProps { start: string; @@ -89,22 +89,24 @@ export function YearRangeSelectBox({ return ( <div className={`flex items-center ${className}`}> - <SelectBox2 - className="flex-1 mr-3 min-w-[120px] max-w-[170px]" + <SelectBox + className="flex-1 mr-3" selectClass={selectClass} label={startLabel} selectArray={getStartYearArray()} onSelectValue={val => onChange({ start: String(val), end })} value={start} + selectContentClass="z-[101]" /> <span className="font-semibold text-xl mx-2">~</span> - <SelectBox2 - className="flex-1 ml-1 mr-6 min-w-[70px] max-w-[150px]" + <SelectBox + className="flex-1 ml-1" selectClass={selectClass} label={endLabel} selectArray={getEndYearArray()} onSelectValue={val => onChange({ start, end: String(val) })} value={end} + selectContentClass="z-[101]" /> </div> ); diff --git a/apps/sn/app/components/ui/fileUploadForm.tsx b/apps/sn/app/components/ui/fileUploadForm.tsx index 0760b3252..fb626348e 100644 --- a/apps/sn/app/components/ui/fileUploadForm.tsx +++ b/apps/sn/app/components/ui/fileUploadForm.tsx @@ -1,148 +1,153 @@ "use client" import { FileUploader } from "@common/components"; import { Button } from '@common/components/ui'; -import { handleUpload } from "@utils"; +import { handleUpload } from "@/components/lib/utils"; import { useId, useState } from 'react'; import type { HttpResponse, Upload } from "tus-js-client"; interface FileUploadFormProps { - onSubmit?: (files: string, year: string, quarter: string, code: string, id: string) => void; - onFileSelect?: (file: File | null, atflCd: string | null, atflId: string | null) => void; // 파일 선택 시 호출될 콜백 추가 - showButton?: boolean; // 버튼 표시 여부 (기본값 true) - showPreview?: boolean; // 파일명 프리뷰 표시 여부 - accept?: string; // 파일 형식 지정 - fileIdx?: number; // 부모가 어떤 FileUploadForm인지 구분하기 위한 인덱스 + onSubmit?: (files: string, year: string, quarter: string, code: string, id: string) => void; + onFileSelect?: (file: File | null, atflCd: string | null, atflId: string | null) => void; // 파일 선택 시 호출될 콜백 추가 + showButton?: boolean; // 버튼 표시 여부 (기본값 true) + showPreview?: boolean; // 파일명 프리뷰 표시 여부 + accept?: string; // 파일 형식 지정 + fileIdx?: number; // 부모가 어떤 FileUploadForm인지 구분하기 위한 인덱스 } const FileUploadForm = ({ - onSubmit, - onFileSelect, - showButton = true, - showPreview = false, - accept = ".jpeg, .jpg, .png", - fileIdx // props에서 fileIdx 받기 + onSubmit, + onFileSelect, + showButton = true, + showPreview = false, + accept = ".jpeg, .jpg, .png", + fileIdx // props에서 fileIdx 받기 }: FileUploadFormProps) => { - const [internalAtflCd, setInternalAtflCd] = useState<string | null>(null); - const [internalAtflId, setInternalAtflId] = useState<string | null>(null); - const [currentFile, setCurrentFile] = useState<File | null>(null); // 현재 선택된 파일을 저장할 상태 - const [fileName, setFileName] = useState<string>(''); - const [files, setFiles] = useState<FileList | null>(null); - const [upload, setUpload] = useState<Upload | null>(null); - const uniqueId = useId(); // 각 FileUploadForm 인스턴스에 대한 고유 ID 생성 + const [internalAtflCd, setInternalAtflCd] = useState<string | null>(null); + const [internalAtflId, setInternalAtflId] = useState<string | null>(null); + const [currentFile, setCurrentFile] = useState<File | null>(null); // 현재 선택된 파일을 저장할 상태 + const [fileName, setFileName] = useState<string>(''); + const [files, setFiles] = useState<FileList | null>(null); + const [upload, setUpload] = useState<Upload | null>(null); + const uniqueId = useId(); // 각 FileUploadForm 인스턴스에 대한 고유 ID 생성 + // FileUploader 컴포넌트를 강제로 재마운트하기 위한 key 상태 추가 + const [uploaderKey, setUploaderKey] = useState(0); - const quater = [{ text: '1분기', val: '1' }, { text: '2분기', val: '2' }, { text: '3분기', val: '3' }, { text: '4분기', val: '4' }]; - // 현재 년도부터 10년 전까지의 년도 옵션 생성 - const yearOptions = Array.from({ length: 10 }, (_, i) => { - const year = new Date().getFullYear() - i; - return { text: `${year}년`, val: year.toString() }; - }); - const [selectedYear, setSelectedYear] = useState<string>(yearOptions[0]?.val || ''); - const [selectedQuarter, setSelectedQuarter] = useState<string>(quater[0]?.val || ''); + const quater = [{ text: '1분기', val: '1' }, { text: '2분기', val: '2' }, { text: '3분기', val: '3' }, { text: '4분기', val: '4' }]; + // 현재 년도부터 10년 전까지의 년도 옵션 생성 + const yearOptions = Array.from({ length: 10 }, (_, i) => { + const year = new Date().getFullYear() - i; + return { text: `${year}년`, val: year.toString() }; + }); + const [selectedYear, setSelectedYear] = useState<string>(yearOptions[0]?.val || ''); + const [selectedQuarter, setSelectedQuarter] = useState<string>(quater[0]?.val || ''); - const internalHandleUploadResponse = (file: File) => async (res: HttpResponse) => { - const fileInfo = res.getHeader("X-File-Info"); - if (!fileInfo) { - if (onFileSelect) onFileSelect(file, null, null); - return; - } - const { atflCd: newAtflCd, atflId: newAtflId } = JSON.parse(fileInfo) ?? {}; - const orgnlFileNm = file.name; - console.log(`FileUploadForm (idx: ${fileIdx ?? 'N/A'}) - orgnlFileNm:`, orgnlFileNm, 'New AtflCd:', newAtflCd, 'New AtflId:', newAtflId); - setFileName(orgnlFileNm); - setInternalAtflCd(newAtflCd); - setInternalAtflId(newAtflId); + const internalHandleUploadResponse = (file: File) => async (res: HttpResponse) => { + const fileInfo = res.getHeader("X-File-Info"); + if (!fileInfo) { + if (onFileSelect) onFileSelect(file, null, null); + return; + } + const { atflCd: newAtflCd, atflId: newAtflId } = JSON.parse(fileInfo) ?? {}; + const orgnlFileNm = file.name; + console.log(`FileUploadForm (idx: ${fileIdx ?? 'N/A'}) - orgnlFileNm:`, orgnlFileNm, 'New AtflCd:', newAtflCd, 'New AtflId:', newAtflId); + setFileName(orgnlFileNm); + setInternalAtflCd(newAtflCd); + setInternalAtflId(newAtflId); - if (onFileSelect) { - onFileSelect(file, newAtflCd, newAtflId); - } - }; + if (onFileSelect) { + onFileSelect(file, newAtflCd, newAtflId); + } + }; - return ( - <div> {/* 컴포넌트 루트 div */} - <div className="card p-6"> {/* 카드 스타일링 */} - <div className='flex items-center gap-2'> {/* FileUploader와 Button을 정렬하는 메인 flex 컨테이너 */} - <div className="w-[300px] mr-4"> {/* FileUploader의 너비와 오른쪽 여백을 제어하는 컨테이너 */} - <FileUploader - id={uniqueId} - className="!h-auto !max-w-none flex-row items-center gap-2" - accept={accept} - onUpload={(e) => { - const selectedFiles = e.target.files; - if (!selectedFiles || selectedFiles.length === 0) { - setFiles(null); - setCurrentFile(null); - setInternalAtflCd(null); - setInternalAtflId(null); - if (onFileSelect) { - onFileSelect(null, null, null); // 파일 선택 취소 시 null 전달 - } - return; - } + return ( + <div> {/* 컴포넌트 루트 div */} + <div className="card p-6"> {/* 카드 스타일링 */} + <div className='flex items-center gap-2'> {/* FileUploader와 Button을 정렬하는 메인 flex 컨테이너 */} + <div className="w-[300px] mr-4"> {/* FileUploader의 너비와 오른쪽 여백을 제어하는 컨테이너 */} + <FileUploader + key={uploaderKey} // key 추가: 변경 시 컴포넌트 재마운트됨 + id={uniqueId} + className="!h-auto !max-w-none flex-row items-center gap-2" + accept={accept} + onUpload={(e) => { + const selectedFiles = e.target.files; + if (!selectedFiles || selectedFiles.length === 0) { + setFiles(null); + setCurrentFile(null); + setInternalAtflCd(null); + setInternalAtflId(null); + if (onFileSelect) { + onFileSelect(null, null, null); // 파일 선택 취소 시 null 전달 + } + return; + } - const firstFile = selectedFiles[0]; - if (!firstFile) return; // 타입스크립트 undefined 가능성 제거 + const firstFile = selectedFiles[0]; + if (!firstFile) return; // 타입스크립트 undefined 가능성 제거 - setFiles(selectedFiles); - setCurrentFile(firstFile); + setFiles(selectedFiles); + setCurrentFile(firstFile); - if (onFileSelect) { - onFileSelect(firstFile, internalAtflCd, internalAtflId); - } + if (onFileSelect) { + onFileSelect(firstFile, internalAtflCd, internalAtflId); + } - handleUpload(selectedFiles, { - handleUploadStart: setUpload, - handleSuccess: setUpload, - // handleResponse는 tus-js-client의 Upload 인스턴스 콜백과 연동 필요 - // 여기서는 handleUpload 유틸리티가 응답을 직접 처리한다고 가정 - handleResponse: internalHandleUploadResponse(firstFile), - }); - }} - customPreview={ - showPreview ? ( - <div className="file-uploader-preview"> - {files && files.length > 0 - ? fileName - : "파일을 선택하세요."} - </div> - ) : null - } - /> - </div> - {showButton ? ( // showButton이 true일 때만 버튼 렌더링 - <Button - className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" - size="sm" - onClick={() => { - console.log(files?.[0]?.name) - if (!currentFile) { - alert('파일을 선택해주세요.'); - return; - } - if (!selectedYear) { - alert('등록년도를 선택해주세요.'); - return; - } - if (!selectedQuarter) { - alert('분기를 선택해주세요.'); - return; - } - const fileNm = currentFile.name; - if (onSubmit) { - // 부모에게 전달할 때는 내부 상태인 internalAtflCd, internalAtflId 사용 - onSubmit(fileNm, selectedYear, selectedQuarter, internalAtflCd || '', internalAtflId || ''); - setFiles(null); // 파일 업로드 후 파일 목록 초기화 - setFileName(''); // 파일명 초기화 - } - }} - > - 등록 - </Button> - ) : null} - </div> - </div> + handleUpload(selectedFiles, { + handleUploadStart: setUpload, + handleSuccess: setUpload, + // handleResponse는 tus-js-client의 Upload 인스턴스 콜백과 연동 필요 + // 여기서는 handleUpload 유틸리티가 응답을 직접 처리한다고 가정 + handleResponse: internalHandleUploadResponse(firstFile), + }); + }} + customPreview={ + showPreview ? ( + <div className="file-uploader-preview"> + {files && files.length > 0 + ? fileName + : "파일을 선택하세요."} + </div> + ) : null + } + /> + </div> + {showButton ? ( // showButton이 true일 때만 버튼 렌더링 + <Button + className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" + size="sm" + onClick={() => { + console.log(files?.[0]?.name) + if (!currentFile) { + alert('파일을 선택해주세요.'); + return; + } + if (!selectedYear) { + alert('등록년도를 선택해주세요.'); + return; + } + if (!selectedQuarter) { + alert('분기를 선택해주세요.'); + return; + } + const fileNm = currentFile.name; + if (onSubmit) { + // 부모에게 전달할 때는 내부 상태인 internalAtflCd, internalAtflId 사용 + onSubmit(fileNm, selectedYear, selectedQuarter, internalAtflCd || '', internalAtflId || ''); + setFiles(null); // 파일 업로드 후 파일 목록 초기화 + setFileName(''); // 파일명 초기화 + // 파일 업로드 후 FileUploader 컴포넌트를 재마운트하기 위해 key 값 변경 + setUploaderKey(prev => prev + 1); + } + }} + > + 등록 + </Button> + ) : null} </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 index 90bc8f735..384808aee 100644 --- a/apps/sn/app/components/ui/formSection.tsx +++ b/apps/sn/app/components/ui/formSection.tsx @@ -2,23 +2,23 @@ import { FormField } from '@common/business_components/ui' import React, { type ReactElement } from 'react' interface FormSectionProps { - children: ReactElement - titleNm: string - form: any - name: string + children: ReactElement + titleNm: string + form: any + name: string } const FormSection = ({ children, titleNm, form, name }: FormSectionProps) => { - return ( - <div className="grid grid-cols-[200px_1fr] items-center"> - <div className="text-xl font-medium">{titleNm}</div> - <FormField - control={form.control} - name={name} - render={({ field }) => children} - /> - </div> - ) + return ( + <div className="grid grid-cols-[200px_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 index f1dda48a6..afc2b5c9a 100644 --- a/apps/sn/app/components/ui/modal.tsx +++ b/apps/sn/app/components/ui/modal.tsx @@ -1,35 +1,34 @@ -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose, DialogOverlay } from '@common/components/ui' +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogOverlay } from '@common/components/ui' import React from 'react' interface ModalProps { - isModalOpen: boolean; - setIsModalOpen: (open: boolean) => void; - title?: string; - children: React.ReactNode; - closeModal: () => void; - saveModal?: () => void + isModalOpen: boolean; + setIsModalOpen: (open: boolean) => void; + title?: string; + children: React.ReactNode; + closeModal: () => void; + saveModal?: () => void; + description?: string; } -const Modal: React.FC<ModalProps> = ({ isModalOpen, setIsModalOpen, title, children, closeModal,saveModal }) => { - return ( - <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}> - <DialogOverlay className='z-[200]' /> - <DialogContent className="w-auto max-w-[1200px] min-w-[400px] min-h-[150px] h-auto max-h-[450px] overflow-auto z-[1000]"> - {title ? <DialogHeader> - <DialogTitle>{title}</DialogTitle> - </DialogHeader> : null} - {children} - <DialogFooter> - <DialogClose> - <div className='flex justify-end gap-2'> - {saveModal ? <button className="px-4 py-2 bg-primary text-white rounded" onClick={saveModal}>저장</button> : null} - <button className="px-4 py-2 bg-gray-600 text-white rounded" onClick={closeModal}>닫기</button> - </div> - </DialogClose> - </DialogFooter> - </DialogContent> - </Dialog> - ) +const Modal: React.FC<ModalProps> = ({ isModalOpen, setIsModalOpen, title, children, closeModal, saveModal }) => { + return ( + <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}> + <DialogOverlay className='z-[200]' /> + <DialogContent className="w-auto max-w-[1200px] min-w-[400px] min-h-[150px] h-auto max-h-[450px] overflow-auto z-[1000]" aria-describedby={undefined}> + {title ? <DialogHeader> + <DialogTitle>{title}</DialogTitle> + </DialogHeader> : null} + {children} + <DialogFooter> + <div className='flex justify-end gap-2'> + {saveModal ? <button className="px-4 py-2 bg-primary text-white rounded" onClick={saveModal}>저장</button> : null} + <button className="px-4 py-2 bg-gray-600 text-white rounded" onClick={closeModal}>닫기</button> + </div> + </DialogFooter> + </DialogContent> + </Dialog> + ) } export default Modal \ No newline at end of file diff --git a/apps/sn/app/components/ui/operFileUploadForm.tsx b/apps/sn/app/components/ui/operFileUploadForm.tsx index bd1c67996..3c37673d2 100644 --- a/apps/sn/app/components/ui/operFileUploadForm.tsx +++ b/apps/sn/app/components/ui/operFileUploadForm.tsx @@ -1,51 +1,67 @@ "use client" import { SelectBox } from '@common/business_components/ui'; -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import FileUploadForm from './fileUploadForm'; -import { yearOptions } from '../lib/utils'; +import { yearOptions, getQuarterOptions } from '@/components/lib/utils'; -const operFileUploadForm = ({ title, onSubmit }: { title: string, onSubmit: (files: string, year: string, quarter: string, code:string, id:string) => void }) => { - const [atflCd, setAtflCd] = useState<string | null>(null); - const [atflId, setAtflId] = useState<string | null>(null); +const operFileUploadForm = ({ title, onSubmit }: { title: string, onSubmit: (files: string, year: string, quarter: string, code: string, id: string) => void }) => { + const [atflCd, setAtflCd] = useState<string | null>(null); + const [atflId, setAtflId] = useState<string | null>(null); + const [selectedYear, setSelectedYear] = useState<string>(yearOptions[0]?.val || ''); + const [quaterOptions, setQuaterOptions] = useState(() => getQuarterOptions(selectedYear)); + const [selectedQuarter, setSelectedQuarter] = useState<string>(quaterOptions[0]?.val || ''); - const quater = [{ text: '1분기', val: '1' }, { text: '2분기', val: '2' }, { text: '3분기', val: '3' }, { text: '4분기', val: '4' }] - const [selectedYear, setSelectedYear] = useState<string>(yearOptions[0]?.val || ''); - const [selectedQuarter, setSelectedQuarter] = useState<string>(quater[0]?.val || ''); + // 년도가 변경될 때 분기 옵션 업데이트 + useEffect(() => { + const updatedQuarterOptions = getQuarterOptions(selectedYear); + setQuaterOptions(updatedQuarterOptions); + // 분기 옵션이 변경되면 첫 번째 옵션으로 자동 선택 + setSelectedQuarter(updatedQuarterOptions[0]?.val || ''); + }, [selectedYear]); - return ( - <div> - <div className='card-header'> - <h3 className="text-2xl font-medium">{title}</h3> - </div> - <div className="card p-6"> - <div className="grid grid-col-2 items-center space-x-4"> - <div className='flex'> - <SelectBox - className="w-50 mr-3 ml-4" - selectClass="rounded-lg bg-white" - label="년도" - selectArray={yearOptions} - onSelectValue={(value: string | number) => setSelectedYear(String(value))} - /><SelectBox - className="w-50" - selectClass="rounded-lg bg-white" - label="분기" - selectArray={quater} - onSelectValue={(value: string | number) => setSelectedQuarter(String(value))} - /> - <FileUploadForm - onSubmit={(files, year, quarter, code, id) => { - onSubmit(files, (selectedYear ?? year) || '', (selectedQuarter ?? quarter) || '', code, id); - }} - showButton={true} - showPreview={true} - accept={""} /> - </div> - </div> - </div> + // 년도 변경 핸들러 + const handleYearChange = (value: string | number) => { + const yearValue = String(value); + setSelectedYear(yearValue); + }; + + 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={handleYearChange} + value={selectedYear} + /> + <SelectBox + className="w-50" + selectClass="rounded-lg bg-white" + label="분기" + selectArray={quaterOptions} + onSelectValue={(value: string | number) => setSelectedQuarter(String(value))} + value={selectedQuarter} + /> + <FileUploadForm + onSubmit={(files, year, quarter, code, id) => { + onSubmit(files, (selectedYear ?? year) || '', (selectedQuarter ?? quarter) || '', code, id); + }} + showButton={true} + showPreview={true} + accept={""} /> + </div> </div> - ) + </div> + </div> + ) } export default operFileUploadForm \ No newline at end of file diff --git a/apps/sn/app/components/ui/verticalTable.tsx b/apps/sn/app/components/ui/verticalTable.tsx index e5e506796..ee13405fb 100644 --- a/apps/sn/app/components/ui/verticalTable.tsx +++ b/apps/sn/app/components/ui/verticalTable.tsx @@ -1,206 +1,221 @@ import UiPageNations from "@common/business_components/ui/table/uiPageNations"; -import { Table, TableBody, TableCell, TableRow } from "@common/components"; +import { Table, TableBody, TableCell, TableRow, Button } from "@common/components"; import { useEffect, useState } from "react"; type UiTableProps = { - headName: React.ReactNode | undefined; - tableName1?: React.ReactNode | undefined; - tableData: { [key: string]: string | number | boolean | object }[]; - headlist: UiHeadProps[]; - pageSize: number; - total: number; - children: Record<string, React.ReactNode> | undefined; - cellClick: ((index: number | undefined, headId: string) => void) | undefined; - onPageChange: (page: number) => void; - className?: string; - resetPageNo?: boolean; - isEditable?: boolean; - tableName2?: React.ReactNode | undefined; - tableData2?: { [key: string]: string | number | boolean | object }[]; - headlist2?: UiHeadProps[]; - pageSize2?: number; - total2?: number; - children2?: Record<string, React.ReactNode> | undefined; - cellClick2?: ((index: number | undefined, headId: string) => void) | undefined; - onPageChange2?: (page: number) => void; - className2?: string; - resetPageNo2?: boolean; - isEditable2?: boolean; - showSecondTable?: boolean; + headName: React.ReactNode | undefined; + tableName1?: React.ReactNode | undefined; + tableData: { [key: string]: string | number | boolean | object }[]; + headlist: UiHeadProps[]; + pageSize: number; + total: number; + children?: Record<string, React.ReactNode> | undefined; + cellClick: ((index: number | undefined, headId: string) => void) | undefined; + onPageChange: (page: number) => void; + className?: string; + resetPageNo?: boolean; + isEditable?: boolean; + tableName2?: React.ReactNode | undefined; + tableData2?: { [key: string]: string | number | boolean | object }[]; + headlist2?: UiHeadProps[]; + pageSize2?: number; + total2?: number; + children2?: Record<string, React.ReactNode> | undefined; + cellClick2?: ((index: number | undefined, headId: string) => void) | undefined; + onPageChange2?: (page: number) => void; + className2?: string; + resetPageNo2?: boolean; + isEditable2?: boolean; + showSecondTable?: boolean; + showSaveButton?: boolean; + onSaveClick?: () => void; + onFormDataChange?: (formData: { [key: string]: string | number | boolean }) => void; // 추가 }; type UiHeadProps = { - id: string; - title: string; - align?: string; - type?: string; - options?: string[]; + id: string; + title: string; + align?: string; + type?: string; + options?: string[]; }; export function VerticalTable({ - headName, - tableName1, - tableData, - headlist, - pageSize, - total, - children, - cellClick, - onPageChange, - className = "", - resetPageNo = false, - isEditable = false, - tableName2, - tableData2, - headlist2, - pageSize2, - total2, - children2, - cellClick2, - onPageChange2, - className2 = "", - resetPageNo2 = false, - isEditable2 = false, - showSecondTable = false, + headName, + tableName1, + tableData, + headlist, + pageSize, + total, + children, + onPageChange, + className = "", + resetPageNo = false, + isEditable = false, + tableName2, + headlist2, + isEditable2 = false, + showSecondTable = false, + showSaveButton = false, + onSaveClick, + onFormDataChange, }: 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]); + 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]); + useEffect(() => { + if (tableData.length > 0 && tableData[0]) { + // 깊은 복사를 통해 새로운 객체 생성 + const newData = JSON.parse(JSON.stringify(tableData[0])); + setFormData(newData); + console.log("Form data initialized:", newData); // 디버깅용 + } + }, [tableData]); - const handleInputChange = (id: string, value: string | number | boolean) => { - setFormData((prev) => ({ - ...prev, - [id]: value, - })); + const handleInputChange = (id: string, value: string | number | boolean) => { + console.log(`Input change: ${id} = ${value}`); // 디버깅용 + const newFormData = { + ...formData, + [id]: value, }; + console.log("New form data:", newFormData); // 디버깅용 + setFormData(newFormData); - const handlePageChange = (page: number) => { - setCur(page); - onPageChange(page); - }; + // 폼 데이터가 변경될 때마다 부모에게 알림 + if (onFormDataChange) { + onFormDataChange(newFormData); + } + }; + 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}`}> - {tableName1 && ( // tableName prop이 있으면 표시 - <div className="text-xl font-semibold my-2 mx-3">{tableName1}</div> - )} - <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"> - {children && children[head.id] ? children[head.id] : isEditable ? ( - head.type === "select" ? ( - <select - id={head.id} - 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 - id={head.id} - 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> + return ( + <> + {headName ? ( + <div className="card-header flex items-center justify-between"> + <h3 className="text-2xl mx-3 font-medium text-label">{headName}</h3> + {showSaveButton && onSaveClick && ( + <Button + onClick={onSaveClick} + className="px-4 mr-3 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" + > + 저장 + </Button> + )} + </div> + ) : null} + <div className={`card !m-[1.2rem] !p-0r ${className}`}> + {tableName1 && ( // tableName prop이 있으면 표시 + <div className="text-xl font-semibold my-2 mx-3">{tableName1}</div> + )} + <Table variant="secondary"> + <TableBody className="text-center"> + {headlist?.map((head) => ( + <TableRow key={head.id}> + {/* 컬럼 제목 */} + <TableCell className={`font-bold bg-blue-100 text-xs w-[120px] whitespace-nowrap`}> + {head.title} + </TableCell> + {/* 데이터 값 또는 입력 필드 */} + <TableCell className="text-xs"> + {children && children[head.id] ? children[head.id] : isEditable ? ( + head.type === "select" ? ( + <select + id={head.id} + value={String(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> ))} - </TableBody> - </Table> - {tableName2 && ( // tableName prop이 있으면 표시 - <div className="text-xl font-semibold mt-8 my-2 mx-3">{tableName2}</div> - )} - {showSecondTable && ( - <Table variant="secondary"> - <TableBody className="text-center"> - {headlist2?.map((head) => ( - <TableRow key={head.id}> - {/* 컬럼 제목 */} - <TableCell className="font-bold bg-blue-100 text-xs w-fit whitespace-nowrap"> - {head.title} - </TableCell> - {/* 데이터 값 또는 입력 필드 */} - <TableCell className="text-xs"> - {children && children[head.id] ? children[head.id] : isEditable2 ? ( - head.type === "select" ? ( - <select - id={head.id} - 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 - id={head.id} - 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> - </> - ); + </select> + ) : head.type === "input" ? ( + <input + id={head.id} + type="text" + value={String(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> + {tableName2 && ( // tableName prop이 있으면 표시 + <div className="text-xl font-semibold mt-8 my-2 mx-3">{tableName2}</div> + )} + {showSecondTable && ( + <Table variant="secondary"> + <TableBody className="text-center"> + {headlist2?.map((head) => ( + <TableRow key={head.id}> + {/* 컬럼 제목 */} + <TableCell className={`font-bold bg-blue-100 text-xs w-[120px] whitespace-nowrap`}> + {head.title} + </TableCell> + {/* 데이터 값 또는 입력 필드 */} + <TableCell className="text-xs"> + {children && children[head.id] ? children[head.id] : isEditable2 ? ( + head.type === "select" ? ( + <select + id={head.id} + value={formData[head.id] !== undefined ? String(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 + id={head.id} + type="text" + value={formData[head.id] !== undefined ? String(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 deleted file mode 100644 index 544802744..000000000 --- a/apps/sn/app/ii/BaseLayout.tsx +++ /dev/null @@ -1,276 +0,0 @@ -'use client' -import { getData } from '@/components/lib/api' -import ContentLayout from '@/components/ui/contentLayout' -import { GovSidoSigunguSearchForm } from '@/components/ui/govSidoSigunguSearchForm' -import { DatePicker, Form, SearchFormRight, SelectBox } from '@common/business_components/ui' -import ListDiv from '@common/business_components/ui/list_div' -import { SelectBox2 } from '@common/business_components/ui/select/index-2' -import { Button, Label } from '@common/components/ui' -import { useEffect, useState, type ReactNode } from 'react' - -type TitleName = { - mainTitle: string - firstCardTitle: string - secondCardTitle?: string -} -type SelectBox = { - labelNm?: Label - searchChk?: boolean - title?: string; - onSearchClick?: () => Promise<void>; -} -type Label = { - first: string - second: string - third: string - fourth: string -} -// API로부터 받아올 데이터 타입을 정의 (SelectBox에 사용될 형태) -interface ApiSelectOption { - text: string; - val: string; -} -type BaseLayout = { - children?: ReactNode[] - titleName: TitleName - form: any - onSubmitEvent: Function - graphBtn?: boolean - analysisBtn?: string - firstDivBtn?: boolean - selectBox?: SelectBox - showDatePicker?: boolean; - enoCd?: string | number - setEnoCd?: (enoCd: string) => void - ctpvCd?: string | number - setCtpvCd?: (ctpvCd: string) => void - sigungu?: ApiSelectOption - setSigungu?: (sigungu: ApiSelectOption) => void - selectedSwageTreatmentPlant?: string | number; - onSwageTreatmentPlantChange?: (value: string | number) => void; - selectedSwageProcessingZone?: string | number; - onSwageProcessingZoneChange?: (value: string | number) => void; - aplcnYm?: Date - setAplcnYm?: (aplcnYm: Date | undefined) => void; - isLoading?: boolean; - // Excel 다운로드 관련 props 추가 - onExcelDownload?: () => void; - onDeleteFile?: () => void; - onAnalysis?: () => void; - selectedRowData?: any; -} -// TODO: 나중에 이벤트 추가되면 파라미터 추가 필요 ? -/** - * - * @param children - * @param titleName : 메인 타이틀, 첫번째 요소 타이틀, 두번째 요소 타이틀 - * @param form : zod 스키마 - * @param onSubmitEvent : 폼 이벤트 처리 - * @param graphBtn : 하단에 그래프 버튼 유무 처리 - * @returns - */ -const BaseLayout = ({ - children, - titleName, - form, - onSubmitEvent, - graphBtn = false, - analysisBtn, - selectBox, - showDatePicker = false, - enoCd, - setEnoCd, - ctpvCd, - setCtpvCd, - sigungu, - setSigungu, - selectedSwageTreatmentPlant, - onSwageTreatmentPlantChange, - selectedSwageProcessingZone, - onSwageProcessingZoneChange, - aplcnYm, - setAplcnYm, - isLoading, - // Excel 다운로드 관련 props 추가 - onExcelDownload, - onDeleteFile, - onAnalysis, - selectedRowData -}: BaseLayout) => { - - const defaultOption: ApiSelectOption = { val: '0', text: '선택' }; - - const [processingZoneOptions, setProcessingZoneOptions] = useState<ApiSelectOption[]>([]); - const [treatmentFacilityOptions, setTreatmentFacilityOptions] = useState<ApiSelectOption[]>([]); - const [isLoadingAreaData, setIsLoadingAreaData] = useState<boolean>(false); - const [atflId, setAtflId] = useState('') - const [fileNm, setFileNm] = useState('') - const [dcrpPplMtncBizSn, setDcrpPplMtncBizSn] = useState('') - - useEffect(() => { - const fetchData = async () => { - if (!sigungu || sigungu.val === '0' || !sigungu.val) { - console.log("BaseLayout: Skipping fetchData because sigungu.val is:", sigungu?.val); - setProcessingZoneOptions([defaultOption]); - setTreatmentFacilityOptions([defaultOption]); - setIsLoadingAreaData(false); - return; - } - - setIsLoadingAreaData(true); - try { - const queryParamsZone = new URLSearchParams({ - lyrTypeNm: 'tb_ms_swage_prcs_zone', - page: '0', - size: '100000', - sort: 'string', - }); - const cqlFiltersZone: string[] = []; - if (sigungu.val && sigungu.val !== '0') { - cqlFiltersZone.push(`sgg_cd='${sigungu.val}'`); - } - if (cqlFiltersZone.length > 0) { - queryParamsZone.append('cqlFiter', cqlFiltersZone.join(' and ')); - } - - const queryParamsFacility = new URLSearchParams({ - lyrTypeNm: 'tb_il_swage_prcs_fclt', - page: '0', - size: '100000', // 요청하신 URL에 맞게 50으로 변경 가능 - sort: 'string', - }); - const cqlFiltersFacility: string[] = []; - if (sigungu.text && sigungu.val !== '0') { - cqlFiltersFacility.push(`sgg_nm ILIKE '%${sigungu.text}%'`); - } - if (cqlFiltersFacility.length > 0) { - queryParamsFacility.append('cqlFiter', cqlFiltersFacility.join(' and ')); // API 명세에 따라 cqlFilter 또는 cqlFiter - } - - console.log("BaseLayout API URL for Zones:", `v1/fclts?${queryParamsZone.toString()}`); - console.log("BaseLayout API URL for Facilities:", `v1/fclts?${queryParamsFacility.toString()}`); - - const [zoneRes, facilityRes] = await Promise.all([ - getData(`v1/fclts?${queryParamsZone.toString()}`), - getData(`v1/fclts?${queryParamsFacility.toString()}`) - ]); - - const zones = (zoneRes.data || []).map((item: any) => ({ - text: item.swtrmtZoneNm || item.name || `구역 ${item.id || item.gid || item.objectid}`, - val: item.fid || item.id || item.gid || item.objectid - })); - setProcessingZoneOptions([defaultOption, ...zones]); - console.log("BaseLayout Fetched Processing Zones:", [defaultOption, ...zones]); - - - const facilities = (facilityRes.data || []).map((item: any) => ({ - text: item.swtrmtFcltNm || item.name || `시설 ${item.id || item.gid || item.objectid}`, - val: item.fid || item.id || item.gid || item.objectid - })); - setTreatmentFacilityOptions([defaultOption, ...facilities]); - console.log("BaseLayout Fetched Treatment Facilities:", [defaultOption, ...facilities]); - - } catch (error) { - console.error("BaseLayout: 지역/시설 데이터 조회 중 오류 발생:", error); - setProcessingZoneOptions([defaultOption]); - setTreatmentFacilityOptions([defaultOption]); - } finally { - setIsLoadingAreaData(false); - } - }; - - fetchData(); - }, [sigungu]); // sigungu 상태가 변경될 때마다 실행 - - // 파일 다운로드 - const isChckFile = () => { - // Excel 다운로드 함수가 있으면 Excel 다운로드 실행 - if (onExcelDownload) { - onExcelDownload(); - return; - } - return true - } - // 파일 삭제 - const isDeleteFile = async () => { - // Excel 다운로드 함수가 있으면 Excel 다운로드 실행 - if (onDeleteFile) { - onDeleteFile(); - return; - } - } - - return ( - <Form {...form} className="min-h-screen flex flex-col"> - <ContentLayout title={titleName.mainTitle} className="flex-grow flex flex-col overflow-hidden" isLoading={isLoading}> - <ListDiv> - {/* searchBox.title이 있으면 h3 태그로 제목을 표시합니다. */} - {selectBox?.title && ( - <div className="card-header"> - <h3 className="text-2xl font-medium">{selectBox.title}</h3> - </div> - )} - <div className="flex items-center w-full"> - {showDatePicker && ( // showDatePicker 값에 따라 DatePicker 렌더링 - <> - <Label className="ml-4 w-20 text-xl text-gray-600 whitespace-nowrap">분석기간</Label> - <DatePicker - mode={"single"} - field={{ - value: aplcnYm ?? new Date(), - onChange: setAplcnYm ?? ((date: Date | undefined) => console.log("DatePicker onChange:", date)) - }} - selectClass="ml-5 date-picker-trigger" /> - </> - )} - <GovSidoSigunguSearchForm - isShowPart={true} - setRtnEnvPartValue={(val) => setEnoCd?.(val)} - setRtnEnvPartSidoValue={(val) => setCtpvCd?.(val)} - setRtnEnvPartSigunValue={setSigungu} // 선택된 시군구 값을 받을 콜백 함수 전달 - > - <SelectBox2 label={"하수처리장"} value={selectedSwageTreatmentPlant} selectArray={treatmentFacilityOptions} className="col-span-3" selectClass="rounded-lg bg-white truncate" onSelectValue={(val) => onSwageTreatmentPlantChange?.(val)} /> - <SelectBox2 label={"하수처리구역"} value={selectedSwageProcessingZone} selectArray={processingZoneOptions} className="col-span-3" selectClass="rounded-lg bg-white truncate" onSelectValue={(val) => onSwageProcessingZoneChange?.(val)} /> - </GovSidoSigunguSearchForm> - {selectBox?.searchChk ? <SearchFormRight><Button size="sm" onClick={() => selectBox.onSearchClick?.()}>조회</Button></SearchFormRight> : null} - </div> - </ListDiv> - <ListDiv> - <div className="card-header"> - <h3 className="text-2xl font-medium">{titleName.firstCardTitle}</h3> - <div> - {graphBtn ? <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" onClick={isChckFile}>파일 다운로드</Button> : null} - {/* 그래프 있을 시 */} - {graphBtn ? <Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size="sm" onClick={isDeleteFile}>삭제</Button> : null} - {/* 분석 버튼 */} - {analysisBtn ? <Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size="sm" onClick={onAnalysis}>{analysisBtn}</Button> : null} - </div> - </div> - <Form {...form}> - <form onSubmit={form.handleSubmit(onSubmitEvent)} className="flex-grow flex flex-col"> - <div className="card !m-[1.2rem] flex-grow flex flex-col"> - {/* 폼 정보 입력 칸 */} - {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> - {graphBtn ? <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> : null} - {/* 그래프 있을 시 */} - {graphBtn ? <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> : null} - </div> - </div> - <div className="card !m-[1.2rem]">{children?.[1]}</div> - </ListDiv> : null - } - </ContentLayout > - </Form> - ) -} - -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 deleted file mode 100644 index 2944af9c0..000000000 --- a/apps/sn/app/ii/irsetting/page.tsx +++ /dev/null @@ -1,380 +0,0 @@ -'use client' -import { postData } from '@/components/lib/api' -import FileUploadForm from '@/components/ui/fileUploadForm' -import FormSection from '@/components/ui/formSection' -import { DatePicker, filePicked } from '@common/business_components/ui' -import ListDiv from '@common/business_components/ui/list_div' -import { Input } from '@common/components/ui' -import { zodResolver } from '@hookform/resolvers/zod' -import { useState } from 'react' -import { useForm } from 'react-hook-form' -import { z } from 'zod' -import { excelToFlowRateData, excelToMatchingResultData, excelToTimePatternData } from '../../components/lib/utils/excelToJson' -import BaseLayout from '../BaseLayout' - -const formSchema = z.object({ - managenm: z.string(), - region: z.string(), - suyeonga: z.number(), - inputdate: z.coerce.date(), - rtkSource: z.string(), - suyeongaPlace: z.string(), - pattern: z.string() -}) - -// 각 파일 업로드 폼의 데이터를 저장할 인터페이스 정의 -interface UploadedFileInfo { - file: File | null; - atflCd: string | null; - atflId: string | null; - // 필요하다면 파일 이름 등 추가 정보 저장 가능 - // fileName: string; -} -// 초기 파일 정보 상태 -const initialFileInfo: UploadedFileInfo = { file: null, atflCd: null, atflId: null }; - -interface ApiSelectOption { - text: string; - val: string; -} - -const IrSetting = () => { - const form = useForm<z.infer<typeof formSchema>>({ - resolver: zodResolver(formSchema), - defaultValues: { - managenm: "", - region: "", - suyeonga: 0, - inputdate: new Date(), - rtkSource: "", - suyeongaPlace: "", - pattern: "" - } - }) - - const [uploadedFilesData, setUploadedFilesData] = useState<UploadedFileInfo[]>([ - initialFileInfo, // 0: 오수관망도 - initialFileInfo, // 1: 수용가-맨홀 매칭결과 - initialFileInfo, // 2: 오수량 시간패턴 - initialFileInfo // 3: 유량자료 등록 - ]); - - const [selectedDate, setSelectedDate] = useState(new Date()); - const [input1, setInput1] = useState(''); - const [input2, setInput2] = useState(''); - const [select1, setSelect1] = useState('0'); // 환경청코드 - const [select2, setSelect2] = useState('0'); // 시도코드 - const [select3, setSelect3] = useState<ApiSelectOption>({ val: '0', text: '전체' }); // 시군구 상태를 객체로 변경 - const [select4, setSelect4] = useState<string | number>('0'); // 하수처리장코드 - const [select5, setSelect5] = useState<string | number>('0'); // 하수처리구역코드 - const [formResetKey, setFormResetKey] = useState(0); // FileUploadForm 리셋을 위한 key 상태 - const [isLoadingResult, setIsLoadingResult] = useState<boolean>(false); // 로딩 상태 - - // 폼 초기화 함수 - const resetAllForms = () => { - form.reset({ // react-hook-form 초기화 - managenm: "", - region: "", - suyeonga: 0, - inputdate: new Date(), - rtkSource: "", - suyeongaPlace: "", - pattern: "" - }); - setUploadedFilesData([ // 파일 업로드 상태 초기화 - initialFileInfo, - initialFileInfo, - initialFileInfo, - initialFileInfo - ]); - setSelectedDate(new Date()); // 날짜 선택 초기화 - setInput1(''); // 입력 필드 초기화 - setInput2(''); // 입력 필드 초기화 - setSelect1('0'); // 선택 필드 초기화 - setSelect2('0'); - setSelect3({ val: '0', text: '전체' }); - setSelect4(0); - setSelect5(0); - setFormResetKey(prevKey => prevKey + 1); // FileUploadForm 재마운트를 위해 key 변경 - }; - - // FileUploadForm의 onFileSelect prop으로 전달될 콜백 함수 - const handleFileSelection = ( - index: number, // FileUploadForm의 fileIdx 값 - selectedFile: File | null, - cd: string | null, - id: string | null - ) => { - setUploadedFilesData(prevData => { - const newData = [...prevData]; - newData[index] = { - file: selectedFile, - atflCd: cd, - atflId: id, - }; - return newData; - }); - console.log(`Page: File data received for form index ${index}`, { fileName: selectedFile?.name, cd, id }); - }; - - - const registFile = async (analysisData: any) => { - setIsLoadingResult(true); // 로딩 상태 시작 - console.log("Submitting combined analysis data to backend:", analysisData); - // 추후 API 엔드포인트에 맞게 URL 수정 필요 - const data = await postData('v1/inftwtAnls', analysisData); - console.log("Response from backend:", data); - if (data) { - alert("분석 데이터가 성공적으로 제출되었습니다."); - resetAllForms(); // 성공 시 폼 초기화 함수 호출 - } else alert("분석 데이터 제출 중 오류가 발생했습니다."); - setIsLoadingResult(false); // 로딩 상태 시작 - }; - - // 침입수 분석 실행 버튼 클릭 핸들러 - const handleAnalysisClick = async () => { - setIsLoadingResult(true); // 로딩 상태 시작 - console.log(uploadedFilesData); - const file1 = uploadedFilesData[0]; // 오수관망도 - const file2 = uploadedFilesData[1]; // 수용가-맨홀 매칭결과 - const file3 = uploadedFilesData[2]; // 오수량 시간패턴 - const file4 = uploadedFilesData[3]; // 유량자료 - - console.log("오수관망도 정보:", file1?.file); - console.log("수용가-맨홀 매칭결과 정보:", file2); - console.log("오수량 시간패턴 정보:", file3); - console.log("유량자료 정보:", file4); - - // 모든 필수 파일이 선택되었는지 확인 (UploadedFileInfo 객체와 그 안의 file 객체 모두 확인) - // 유량자료는 선택값 - if (!file1?.file || !file2?.file || !file3?.file) { - setIsLoadingResult(false); - alert('모든 분석용 파일을 선택해주세요.'); - return; - } - - type MatchingResult = { anlsSn: number; mhlMngNo: string; rcptrNo: string; }; - type TimePattern = { hrId: number; rt?: number | null | undefined; }; - type FlowRate = { pplMngNo: string; mtrngDt: string; oilq?: number | null | undefined; }; - - const filePickers: { - file: UploadedFileInfo; - parser: (excelData: any) => MatchingResult[] | TimePattern[] | FlowRate[]; - }[] = [ - { file: file2, parser: excelToMatchingResultData }, - { file: file3, parser: excelToTimePatternData } - ]; - - if (file4?.file) { - filePickers.push({ file: file4, parser: excelToFlowRateData }); - } - - try { - const parsedResults = await Promise.all( - filePickers.map(async ({ file, parser }) => { - if (!file || !file.file) return null; - - const jsonData = await filePicked(file.file, 0); - - if (jsonData === undefined) { - console.warn(`Warning: filePicked returned undefined for ${file.file.name}`); - return { - fileName: file.file.name, - atflCd: file.atflCd, - atflId: file.atflId, - parsedData: null - }; - } - - // jsonData를 각 parser 함수로 처리 - const parsedDataBySpecificFunction = parser(jsonData); - - return { - fileName: file.file.name, - atflCd: file.atflCd, - atflId: file.atflId, - parsedData: parsedDataBySpecificFunction - }; - }) - ); - - // 각 파싱 결과 접근 - const matchingResultData = parsedResults.find(r => r && r.fileName === file2.file?.name)?.parsedData; - const timePatternData = parsedResults.find(r => r && r.fileName === file3.file?.name)?.parsedData; - const flowRateData = file4?.file - ? parsedResults.find(r => r && r.fileName === file4.file?.name)?.parsedData - : null; - - // 새로운 JSON 데이터 생성 - const combinedAnalysisData = { - enoCd: select1.toString(), // 환경청코드 - ctpvCd: select2.toString(), // 시도코드 - sggCd: select3.val.toString(), // 시군구코드 - swageTmplntCd: select4.toString(), // 하수처리장코드 - swtrmtZoneCd: select5.toString(), // 하수처리구역코드 - aplcnYm: new Date(selectedDate).toISOString().substring(0, 7).replace("-", ""), // 적용연월 - drtwSwtcRt: parseFloat(input1) || null, // 오수전환율 (숫자로 변환) - anlsInfo: input2, // 분석정보 - atflCd: file1?.atflCd, // 첨부파일CD (오수관망도) - atflId: file1?.atflId, // 첨부파일ID (오수관망도) - atflInpNm: file1?.file?.name, // 오수관망도 파일명 - atflHrPttnNm: file3?.file?.name, // 오수량 시간패턴 파일명 - atflMpngNm: file2?.file?.name, // 수용가-맨홀 매칭결과 파일명 - atflOilqNm: file4?.file?.name || null, // 유량자료 파일명 (없을 수 있음) - tbSnRcptrMhlMpng: matchingResultData, // 수용가-맨홀 매칭결과 - tbSnHrPttn: timePatternData, // 오수량 시간패턴 - tbSnOilqData: flowRateData, // 유량자료 (없을 수 있음) - }; - - console.log("통합 분석 데이터 (JSON):", combinedAnalysisData); - - // 수정된 registFile 함수를 호출하여 combinedAnalysisData를 전송합니다. - await registFile(combinedAnalysisData); - setIsLoadingResult(false); - } catch (error) { - setIsLoadingResult(false); - console.error("파일 파싱 중 오류 발생:", error); - alert("파일 처리 중 오류가 발생했습니다."); - return; - } - } - - return ( - <> - <BaseLayout - titleName={{ mainTitle: '침입수 분석', firstCardTitle: '침입수 분석 자료 입력' }} - form={form} - onSubmitEvent={() => { }} - graphBtn={false} - analysisBtn={'침입수분석 실행'} - selectBox={{ title: '분석 대상지역 선택' }} - enoCd={select1} - setEnoCd={setSelect1} - ctpvCd={select2} - setCtpvCd={setSelect2} - sigungu={select3} - setSigungu={setSelect3} - selectedSwageTreatmentPlant={select4} - onSwageTreatmentPlantChange={setSelect4} - selectedSwageProcessingZone={select5} - onSwageProcessingZoneChange={setSelect5} - isLoading={isLoadingResult} - onAnalysis={handleAnalysisClick} - > - {[<div className="grid gap-4"> - <FormSection name='suyeongaPlace' form={form} titleNm='▷ 오수관망도'> - <FileUploadForm - key={`file-upload-0-${formResetKey}`} // 폼 초기화를 위한 key - onFileSelect={(file, cd, id) => handleFileSelection(0, file, cd, id)} - showButton={false} - showPreview={true} - accept={""} - fileIdx={0} /> - </FormSection> - <FormSection name='suyeonga' form={form} titleNm='▷ 수용가-맨홀 매칭결과'> - <FileUploadForm - key={`file-upload-1-${formResetKey}`} // 폼 초기화를 위한 key - onFileSelect={(file, cd, id) => handleFileSelection(1, file, cd, id)} - showButton={false} - showPreview={true} - accept={".xls, .xlsx, .xlsm, .xltx"} - fileIdx={1} /> - </FormSection> - <FormSection name='pattern' form={form} titleNm='▷ 오수량 시간패턴'> - <FileUploadForm - key={`file-upload-2-${formResetKey}`} // 폼 초기화를 위한 key - onFileSelect={(file, cd, id) => handleFileSelection(2, file, cd, id)} - showButton={false} - showPreview={true} - accept={".xls, .xlsx, .xlsm, .xltx"} - fileIdx={2} /> - </FormSection> - <FormSection - name='inputdate' - form={form} - titleNm='▷ 상수사용량 적용기간'> - <DatePicker - mode={"single"} - field={{ - value: selectedDate, // 현재 페이지의 selectedDate 상태 사용 - onChange: (date: Date | undefined) => { - if (date) { - setSelectedDate(date); - form.setValue('inputdate', date, { shouldValidate: true }); - } - } - }} - selectClass="ml-5 date-picker-trigger" // date-picker.css의 스타일 적용 - /> - </FormSection> - <FormSection - name='rtkSource' - form={form} - titleNm='▷ 오수전환율'> - <Input - value={input1} - onChange={(e) => { - const newValue = e.target.value; - - if (newValue === "") { - setInput1(newValue); - return; - } - - // 숫자와 '.'만 허용하고, '.'은 최대 하나인지 확인 - const isValidChars = /^[0-9]*\.?[0-9]*$/.test(newValue); - if (!isValidChars) return; // 유효하지 않은 문자 또는 여러 개의 '.' 포함 - - // '.' 기호가 포함된 경우, 처음이나 끝에 오면 안 됨 - if (newValue.includes('.') && newValue.startsWith('.')) return; // '.'이 처음에는 올 수 없음 - - setInput1(newValue); - }} - className="ml-5 h-[3.2rem] text-center text-xl w-[150px]" - placeholder="예: 0.75" /> - </FormSection> - <FormSection - name='analysis information' - form={form} - titleNm='▷ 분석 정보'> - <Input - value={input2} - onChange={(e) => setInput2(e.target.value)} - className="ml-5 h-[3.2rem] text-center text-xl w-[150px]" /> - </FormSection> - <ListDiv> - <div className="card !m-[1.2rem]"> - <FormSection name='region' form={form} titleNm='▷ 유량자료 등록'> - <FileUploadForm - key={`file-upload-3-${formResetKey}`} // 폼 초기화를 위한 key - onFileSelect={(file, cd, id) => handleFileSelection(3, file, cd, id)} - showButton={false} - showPreview={true} - accept={".xls, .xlsx, .xlsm, .xltx"} - fileIdx={3} - /> - </FormSection> - </div> - </ListDiv> - </div>]} - {/* <div className="flex items-center gap-4"> - <FormSection - name='region' form={form} titleNm='▷ 유량자료 등록'> - <FileUploadForm - key={`file-upload-3-${formResetKey}`} // 폼 초기화를 위한 key - onFileSelect={(file, cd, id) => handleFileSelection(3, file, cd, id)} - showButton={false} - showPreview={true} - accept={".xls, .xlsx, .xlsm, .xltx"} - fileIdx={3} /> - </FormSection> - <div className="flex justify-center mt-1"> - <Button size="sm" type="submit" onClick={handleAnalysisClick}>침입수분석 실행</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 deleted file mode 100644 index 2f61b57b9..000000000 --- a/apps/sn/app/ii/rdii/page.tsx +++ /dev/null @@ -1,438 +0,0 @@ -"use client" - -import { postData } from '@/components/lib/api' -import FileUploadForm from "@/components/ui/fileUploadForm" -import FormSection from "@/components/ui/formSection" -import BaseLayout from "@/ii/BaseLayout" -import { DatePicker, filePicked } from "@common/business_components/ui" -import { Input } from "@common/components/ui" -import { zodResolver } from "@hookform/resolvers/zod" -import { Label } from '@radix-ui/react-label' -import { useState } from "react" -import { useFieldArray, useForm } from "react-hook-form" -import { z } from "zod" -import { excelToMatchingResultData, excelToRainData } from '../../components/lib/utils/excelToJson' -import { EditableFormTable, type EditableTableHeadItem } from '../../components/ui/EditableFormTable' - - -// zod 스키마 정의 -const formSchema = z.object({ - enoCd: z.string(), // 환경청코드 - ctpvCd: z.string(), // 시도코드 - sggCd: z.string(), // 시군구코드 - swageTmplntCd: z.string(), // 하수처리장코드 - swtrmtZoneCd: z.string(), // 하수처리구역코드 - rtkSource: z.string(), // RTK계수 출처 - rdiiArea: z.string(), // RDII 기여면적 - 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(), - })), - // osumap: z.instanceof(File).optional(), // 오수관망도 - // matching: z.instanceof(File).optional(), // 수용가-맨홀 매칭결과 - aplcnYm: z.string(), // 상수사용량 적용기간 (YYYYMM 형식) - rate: z.string(), // 오수전환율 - // rain: z.instanceof(File).optional(), // 강우자료 - analysisInfo: z.string().optional(), // 분석 정보 - date: z.date(), - // date: z.coerce.date(), -}) -// zod 스키마 타입 추론 -type FormSchemaType = z.infer<typeof formSchema>; -// 기본 폼 값 정의 (테스트 편의를 위해 초기값 설정, 추후 삭제 필요) -const defaultFormValues: FormSchemaType = { - enoCd: "", - ctpvCd: "", - sggCd: "", - swageTmplntCd: "", - swtrmtZoneCd: "", - rtkSource: "출처", - rdiiArea: "123", // 문자열로 기본값 설정 - unitFactors: [ - { response: "Short-Term", R: "0.040", T: "0.5", K: "1" }, // 문자열로 기본값 설정 - { response: "Medium-Term", R: "0.005", T: "1", K: "2" }, - { response: "Long-Term", R: "0.005", T: "2", K: "3" }, - ], - initialDepths: [ - { response: "Short-Term", Dmax: "0", Drec: "0", Do: "0" }, // 문자열로 기본값 설정 - { response: "Medium-Term", Dmax: "0", Drec: "0", Do: "0" }, - { response: "Long-Term", Dmax: "0", Drec: "0", Do: "0" }, - ], - // osumap: undefined, - // matchingResultFile: undefined, - aplcnYm: "", - rate: "0.8", // 문자열로 기본값 설정 - // rainFile: undefined, - analysisInfo: "test 분석정보", - date: new Date(), -}; - -// 각 파일 업로드 폼의 데이터를 저장할 인터페이스 정의 -interface UploadedFileInfo { - file: File | null; - atflCd: string | null; - atflId: string | null; -} - -// 초기 파일 정보 상태 -const initialFileInfo: UploadedFileInfo = { file: null, atflCd: null, atflId: null }; - -interface ApiSelectOption { - text: string; - val: string; -} - -export default function RTKForm() { - const [formResetKey, setFormResetKey] = useState(0); // FileUploadForm 리셋을 위한 key 상태 - const [isLoadingResult, setIsLoadingResult] = useState<boolean>(false); // 로딩 상태 - - // FileUploadForm에서 전달받은 파일 정보를 저장할 상태 배열 - // 각 인덱스는 FileUploadForm의 fileIdx와 매칭됨 - const [uploadedFilesData, setUploadedFilesData] = useState<UploadedFileInfo[]>([ - initialFileInfo, // 0: 오수관망도 - initialFileInfo, // 1: 수용가-맨홀 매칭결과 - initialFileInfo, // 2: 강우자료 - ]); - - const [select1, setSelect1] = useState('0'); // 환경청코드 (enoCd) - const [select2, setSelect2] = useState('0'); // 시도코드 (ctpvCd) - const [select3, setSelect3] = useState<ApiSelectOption>({ val: '0', text: '전체' }); // 시군구 (sggCd) - const [select4, setSelect4] = useState<string | number>('0'); // 하수처리장 (swageTmplntCd) - const [select5, setSelect5] = useState<string | number>('0'); // 하수처리구역 (swtrmtZoneCd) - - - // form 인스턴스 생성 - const form = useForm<FormSchemaType>({ - resolver: zodResolver(formSchema), - defaultValues: defaultFormValues - }); - - // 폼 초기화 함수 - const resetAllForms = () => { - form.reset(defaultFormValues); // 폼 값 초기화 - setUploadedFilesData([ // 파일 업로드 상태 초기화 (3개 파일) - initialFileInfo, - initialFileInfo, - initialFileInfo, - ]); - setFormResetKey(prevKey => prevKey + 1); // FileUploadForm 재마운트를 위해 key 변경 - setSelect1('0'); - setSelect2('0'); - setSelect3({ val: '0', text: '전체' }); - setSelect4('0'); - setSelect5('0'); - }; - - // FileUploadForm의 onFileSelect prop으로 전달될 콜백 함수 - const handleFileSelection = ( - index: number, // FileUploadForm의 fileIdx 값 - selectedFile: File | null, - cd: string | null, - id: string | null - ) => { - setUploadedFilesData(prevData => { - const newData = [...prevData]; - newData[index] = { - file: selectedFile, - atflCd: cd, - atflId: id, - }; - return newData; - }); - console.log(`Page: File data received for form index ${index}`, { fileName: selectedFile?.name, cd, id }); - }; - - const registFile = async (analysisData: any) => { - console.log("Submitting combined analysis data to backend"); - // 추후 API 엔드포인트에 맞게 URL 수정 필요 - const data = await postData('v1/rdiiAnls', analysisData); - if (data) { - alert("분석 데이터가 성공적으로 제출되었습니다."); - resetAllForms(); // 성공 시 폼 초기화 함수 호출 - } else alert("분석 데이터 제출 중 오류가 발생했습니다."); - }; - - // 침입수 분석 실행 버튼 클릭 핸들러 - const handleAnalysisClick = async () => { - setIsLoadingResult(true); // 로딩 상태 시작 - const formData = form.getValues(); - console.log(uploadedFilesData); - const file1 = uploadedFilesData[0]; // 오수관망도 - const file2 = uploadedFilesData[1]; // 수용가-맨홀 매칭결과 - const file3 = uploadedFilesData[2]; // 강우자료 - - console.log("오수관망도 정보:", file1?.file); - console.log("수용가-맨홀 매칭결과 정보:", file2); - console.log("강우자료:", file3); - - // 모든 필수 파일이 선택되었는지 확인 - if (!file1?.file || !file2?.file || !file3?.file) { - setIsLoadingResult(false); - alert('모든 분석용 파일을 선택해주세요.'); - return; - } - - const filePickers = [ - { file: file2, parser: excelToMatchingResultData }, - { file: file3, parser: excelToRainData }, - ]; - - try { - const parsedResults = await Promise.all( - filePickers.map(async ({ file, parser }) => { - if (!file || !file.file) return null; // fileInfo 객체 및 file 객체 null 체크 - - // filePicked 함수에 File 객체와 headerNum 전달 - const jsonData = await filePicked(file.file, 0); - - if (jsonData === undefined) { - console.warn(`Warning: filePicked returned undefined for ${file.file.name}`); - return { - fileName: file.file.name, - atflCd: file.atflCd, - atflId: file.atflId, - parsedData: null // 또는 적절한 기본값 - }; - } - - // jsonData를 각 parser 함수로 처리 - // parser 함수가 filePicked가 반환하는 JSON 형식을 직접 처리한다고 가정 - const parsedDataBySpecificFunction = parser(jsonData); - - return { - fileName: file.file.name, - atflCd: file.atflCd, - atflId: file.atflId, - parsedData: parsedDataBySpecificFunction - }; - }) - ); - - // 각 파싱 결과 접근 - const matchingResultData = parsedResults.find(r => r && r.fileName === file2.file?.name)?.parsedData; - const rainData = parsedResults.find(r => r && r.fileName === file3.file?.name)?.parsedData; - - // 새로운 JSON 데이터 생성 - const combinedAnalysisData = { - enoCd: select1.toString(), // 환경청코드 - aplcnYm: new Date(formData.date).toISOString().substring(0, 7).replace("-", ""), // 적용연월 (YYYYMM 형식) - ctpvCd: select2.toString(), // 시도코드 - sggCd: select3.val.toString(), // 시군구코드 - swageTmplntCd: select4.toString(), // 하수처리장코드 - swtrmtZoneCd: select5.toString(), // 하수처리구역코드 - atflCd: file1?.atflCd, // 첨부파일CD (오수관망도) - atflId: file1?.atflId, // 첨부파일ID (오수관망도) - atflInpNm: file1?.file?.name || "", // 오수관망도 파일명 - atflMpngNm: file2?.file?.name || "", // 수용가-맨홀 매칭결과 파일명 - atflRainNm: file3?.file?.name || "", // 강우자료 파일명 - cntbArea: parseFloat(formData.rdiiArea) || null, // RDII 기여면적(ha) - srcNm: formData.rtkSource, // 출처이름 (RTK계수 출처) - - shortPrdR: parseFloat(formData.unitFactors[0]?.R || "0"), - shortPrdT: parseFloat(formData.unitFactors[0]?.T || "0"), - shortPrdK: parseFloat(formData.unitFactors[0]?.K || "0"), - - mdPrdR: parseFloat(formData.unitFactors[1]?.R || "0"), - mdPrdT: parseFloat(formData.unitFactors[1]?.T || "0"), - mdPrdK: parseFloat(formData.unitFactors[1]?.K || "0"), - - longPrdR: parseFloat(formData.unitFactors[2]?.R || "0"), - longPrdT: parseFloat(formData.unitFactors[2]?.T || "0"), - longPrdK: parseFloat(formData.unitFactors[2]?.K || "0"), - - shortPrdMaxDpth: parseFloat(formData.initialDepths[0]?.Dmax || "0"), - shortPrdRcvryRt: parseFloat(formData.initialDepths[0]?.Drec || "0"), - shortPrdBgngDpth: parseFloat(formData.initialDepths[0]?.Do || "0"), - - mdPrdMaxDpth: parseFloat(formData.initialDepths[1]?.Dmax || "0"), - mdPrdRcvryRt: parseFloat(formData.initialDepths[1]?.Drec || "0"), - mdPrdBgngDpth: parseFloat(formData.initialDepths[1]?.Do || "0"), - - longTermMaxDpth: parseFloat(formData.initialDepths[2]?.Dmax || "0"), - longTermRcvryRt: parseFloat(formData.initialDepths[2]?.Drec || "0"), - longTermBgngDpth: parseFloat(formData.initialDepths[2]?.Do || "0"), - - drtwSwtcRt: parseFloat(formData.rate || "0"), // 오수전환율 - anlsInfo: formData.analysisInfo, // 분석정보 - - tbSnRcptrMhlMpng: matchingResultData, // 수용가-맨홀 매칭결과 - tbSnRainData: rainData, // 강우자료 - }; - - console.log("통합 분석 데이터 (JSON):", combinedAnalysisData); - - // 수정된 registFile 함수를 호출하여 combinedAnalysisData를 전송합니다. - await registFile(combinedAnalysisData); - setIsLoadingResult(false); - } catch (error) { - setIsLoadingResult(false); - console.error("파일 파싱 중 오류 발생:", error); - alert("파일 처리 중 오류가 발생했습니다."); - return; - } - } - - 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) - } - - // 단위도 계수 테이블 설정 (EditableTableHeadItem 타입 사용 및 id 수정) - const unitFactorHeadlist: EditableTableHeadItem<FormSchemaType['unitFactors'][number]>[] = [ - { id: 'response', title: 'Response', editable: false, width: '150px' }, // width 속성 추가 - { id: 'R', title: 'R', editable: true, inputProps: { type: 'text' } }, - { id: 'T', title: 'T', editable: true, inputProps: { type: 'text' } }, - { id: 'K', title: 'K', editable: true, inputProps: { type: 'text' } } - ]; - - // 초기 차단 깊이 테이블 설정 (EditableTableHeadItem 타입 사용) - const initialDepthHeadlist: EditableTableHeadItem<FormSchemaType['initialDepths'][number]>[] = [ - { id: 'response', title: 'Response', editable: false, width: '150px' }, // width 속성 추가 - { id: 'Dmax', title: 'Dmax', editable: true, inputProps: { type: 'text' } }, - { id: 'Drec', title: 'Drec', editable: true, inputProps: { type: 'text' } }, - { id: 'Do', title: 'Do', editable: true, inputProps: { type: 'text' } } - ]; - - // 테이블 페이지 변경 핸들러 - const handlePageChange = (page: number) => { - console.log('Page changed:', page) - } - - return ( - // <BaseLayout searchBox={{ title: '분석 대상지역 선택' }} titleName={{ mainTitle: 'RDII 분석', firstCardTitle: 'RDII 분석자료 입력', secondCardTitle: '실측자료 등록' }} form={form} onSubmitEvent={() => { }} graphBtn={false} > - <BaseLayout - selectBox={{ title: '분석 대상지역 선택' }} - titleName={{ mainTitle: 'RDII 분석', firstCardTitle: 'RDII 분석자료 입력', secondCardTitle: '실측자료 등록' }} - form={form} // react-hook-form 인스턴스 전달 - onSubmitEvent={() => { }} // BaseLayout의 form 태그에 연결될 onSubmit - graphBtn={false} - analysisBtn={'RDII분석 실행'} - enoCd={select1} - setEnoCd={setSelect1} - ctpvCd={select2} - setCtpvCd={setSelect2} - sigungu={select3} - setSigungu={setSelect3} - selectedSwageTreatmentPlant={select4} - onSwageTreatmentPlantChange={setSelect4} - selectedSwageProcessingZone={select5} - onSwageProcessingZoneChange={setSelect5} - isLoading={isLoadingResult} - onAnalysis={handleAnalysisClick} - > - {[<div className="space-y-[1.2rem] overflow-x-auto pt-4 pb-4 px-2" key="form-section"> - <div className="grid grid-cols-2 gap-4"> - <FormSection name="rtkSource" form={form} titleNm="○ RTK계수 출처"> - <Input {...form.register("rtkSource")} className="h-12 text-center text-xl w-[225px]" /> - </FormSection> - <FormSection name="rdiiArea" form={form} titleNm="○ RDII 기여면적(ha)"> - <Input {...form.register("rdiiArea")} className="h-12 text-center text-xl w-[225px]" /> - </FormSection></div> - <div className="grid grid-cols-2 gap-4"> - <div> - <Label className="mb-2 text-xl font-medium">○ 단위도 계수</Label> - <EditableFormTable - form={form} - name="unitFactors" - fields={unitFactorFields} - headlist={unitFactorHeadlist} - tableClassName="w-[400px]" - /> - </div> - <div> - <Label className="mb-[1.2rem] text-xl font-medium">○ 초기 차단 깊이</Label> - <EditableFormTable - form={form} - name="initialDepths" - fields={initialDepthFields} - headlist={initialDepthHeadlist} - tableClassName="w-[400px]" - /> - </div> - </div> - <div className="grid grid-cols-2 gap-4"> - <FormSection name="suyeonga" form={form} titleNm="○ 오수관망도"><div className="w-[350px]"> - <FileUploadForm - key={`file-upload-0-${formResetKey}`} // 폼 초기화를 위한 key - onFileSelect={(file, cd, id) => handleFileSelection(0, file, cd, id)} - showButton={false} - showPreview={true} - accept={""} - fileIdx={0} /> - </div></FormSection> - <FormSection name="regopm" form={form} titleNm="○ 수용가-맨홀 매칭결과"><div className="w-[350px]"> - <FileUploadForm - key={`file-upload-1-${formResetKey}`} // 폼 초기화를 위한 key - onFileSelect={(file, cd, id) => handleFileSelection(1, file, cd, id)} - showButton={false} - showPreview={true} - accept={".xls, .xlsx, .xlsm, .xltx"} - fileIdx={1} /> - </div></FormSection> - <FormSection name="rain" form={form} titleNm="○ 강우자료"><div className="w-[350px]"> - <FileUploadForm - key={`file-upload-2-${formResetKey}`} // 폼 초기화를 위한 key - onFileSelect={(file, cd, id) => handleFileSelection(2, file, cd, id)} - showButton={false} - showPreview={true} - accept={".xls, .xlsx, .xlsm, .xltx"} - fileIdx={2} /> - </div></FormSection> - <FormSection name='rate' form={form} titleNm='○ 오수전환율'> - <Input {...form.register("rate")} className="h-12 text-center text-xl w-[225px] ml-5" /> - </FormSection> - <FormSection name='date' form={form} titleNm='○ 상수사용량 적용기간'> - {/* <DatePicker - selected={form.watch("date")} - onSelect={(date: Date | undefined) => { - if (date) { - form.setValue("date", date, { shouldValidate: true }); - } - }} - mode={"single"} - className="ml-5" - /> */} - <DatePicker - mode={"single"} // mode는 DayPickerSingleProps에서 오므로 그대로 사용 가능 - field={{ - value: form.watch("date"), // react-hook-form의 "date" 필드 값 사용 - onChange: (date: Date | undefined) => { - if (date) { - form.setValue("date", date, { shouldValidate: true }); - } - } - }} - // className을 직접 사용하거나, DatePicker 내부에서 selectClass를 처리하도록 수정 필요 - // 여기서는 DatePicker의 selectClass prop을 사용하고, date-picker.css의 스타일을 적용 - selectClass="ml-5 date-picker-trigger" - /> - </FormSection> - <FormSection name='rate' form={form} titleNm='○ 분석 정보'> - <Input {...form.register("analysisInfo")} className="h-12 text-center text-xl w-[225px] ml-5" /> - </FormSection> - </div> - {/* <div className="flex justify-end mt-1"> - <Button size="sm" type="submit" onClick={handleAnalysisClick}>RDII분석 실행</Button> - </div> */} - </div> - ]} - </BaseLayout > - ) -} diff --git a/apps/sn/app/ii/rdii_result/page.tsx b/apps/sn/app/ii/rdii_result/page.tsx deleted file mode 100644 index 130c25419..000000000 --- a/apps/sn/app/ii/rdii_result/page.tsx +++ /dev/null @@ -1,258 +0,0 @@ -'use client' -import { deleteData, getData } from '@/components/lib/api' -import { UiTable, downloadExcel } from '@common/business_components/ui' -import { zodResolver } from '@hookform/resolvers/zod' -import { useState } from 'react' -import { useForm } from 'react-hook-form' -import { z } from 'zod' -import BaseLayout from '../BaseLayout' -import { handleDelete } from '@/components/lib/utils/fileDeleteUtil' - - -const formSchema = z.object({ - enoCd: z.string().default('0'), // 환경청코드 - ctpvCd: z.string().default('0'), // 시도코드 - sggCd: z.object({ // 시군구 - val: z.string().default('0'), - text: z.string().default('전체'), - }).default({ val: '0', text: '전체' }), - swageTmplntCd: z.string().default('0'), // 하수처리장 - swtrmtZoneCd: z.string().default('0'), // 하수처리구역 - aplcnYm: z.date().default(new Date()), // 분석일자 (DatePicker와 연동) -}) - -type ResultFormValues = z.infer<typeof formSchema> - -const tableColumn = [ - { id: 'rainInfwtAnlsSn', title: '분석번호' }, - { id: 'swageTmplntNm', title: '하수처리장' }, - { id: 'swtrmtZoneNm', title: '처리구역' }, - { id: 'aplcnYm', title: '상수사용연월' }, - { id: 'drtwSwtcRt', title: '오수 전환율' }, - { id: 'srcNm', title: 'RTX 계수 출처' }, - { id: 'cntbArea', title: 'RDII 기여면적' }, - { id: 'anlsInfo', title: 'RDII 분석정보' }, - { id: 'regDt', title: '분석일자' }, - { id: 'rgtrNm', title: '분석자' } -] - -const Result = () => { - const form = useForm<ResultFormValues>({ - resolver: zodResolver(formSchema), - defaultValues: { - enoCd: '0', - ctpvCd: '0', - sggCd: { val: '0', text: '전체' }, - swageTmplntCd: '0', - swtrmtZoneCd: '0', - aplcnYm: new Date(), - } - }) - - - const [tableData, setTableData] = useState<any[]>([]); // API 조회 결과 저장 상태 - const [totalData, setTotalData] = useState(0) //총 데이터 수 - const [isLoadingResult, setIsLoadingResult] = useState<boolean>(false); // 로딩 상태 - const [selectedRowData, setSelectedRowData] = useState<any>(null); - const [selectedRowIndex, setSelectedRowIndex] = useState<number | undefined>(undefined); - - - const handleSearchButtonClick = async (customFormValues?: Partial<ResultFormValues>) => { - setIsLoadingResult(true); - const formValues = customFormValues || form.getValues(); // react-hook-form에서 현재 값 가져오기 - console.log("formValues", formValues); - - let inftwtAnlsSearchDto; - - if (customFormValues) { - inftwtAnlsSearchDto = { - "aplcnYm": formValues.aplcnYm, - "enoCd": formValues.enoCd, - "ctpvCd": formValues.ctpvCd, - "sggCd": formValues.sggCd, - "swtrmtZoneCd": formValues.swtrmtZoneCd, - }; - } else { - inftwtAnlsSearchDto = { - "aplcnYm": formValues.aplcnYm ? formValues.aplcnYm.toISOString().substring(0, 7).replace("-", "") : null, - "enoCd": formValues.enoCd === '0' ? null : formValues.enoCd, - "ctpvCd": formValues.ctpvCd === '0' ? null : formValues.ctpvCd, - "sggCd": formValues.sggCd?.val === '0' ? null : formValues.sggCd?.val, - "swtrmtZoneCd": formValues.swtrmtZoneCd === '0' ? null : formValues.swtrmtZoneCd, - }; - } - console.log("Search DTO:", inftwtAnlsSearchDto); - - const queryParams = new URLSearchParams({ - page: '0', - size: '10', - sort: 'rainInfwtAnlsSn,desc' - }); - Object.entries(inftwtAnlsSearchDto).forEach(([key, value]) => { - if (value !== undefined && value !== null && String(value).trim() !== '') { // undefined, null, 빈 문자열이 아닌 경우에만 추가 - queryParams.append(key, String(value)); - } - }); - const queryString = queryParams.toString(); - - console.log("API: ", `v1/rdiiAnls${queryString ? `?${queryString}` : ''}`); - try { - const data = await getData(`v1/rdiiAnls${queryString ? `?${queryString}` : ''}`); - setTableData(data.content || []); - setTotalData(tableData.length); - console.log("response:", data); - console.log("response data:", data.content || []); - setIsLoadingResult(false); - } catch (error) { - setIsLoadingResult(false); - console.error("Error fetching analysis result data:", error); - alert("분석결과 조회 중 오류가 발생했습니다."); - setTableData([]); - setTotalData(0); - } - }; - - // 테이블 셀 클릭 핸들러 추가 - const handleCellClick = (index: number | undefined, headId: string) => { - if (index !== undefined && tableData[index]) { - setSelectedRowData(tableData[index]); - setSelectedRowIndex(index); - console.log("Selected row data:", tableData[index]); - } - }; - - // Excel 다운로드 핸들러 추가 - const handleExcelDownload = async () => { - setIsLoadingResult(true); - console.log("Selected row data for download:", selectedRowData); - if (!selectedRowData || selectedRowData.rainInfwtAnlsSn === undefined) { - setIsLoadingResult(false); - alert('다운로드할 데이터를 선택해주세요.'); - return; - } - - try { - const data = await getData(`v1/rdiiAnlsRslt/ppl/${selectedRowData.rainInfwtAnlsSn}`); - - // 선택된 행 데이터를 Excel 다운로드 형식으로 변환 - const excelData = data.content; // 배열로 감싸서 전달 - - // 파일명 생성 (분석번호 기반) - const fileName = `RDII분석결과_${selectedRowData.rainInfwtAnlsSn}_${selectedRowData.aplcnYm}`; - - downloadExcel({ - data: excelData as JSON[], - fileName: fileName, - sheetNameArray: ["RDII 분석결과"], - headerRange: "A1:D1" // 테이블 컬럼 수에 맞춰 조정 - }); - setIsLoadingResult(false); - } catch (error) { - setIsLoadingResult(false); - console.error("Excel 다운로드 중 오류 발생:", error); - alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요."); - } - }; - - // 파일 삭제 핸들러 추가 - const handleFileDelete = async () => { - setIsLoadingResult(true); - console.log("Selected row data for deletion:", selectedRowData); - if (!selectedRowData || selectedRowData.rainInfwtAnlsSn === undefined) { - alert('삭제할 파일을 선택해주세요.'); - return; - } - // 삭제 확인 대화상자 - const confirmDelete = window.confirm('선택된 분석 결과를 삭제하시겠습니까?'); - if (!confirmDelete) { - return; - } - - try { - // 삭제 API 호출 - const response1 = await deleteData(`v1/rdiiAnlsRslt/${selectedRowData.rainInfwtAnlsSn}`); - const response2 = await deleteData(`v1/rdiiAnls/${selectedRowData.rainInfwtAnlsSn}`); - // 첨부파일(inp파일) 삭제 API 호출 - handleDelete(selectedRowData.atflCd, { - handleDeleteStart: () => console.log("파일 삭제 시작"), - handleDeleteSuccess: (response) => console.log("파일 삭제 성공:", response), - handleDeleteError: (error) => console.error("파일 삭제 실패:", error), - }); - console.log("File delete response1:", response1); - console.log("File delete response2:", response2); - - // 삭제 후 테이블 새로고침을 위해 재검색 - const customFormValues: Partial<ResultFormValues> = { - aplcnYm: selectedRowData.aplcnYm || '0', - enoCd: selectedRowData.enoCd || '0', - ctpvCd: selectedRowData.ctpvCd || '0', - sggCd: selectedRowData.sggCd || '0', - swtrmtZoneCd: selectedRowData.swtrmtZoneCd || '0', - }; - await handleSearchButtonClick(customFormValues); - - // 선택된 행 초기화 - setSelectedRowData(null); - setSelectedRowIndex(undefined); - - alert('삭제가 완료되었습니다.'); - } catch (error) { - console.error("분석결과 파일 삭제 중 오류 발생:", error); - alert("분석결과 파일 삭제 중 오류가 발생했습니다. 다시 시도해주세요."); - } - setIsLoadingResult(false); - }; - - return ( - <BaseLayout - selectBox={{ title: '분석 대상지역 선택', searchChk: true, onSearchClick: handleSearchButtonClick }} - titleName={{ mainTitle: '분석결과', firstCardTitle: 'RDII 산정결과 현황' }} - form={form} // form 인스턴스 전달 - onSubmitEvent={() => { }} // BaseLayout의 form 태그에 연결될 onSubmit - graphBtn={true} - showDatePicker={true} - enoCd={form.watch("enoCd")} - setEnoCd={(val) => form.setValue("enoCd", val)} - ctpvCd={form.watch("ctpvCd")} - setCtpvCd={(val) => form.setValue("ctpvCd", val)} - sigungu={form.watch("sggCd")} - setSigungu={(val) => form.setValue("sggCd", val)} - selectedSwageTreatmentPlant={form.watch("swageTmplntCd")} - onSwageTreatmentPlantChange={(val) => form.setValue("swageTmplntCd", String(val))} - selectedSwageProcessingZone={form.watch("swtrmtZoneCd")} - onSwageProcessingZoneChange={(val) => form.setValue("swtrmtZoneCd", String(val))} - aplcnYm={form.watch("aplcnYm")} - setAplcnYm={(val) => { - if (val instanceof Date) { - form.setValue("aplcnYm", val) - } else if (val) { - form.setValue("aplcnYm", new Date(val)) - } - }} - isLoading={isLoadingResult} - // Excel 다운로드 핸들러 추가 - onExcelDownload={handleExcelDownload} - onDeleteFile={handleFileDelete} - selectedRowData={selectedRowData} - > - {[<div key='table1' className="h-[55vh] flex flex-col p-4 border rounded-lg bg-white shadow-md"> - <div className="mb-[1.2rem] text-xl font-medium">○ RDII 분석결과 현황</div> - <div className="flex-grow overflow-auto"> - <UiTable - headName='' - headlist={tableColumn} - tableData={tableData} - total={totalData} - pageSize={10} - children={undefined} - cellClick={handleCellClick} - onPageChange={(e) => { }} - /> - </div> - </div> - ]} - </BaseLayout> - ) -} - -export default Result \ No newline at end of file diff --git a/apps/sn/app/ii/result/page.tsx b/apps/sn/app/ii/result/page.tsx deleted file mode 100644 index e2015c8d9..000000000 --- a/apps/sn/app/ii/result/page.tsx +++ /dev/null @@ -1,259 +0,0 @@ -'use client' -import { deleteData, getData } from '@/components/lib/api' -import { UiTable, downloadExcel } from '@common/business_components/ui' -import { zodResolver } from '@hookform/resolvers/zod' -import { useState } from 'react' -import { useForm } from 'react-hook-form' -import { z } from 'zod' -import BaseLayout from '../BaseLayout' -import { handleDelete } from '@/components/lib/utils/fileDeleteUtil' - - -const formSchema = z.object({ - enoCd: z.string().default('0'), // 환경청코드 - ctpvCd: z.string().default('0'), // 시도코드 - sggCd: z.object({ // 시군구 - val: z.string().default('0'), - text: z.string().default('전체'), - }).default({ val: '0', text: '전체' }), - swageTmplntCd: z.string().default('0'), // 하수처리장 - swtrmtZoneCd: z.string().default('0'), // 하수처리구역 - aplcnYm: z.date().default(new Date()), // 분석일자 (DatePicker와 연동) -}) - -type ResultFormValues = z.infer<typeof formSchema> - -const tableColumn = [ - // { id: 'no', title: '번호' }, - { id: 'inftwtAnlsSn', title: '분석번호' }, - { id: 'swageTmplntNm', title: '하수처리장' }, - { id: 'swtrmtZoneNm', title: '처리구역' }, - { id: 'aplcnYm', title: '적용연월' }, - { id: 'drtwSwtcRt', title: '오수 전환율' }, - { id: 'anlsInfo', title: '침입수 분석정보' }, - { id: 'regDt', title: '분석일자' }, - { id: 'rgtrNm', title: '분석자' } -] - -const Result = () => { - const form = useForm<ResultFormValues>({ - resolver: zodResolver(formSchema), - defaultValues: { - enoCd: '0', - ctpvCd: '0', - sggCd: { val: '0', text: '전체' }, - swageTmplntCd: '0', - swtrmtZoneCd: '0', - aplcnYm: new Date(), - } - }) - - - const [tableData, setTableData] = useState<any[]>([]); // API 조회 결과 저장 상태 - const [totalData, setTotalData] = useState(0) //총 데이터 수 - const [isLoadingResult, setIsLoadingResult] = useState<boolean>(false); // 로딩 상태 - const [selectedRowData, setSelectedRowData] = useState<any>(null); - const [selectedRowIndex, setSelectedRowIndex] = useState<number | undefined>(undefined); - - - const handleSearchButtonClick = async (customFormValues?: Partial<ResultFormValues>) => { - setIsLoadingResult(true); - const formValues = customFormValues || form.getValues(); // react-hook-form에서 현재 값 가져오기 - console.log("formValues", formValues); - - let inftwtAnlsSearchDto; - - if (customFormValues) { - inftwtAnlsSearchDto = { - "aplcnYm": formValues.aplcnYm, - "enoCd": formValues.enoCd, - "ctpvCd": formValues.ctpvCd, - "sggCd": formValues.sggCd, - "swtrmtZoneCd": formValues.swtrmtZoneCd, - }; - } else { - inftwtAnlsSearchDto = { - "aplcnYm": formValues.aplcnYm ? formValues.aplcnYm.toISOString().substring(0, 7).replace("-", "") : null, - "enoCd": formValues.enoCd === '0' ? null : formValues.enoCd, - "ctpvCd": formValues.ctpvCd === '0' ? null : formValues.ctpvCd, - "sggCd": formValues.sggCd?.val === '0' ? null : formValues.sggCd?.val, - "swtrmtZoneCd": formValues.swtrmtZoneCd === '0' ? null : formValues.swtrmtZoneCd, - }; - } - console.log("Search DTO:", inftwtAnlsSearchDto); - - const queryParams = new URLSearchParams({ - page: '0', - size: '10', - sort: 'inftwtAnlsSn,desc' - }); - Object.entries(inftwtAnlsSearchDto).forEach(([key, value]) => { - if (value !== undefined && value !== null && String(value).trim() !== '') { // undefined, null, 빈 문자열이 아닌 경우에만 추가 - queryParams.append(key, String(value)); - } - }); - const queryString = queryParams.toString(); - - console.log("API: ", `v1/inftwtAnls${queryString ? `?${queryString}` : ''}`); - try { - const data = await getData(`v1/inftwtAnls${queryString ? `?${queryString}` : ''}`); - setTableData(data.content || []); - setTotalData(tableData.length); - console.log("response:", data); - console.log("response data:", data.content || []); - setIsLoadingResult(false); - } catch (error) { - setIsLoadingResult(false); - console.error("Error fetching analysis result data:", error); - alert("분석결과 조회 중 오류가 발생했습니다."); - setTableData([]); - setTotalData(0); - } - }; - - // 테이블 셀 클릭 핸들러 추가 - const handleCellClick = (index: number | undefined, headId: string) => { - if (index !== undefined && tableData[index]) { - setSelectedRowData(tableData[index]); - setSelectedRowIndex(index); - console.log("Selected row data:", tableData[index]); - } - }; - - // Excel 다운로드 핸들러 추가 - const handleExcelDownload = async () => { - setIsLoadingResult(true); - console.log("Selected row data for download:", selectedRowData); - if (!selectedRowData || selectedRowData.inftwtAnlsSn === undefined) { - setIsLoadingResult(false); - alert('다운로드할 데이터를 선택해주세요.'); - return; - } - - try { - const data = await getData(`v1/inftwtAnlsRslt/ppl/${selectedRowData.inftwtAnlsSn}`); - - // 선택된 행 데이터를 Excel 다운로드 형식으로 변환 - const excelData = data.content; // 배열로 감싸서 전달 - - // 파일명 생성 (분석번호 기반) - const fileName = `침입수분석결과_${selectedRowData.inftwtAnlsSn}_${selectedRowData.aplcnYm}`; - - downloadExcel({ - data: excelData as JSON[], - fileName: fileName, - sheetNameArray: ["침입수 분석결과"], - headerRange: "A1:D1" // 테이블 컬럼 수에 맞춰 조정 - }); - setIsLoadingResult(false); - } catch (error) { - setIsLoadingResult(false); - console.error("Excel 다운로드 중 오류 발생:", error); - alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요."); - } - }; - - // 파일 삭제 핸들러 추가 - const handleFileDelete = async () => { - setIsLoadingResult(true); - console.log("Selected row data for deletion:", selectedRowData); - if (!selectedRowData || selectedRowData.inftwtAnlsSn === undefined) { - setIsLoadingResult(false); - alert('삭제할 파일을 선택해주세요.'); - return; - } - // 삭제 확인 대화상자 - const confirmDelete = window.confirm('선택된 분석 결과를 삭제하시겠습니까?'); - if (!confirmDelete) { - return; - } - - try { - // 삭제 API 호출 - const response1 = await deleteData(`v1/inftwtAnlsRslt/${selectedRowData.inftwtAnlsSn}`); - const response2 = await deleteData(`v1/inftwtAnls/${selectedRowData.inftwtAnlsSn}`); - // 첨부파일(inp파일) 삭제 API 호출 - handleDelete(selectedRowData.atflCd, { - handleDeleteStart: () => console.log("파일 삭제 시작"), - handleDeleteSuccess: (response) => console.log("파일 삭제 성공:", response), - handleDeleteError: (error) => console.error("파일 삭제 실패:", error), - }); - console.log("File delete response1:", response1); - console.log("File delete response2:", response2); - - // 삭제 후 테이블 새로고침을 위해 재검색 - const customFormValues: Partial<ResultFormValues> = { - aplcnYm: selectedRowData.aplcnYm || '0', - enoCd: selectedRowData.enoCd || '0', - ctpvCd: selectedRowData.ctpvCd || '0', - sggCd: selectedRowData.sggCd || '0', - swtrmtZoneCd: selectedRowData.swtrmtZoneCd || '0', - }; - await handleSearchButtonClick(customFormValues); - - // 선택된 행 초기화 - setSelectedRowData(null); - setSelectedRowIndex(undefined); - setIsLoadingResult(false); - - alert('삭제가 완료되었습니다.'); - } catch (error) { - setIsLoadingResult(false); - console.error("분석결과 파일 삭제 중 오류 발생:", error); - alert("분석결과 파일 삭제 중 오류가 발생했습니다. 다시 시도해주세요."); - } - }; - - return ( - <BaseLayout - selectBox={{ title: '분석 대상지역 선택', searchChk: true, onSearchClick: handleSearchButtonClick }} - titleName={{ mainTitle: '분석결과', firstCardTitle: '분석이력' }} - form={form} // form 인스턴스 전달 - onSubmitEvent={() => { }} // BaseLayout의 form 태그에 연결될 onSubmit - graphBtn={true} - showDatePicker={true} - enoCd={form.watch("enoCd")} - setEnoCd={(val) => form.setValue("enoCd", val)} - ctpvCd={form.watch("ctpvCd")} - setCtpvCd={(val) => form.setValue("ctpvCd", val)} - sigungu={form.watch("sggCd")} - setSigungu={(val) => form.setValue("sggCd", val)} - selectedSwageTreatmentPlant={form.watch("swageTmplntCd")} - onSwageTreatmentPlantChange={(val) => form.setValue("swageTmplntCd", String(val))} - selectedSwageProcessingZone={form.watch("swtrmtZoneCd")} - onSwageProcessingZoneChange={(val) => form.setValue("swtrmtZoneCd", String(val))} - aplcnYm={form.watch("aplcnYm")} - setAplcnYm={(val) => { - if (val instanceof Date) { - form.setValue("aplcnYm", val) - } else if (val) { - form.setValue("aplcnYm", new Date(val)) - } - }} - isLoading={isLoadingResult} // 로딩 상태 전달 - // Excel 다운로드 핸들러 추가 - onExcelDownload={handleExcelDownload} - onDeleteFile={handleFileDelete} - selectedRowData={selectedRowData} - > - {[<div key='table1' className="h-[55vh] flex flex-col p-4 border rounded-lg bg-white shadow-md"> - <div className="mb-[1.2rem] text-xl font-medium">○ 침입수 산정결과 현황</div> - <div className="flex-grow overflow-auto"> - <UiTable - headName='' - headlist={tableColumn} - tableData={tableData} - total={totalData} - pageSize={10} - children={undefined} - cellClick={handleCellClick} - onPageChange={(e) => { }} - /> - </div> - </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 index f983b20bc..066d9646b 100644 --- a/apps/sn/app/layout.tsx +++ b/apps/sn/app/layout.tsx @@ -1,10 +1,5 @@ -import "./styles/base.css"; -import "./styles/components.css"; import type { Metadata } from "next"; -import { cookies } from "next/headers"; -import { StoreProvider } from "./store/store-provider"; -import { AuthWrapper } from "./components/lib/auth"; -import { ClientLayout } from "./clientLayout"; +import { App } from "@/app"; export const metadata: Metadata = { title: "디지털 하수관로", @@ -16,22 +11,4 @@ export const metadata: Metadata = { } }; -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> - <AuthWrapper> - <ClientLayout>{children}</ClientLayout> - </AuthWrapper> - </StoreProvider> - </body> - </html> - ); -} +export default App; \ No newline at end of file diff --git a/apps/sn/app/map/SearchModal.tsx b/apps/sn/app/map/SearchModal.tsx deleted file mode 100644 index e1c2bd0de..000000000 --- a/apps/sn/app/map/SearchModal.tsx +++ /dev/null @@ -1,1128 +0,0 @@ -import FeatureClickEventPopup from './featureClickEventPopup'; -import React, { useState } from 'react'; -import axios from 'axios'; -import { UiModal, UiTable } from '@common/business_components/ui'; -import { Button, Input, Label } from '@common/components'; -import { UiTable2 } from '@common/business_components/ui/table/index-2'; -import { UITab2 } from '@common/business_components/ui/tab/index-2'; -import { SelectBox2 } from '@common/business_components/ui/select/index-2'; -import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox'; -import { SidoSigunguSelectBox } from '@/components/ui/SidoSigunguSelectBox'; -import { SwtrmtFcltCol, SwtrmtPrcsZoneCol } from './components/common/table'; -import LoadingOverlay from '@/components/ui/LoadingBar'; - -const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}`; - -// 탭 -const TABS = [ - { val: "basic", text: "기본정보" }, - { val: "operation", text: "운영정보" }, - { val: "report", text: "하수도정비기본계획보고서" }, -]; -// 기본정보 '구분' 선택 목록 -const BASIC_TYPES = [ - { val: "tb_ms_swage_ppl", text: "하수관로" }, - { val: "tb_il_swage_prcs_fclt", text: "하수처리시설" }, - { val: "tb_ms_swage_prcs_zone", text: "하수처리구역" }, - { val: "tb_ms_swage_mh", text: "하수맨홀" }, - { val: "tb_ms_swage_psmtn_1", text: "오수펌프" }, - { val: "tb_ms_swage_psmtn_2", text: "빗물펌프" }, - { val: "tb_ms_otfl", text: "우수토실" }, -]; -// 운영정보 '구분' 선택 목록 -const WORK_TYPES = [ - { val: "maintenance", text: "보수 및 갱신" }, - { val: "cleaning", text: "청소 및 준설" }, - { val: "inspection", text: "점검 및 진단" }, -]; - -const YEARS = Array.from({ length: 100 }, (_, i) => `${2025 - i}`); -// 테이블 헤더 리스트 -// 하수관로 -const swagePplTableHeadlist = [ - { id: 'pipeNumber', title: '관로번호' }, - { id: 'gisManageNumber', title: '관리번호(GIS)' }, - { id: 'managingInstitution', title: '관리기관' }, - { id: 'drawingNumber', title: '도엽번호' }, - { id: 'installationLocation', title: '설치위치' }, - { id: 'usage', title: '용도' }, - { id: 'pipeDiameter', title: '관경' }, - { id: 'pipeLength', title: '관로연장' }, - { id: 'pipeType', title: '관종' }, - { id: 'burialDepth', title: '매설심도' }, - { id: 'constructionYear', title: '개보수 년도' }, - { id: 'installationSubject', title: '설치 주체' }, - { id: 'area', title: '구역' }, - { id: 'division', title: '분구' }, - { id: 'alignmentType', title: '지선/간선 구분' }, - { id: 'flowType', title: '흐름 방식' }, - { id: 'boxCount', title: '박스 열수' }, - { id: 'height', title: '높이' }, - { id: 'width', title: '폭' }, - { id: 'startManholeNumber', title: '시점 맨홀번호' }, - { id: 'endManholeNumber', title: '종점 맨홀번호' }, - { id: 'startElevation', title: '시점관저고' }, - { id: 'endElevation', title: '종점관저고' }, -] - -// 하수맨홀 (추후 작성 예정) -const swageMhTableHeadlist = [ - { id: "swtrmtFcltCd", title: "하수처리시설코드" }, -]; -// 운영정보 -const operationTableHeadlist = [ - { id: "no", title: "NO" }, - { id: "manageNumber", title: "관리번호" }, - { id: "diagnosisYear", title: "진단년도" }, - { id: "null1", title: "..." }, - { id: "null2", title: "..." }, - { id: "installationLocation", title: "매설위치" }, -]; -// 하수도정비 기본계획 보고서 -const reportTableHeadlist = [ - { id: "no", title: "NO" }, - { id: "approvalYear", title: "승인년도" }, - { id: "sido", title: "시도" }, - { id: "sigungu", title: "시군구" }, - { id: "treatmentPlant", title: "하수처리장" }, - { id: "treatmentZone", title: "하수처리구역" }, - { id: "reportCount", title: "보고서(00건)" }, - { id: "planMapCount", title: "하수도대장 평면도(00건)" }, - { id: "regDate", title: "등록일자" }, -]; - -const 처리구역List = [ - { val: "0", text: "선택" }, - { val: "a", text: "A" }, - { val: "b", text: "B" }, -]; - -interface SearchModalProps { - open: boolean; - onOpenChange: (open: boolean) => void; - onFeatureSelect: (row: any) => void; -} - -// 날짜 조합 함수 -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, - onFeatureSelect, -}) => { - const [activeTab, setActiveTab] = useState(TABS[0]?.val ?? "basic"); - const [workType, setWorkType] = useState(WORK_TYPES[0]?.val ?? ""); - const [showTable, setShowTable] = useState(false); - const [tableData, setTableData] = useState<any[]>([]); - const [tableCount, setTableCount] = useState<number>(0); - const [curPage, setCurPage] = useState(1); - const [basicType, setBasicType] = useState(BASIC_TYPES[0]?.val ?? ""); - const [시도, set시도] = useState(0); - const [시군구, set시군구] = useState(0); - const [처리구역, set처리구역] = useState(""); - // 날짜 상태 선언 - const [시작1, set시작1] = useState(""); - const [시작2, set시작2] = useState(""); - const [종료1, set종료1] = useState(""); - const [종료2, set종료2] = useState(""); - const [변수1, set변수1] = useState(""); - const [변수2, set변수2] = useState(""); - const [변수3, set변수3] = useState(""); - const [변수4, set변수4] = useState(""); - // 시작, 종료, 경과년도 - const [yearFilter, setYearFilter] = useState({ - 시작: "", - 종료: "", - 경과년도: "", - }); - - const [가동개시_시작_월, set가동개시_시작_월] = useState(''); - const [가동개시_종료_월, set가동개시_종료_월] = useState(''); - const [가동개시_시작_일, set가동개시_시작_일] = useState(''); - const [가동개시_종료_일, set가동개시_종료_일] = useState(''); - - // 로딩바 - const [isLoading, setIsLoading] = useState(false); - // 상세 정보 조회를 위한 상태 추가 - const [popupFeature, setPopupFeature] = useState<any>(null); - - // 오늘 날짜 구하기 - 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 = SwtrmtPrcsZoneCol; // default 값 - if (activeTab === 'operation') dynamicHeadlist = operationTableHeadlist; // 운영정보 - else if (activeTab === 'report') dynamicHeadlist = reportTableHeadlist; // 하수도정비기본계획보고서 - else if (basicType === 'tb_ms_swage_ppl') dynamicHeadlist = swagePplTableHeadlist; // 하수관로 - else if (basicType === 'tb_il_swage_prcs_fclt') dynamicHeadlist = SwtrmtFcltCol; // 하수처리시설 - else if (basicType === 'tb_ms_swage_prcs_zone') dynamicHeadlist = SwtrmtPrcsZoneCol; // 하수처리구역 - else if (basicType === 'tb_ms_swage_mh') dynamicHeadlist = swageMhTableHeadlist; // 하수맨홀 - - // 조회 버튼 클릭 시 1페이지로 이동하도록 수정 - const handleSearch = () => { - setCurPage(1); // 페이지를 1로 초기화 - fetchFilteredData(); - setShowTable(true); - }; - - // cellClick 함수 타입 지정 - const handleCellClick = (index?: number, headId?: string) => { - if (typeof index === "number") { - onFeatureSelect(tableData[index % 10]); // 10은 page size 하드 코딩한 숫자 - onOpenChange(false); // SearchModal 닫기 - } - }; - - // 구분(basicType)이 바뀔 때마다 queryParams, filterParams 초기화 - React.useEffect(() => { - // filterParams 초기화 - // setFilterParams({}); - set시작1(""); - set시작2(""); - set종료1(""); - set종료2(""); - set변수1(""); - set변수2(""); - set변수3(""); - set변수4(""); - set가동개시_시작_월(""); - set가동개시_종료_월(""); - set가동개시_시작_일(""); - set가동개시_종료_일(""); - set시도(0); - set시군구(0); - set처리구역(""); - setYearFilter({ - 시작: "", - 종료: "", - 경과년도: "", - }); - setShowTable(false); - setTableCount(0); - setTableData([]); - // queryParams는 fetchFilteredData 내에서 매번 새로 생성되므로 별도 상태 초기화는 필요 없음 - }, [activeTab, basicType]); - - // 페이지 변경 시마다 데이터 fetch - React.useEffect(() => { - // showTable이 true일 때만 fetch (최초 렌더 방지) - if (showTable) { - fetchFilteredData(); // 실제 API 사용 시 - // fetchDummyData(); // 더미 데이터 사용 시 - } - }, [curPage]); - - // 하수관로용 CQL 필터 생성 - const pplCqlFilters = () => { - const cqlFilters: string[] = []; - // 추후 작성 예정 - if (변수1) cqlFilters.push(`용도코드='${변수1}'`); - if (변수2) cqlFilters.push(`재질코드='${변수2}'`); - if (변수3) cqlFilters.push(`행정읍면동코드='${변수3}'`); - if (변수4) cqlFilters.push(`경과년도='${변수4}'`); - // 설치년도 범위 - const startDate = getDateString( - yearFilter.시작, - 가동개시_시작_월, - 가동개시_시작_일, - "19000101", - ); - const endDate = getDateString( - yearFilter.종료, - 가동개시_종료_월, - 가동개시_종료_일, - `${todayYear}${todayMonth}${todayDate}`, - ); - cqlFilters.push( - `설치시작년도 >= '${startDate}' and 설치시작년도 <= '${endDate}'`, - ); - // 관경(mm) 범위 - if (시작1 && 종료1) cqlFilters.push(`관경 BETWEEN ${시작1} AND ${종료1}`); - else if (시작1) cqlFilters.push(`관경 >= ${시작1}`); - else if (종료1) cqlFilters.push(`관경 <= ${종료1}`); - // 연장(m) 범위 - if (시작2 && 종료2) cqlFilters.push(`연장 BETWEEN ${시작2} AND ${종료2}`); - else if (시작2) cqlFilters.push(`연장 >= ${시작2}`); - else if (종료2) cqlFilters.push(`연장 <= ${종료2}`); - - return cqlFilters; - }; - // 하수처리시설용 CQL 필터 생성 - const facilityCqlFilters = () => { - const cqlFilters: string[] = []; - // 가동개시일자 범위 - const startDate = getDateString( - yearFilter.시작, - 가동개시_시작_월, - 가동개시_시작_일, - "19000101", - ); - const endDate = getDateString( - yearFilter.종료, - 가동개시_종료_월, - 가동개시_종료_일, - `${todayYear}${todayMonth}${todayDate}`, - ); - cqlFilters.push( - `oprtng_strt_ymd >= '${startDate}' and oprtng_strt_ymd <= '${endDate}'`, - ); - // 시설용량 범위 - if (시작1 && 종료1) - cqlFilters.push(`fclt_cpct BETWEEN ${시작1} AND ${종료1}`); - else if (시작1) cqlFilters.push(`fclt_cpct >= ${시작1}`); - else if (종료1) cqlFilters.push(`fclt_cpct <= ${종료1}`); - // 하수처리시설명 - if (변수1) cqlFilters.push(`swtrmt_fclt_nm ILIKE '%${변수1}%'`); - return cqlFilters; - }; - // 하수처리구역용 CQL 필터 생성 - const zoneCqlFilters = () => { - const cqlFilters: string[] = []; - if (시도) cqlFilters.push(`ctpv_cd='${시도}'`); - if (시군구) cqlFilters.push(`sgg_cd='${시군구}'`); - if (변수1) cqlFilters.push(`swtrmt_zone_nm ILIKE '%${변수1}%'`); - if (변수2) cqlFilters.push(`swtrmt_fclt_nm ILIKE '%${변수2}%'`); - return cqlFilters; - }; - // 하수맨홀용 CQL 필터 생성 - const mhCqlFilters = () => { - const cqlFilters: string[] = []; - // 설치년도 범위 - const startDate = getDateString( - yearFilter.시작, - 가동개시_시작_월, - 가동개시_시작_일, - "19000101", - ); - const endDate = getDateString( - yearFilter.종료, - 가동개시_종료_월, - 가동개시_종료_일, - `${todayYear}${todayMonth}${todayDate}`, - ); - cqlFilters.push( - `설치시작년도 >= '${startDate}' and 설치시작년도 <= '${endDate}'`, - ); - if (변수1) cqlFilters.push(`경과년도='${변수1}'`); - if (변수2) cqlFilters.push(`용도코드='${변수2}'`); - // 구경(mm) 범위 - if (시작1 && 종료1) cqlFilters.push(`구경 BETWEEN ${시작1} AND ${종료1}`); - else if (시작1) cqlFilters.push(`구경 >= ${시작1}`); - else if (종료1) cqlFilters.push(`구경 <= ${종료1}`); - - return cqlFilters; - }; - // 하수펌프장용 CQL 필터 생성 - const psmtnCqlFilters = () => { - const cqlFilters: string[] = []; - // 설치년도 범위 - const startDate = getDateString( - yearFilter.시작, - 가동개시_시작_월, - 가동개시_시작_일, - "19000101", - ); - const endDate = getDateString( - yearFilter.종료, - 가동개시_종료_월, - 가동개시_종료_일, - `${todayYear}${todayMonth}${todayDate}`, - ); - cqlFilters.push( - `설치시작년도 >= '${startDate}' and 설치시작년도 <= '${endDate}'`, - ); - if (변수1) cqlFilters.push(`경과년도='${변수1}'`); - // 일일처리용량 범위 - if (시작1 && 종료1) - cqlFilters.push(`일일처리용량 BETWEEN ${시작1} AND ${종료1}`); - else if (시작1) cqlFilters.push(`일일처리용량 >= ${시작1}`); - else if (종료1) cqlFilters.push(`일일처리용량 <= ${종료1}`); - // 우수양수능력 범위 - if (시작2 && 종료2) - cqlFilters.push(`우수양수능력 BETWEEN ${시작2} AND ${종료2}`); - else if (시작2) cqlFilters.push(`우수양수능력 >= ${시작2}`); - else if (종료2) cqlFilters.push(`우수양수능력 <= ${종료2}`); - - return cqlFilters; - }; - // 우수토실용 CQL 필터 생성 - const otflCqlFilters = () => { - const cqlFilters: string[] = []; - // 설치년도 범위 - const startDate = getDateString( - yearFilter.시작, - 가동개시_시작_월, - 가동개시_시작_일, - "19000101", - ); - const endDate = getDateString( - yearFilter.종료, - 가동개시_종료_월, - 가동개시_종료_일, - `${todayYear}${todayMonth}${todayDate}`, - ); - cqlFilters.push( - `설치시작년도 >= '${startDate}' and 설치시작년도 <= '${endDate}'`, - ); - if (변수1) cqlFilters.push(`경과년도='${변수1}'`); - if (변수2) cqlFilters.push(`하천명 ILIKE '%${변수1}%'`); - // 계획방류량 범위 - if (시작1 && 종료1) - cqlFilters.push(`계획방류량 BETWEEN ${시작1} AND ${종료1}`); - else if (시작1) cqlFilters.push(`계획방류량 >= ${시작1}`); - else if (종료1) cqlFilters.push(`계획방류량 <= ${종료1}`); - - return cqlFilters; - }; - // 운영정보용 CQL 필터 생성 - const operationCqlFilters = () => { - const cqlFilters: string[] = []; - // 설치년도 범위 - const startDate = getDateString( - yearFilter.시작, - 가동개시_시작_월, - 가동개시_시작_일, - "19000101", - ); - const endDate = getDateString( - yearFilter.종료, - 가동개시_종료_월, - 가동개시_종료_일, - `${todayYear}${todayMonth}${todayDate}`, - ); - cqlFilters.push( - `설치시작년도 >= '${startDate}' and 설치시작년도 <= '${endDate}'`, - ); - return cqlFilters; - }; - // 하수도정비기본계획보고서용 CQL 필터 생성 - const reportCqlFilters = () => { - const cqlFilters: string[] = []; - // 설치년도 범위 - if (시도) cqlFilters.push(`ctpv_cd='${시도}'`); - if (시군구) cqlFilters.push(`sgg_cd='${시군구}'`); - if (처리구역) cqlFilters.push(`처리구역='${처리구역}'`); - if (변수1) cqlFilters.push(`처리구역코드='${변수1}'`); - const startDate = getDateString( - yearFilter.시작, - 가동개시_시작_월, - 가동개시_시작_일, - "19000101", - ); - const endDate = getDateString( - yearFilter.종료, - 가동개시_종료_월, - 가동개시_종료_일, - `${todayYear}${todayMonth}${todayDate}`, - ); - cqlFilters.push( - `설치시작년도 >= '${startDate}' and 설치시작년도 <= '${endDate}'`, - ); - return cqlFilters; - }; - - // 검색 API 호출 함수 - const fetchFilteredData = async () => { - setIsLoading(true); // 로딩 시작 - try { - const queryParams = new URLSearchParams({ - lyrTypeNm: basicType, - page: `${curPage - 1}`, - size: '10', - sort: 'string', - }); - - let cqlFilters: string[] = []; - if (activeTab === 'basic') { - if (basicType === 'tb_ms_swage_prcs_zone') cqlFilters = zoneCqlFilters(); - else if (basicType === 'tb_il_swage_prcs_fclt') cqlFilters = facilityCqlFilters(); - else if (basicType === 'tb_ms_swage_ppl') cqlFilters = pplCqlFilters(); - } else if (activeTab === 'operation') { - cqlFilters = zoneCqlFilters(); - } else { - cqlFilters = reportCqlFilters(); - } - - if (cqlFilters.length > 0) queryParams.append('cqlFiter', cqlFilters.join(' and ')); - - console.log('API URL:', `${API_URL}/v1/fclts?${queryParams.toString()}`); - const res = await axios.get(`${API_URL}/v1/fclts?${queryParams.toString()}`); - console.log('필터링된 API 결과(개수):', res.data.count); - console.log('필터링된 API 결과(데이터):', res.data.data); - - setTableData(res.data.data || []); - setTableCount(res.data.count || []); - console.log('tableCount 상태 업데이트 후:', res.data.count); // tableCount 로그 추가 - setShowTable(true); - } catch (err) { - console.error('API ERROR:', err); - setTableData([]); - } finally { - setIsLoading(false); // 로딩 종료 - } - }; - - const renderBody = () => ( - <> - {/* 탭 영역 */} - <UITab2 - defaultValue={TABS[0]?.val} - tabList={TABS.map((tab) => ({ - value: tab.val, - label: tab.text, - onClick: () => setActiveTab(tab.val), - }))} - > - {/* 각각의 탭 영역 */} - {/* 기본정보 탭 */} - {activeTab === 'basic' && ( - <div className="relative border rounded-lg p-6 mt-4 w-full h-[23vh]"> - {/* 구분 */} - <div className="mb-3 flex items-center justify-between gap-4"> - <div className="flex items-center"> - <SelectBox2 - className="w-[150px] mr-3" - selectClass="rounded-lg bg-white" - label="구분" - selectArray={BASIC_TYPES} - onSelectValue={(value: string | number) => - setBasicType(String(value)) - } - value={basicType} - /> - </div> - </div> - {/* 분기처리: 하수관로 */} - {basicType === "tb_ms_swage_ppl" && ( - <div className="mb-6"> - <div className="mb-2 flex items-center"> - {/* 설치년도 */} - <YearRangeSelectBox - start={yearFilter.시작} - end={yearFilter.종료} - onChange={(range) => - setYearFilter({ - ...yearFilter, - 시작: range.start, - 종료: range.end, - }) - } - startLabel="설치년도" - endLabel="" - /> - - {/* 경과년도 */} - <div className="flex items-center"> - <Label className="mr-2 w-auto text-xl text-gray-600"> - 경과년도 - </Label> - <Input - className='!text-xl w-[70px] placeholder:text-center mr-8' - placeholder="입력" - value={yearFilter.경과년도} - onChange={(e) => { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - setYearFilter({ ...yearFilter, 경과년도: val }); - }} - /> - </div> - {/* 용도코드 */} - <Label className="mr-2 w-auto text-xl text-gray-600"> - 용도코드 - </Label> - <Input - className="mr-8 w-[100px] !text-xl placeholder:text-center" - placeholder="입력" - value={변수1} - onChange={(e) => { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set변수1(val); - }} - /> - {/* 재질코드 */} - <Label className="mr-2 w-auto text-xl text-gray-600"> - 재질코드 - </Label> - <Input - className="mr-8 w-[100px] !text-xl placeholder:text-center" - placeholder="입력" - value={변수2} - onChange={(e) => { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set변수2(val); - }} - /> - </div> - <div className="flex items-center mb-2"> - {/* 행정읍면동코드 */} - <Label className="mr-2 w-auto text-xl text-gray-600"> - 행정읍면동코드 - </Label> - <Input - className="mr-8 w-[100px] !text-xl placeholder:text-center" - placeholder="입력" - value={변수3} - onChange={(e) => { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set변수3(val); - }} - /> - {/* 관경 */} - <Label className="mr-2 w-auto text-xl text-gray-600"> - 관경(mm) - </Label> - <Input - className="w-[100px] !text-xl placeholder:text-center" - placeholder="입력" - value={시작1} - onChange={(e) => { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set시작1(val); - }} - /> - <span className="mx-2 text-xl font-semibold">~</span> - <Input - className="mr-8 w-[100px] !text-xl placeholder:text-center" - placeholder="입력" - value={종료1} - onChange={(e) => { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set종료1(val); - }} - /> - {/* 연장 */} - <Label className="mr-2 w-auto text-xl text-gray-600"> - 연장(m) - </Label> - <Input - className="w-[100px] !text-xl placeholder:text-center" - placeholder="입력" - value={시작2} - onChange={(e) => { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set시작2(val); - }} - /> - <span className="mx-2 text-xl font-semibold">~</span> - <Input - className="mr-8 w-[100px] !text-xl placeholder:text-center" - placeholder="입력" - value={종료2} - onChange={(e) => { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set종료2(val); - }} - /> - </div> - </div> - )} - {/* 분기처리: 하수처리시설 */} - {basicType === "tb_il_swage_prcs_fclt" && ( - <div className="mb-6"> - {/* 가동개시일자 */} - <div className="mb-2 flex items-center"> - <YearRangeSelectBox - start={yearFilter.시작} - end={yearFilter.종료} - onChange={(range) => - setYearFilter({ - ...yearFilter, - 시작: range.start, - 종료: range.end, - }) - } - startLabel="가동개시일자" - endLabel=" " - /> - {/* 시설용량 */} - <Label className="mr-2 w-auto text-xl text-gray-600"> - 시설용량 - </Label> - <Input - className="w-[100px] !text-xl placeholder:text-center" - placeholder="입력" - value={시작1} - onChange={(e) => { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set시작1(val); - }} - /> - <span className="mx-2 text-xl font-semibold">~</span> - <Input - className="mr-8 w-[100px] !text-xl placeholder:text-center" - placeholder="입력" - value={종료1} - onChange={(e) => { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set종료1(val); - }} - /> - {/* 하수처리시설명 */} - <Label className="mr-2 w-auto text-xl text-gray-600"> - 하수처리시설명 - </Label> - <Input - className="mr-8 w-[100px] !text-xl placeholder:text-center" - placeholder="입력" - value={변수1} - onChange={(e) => { - set변수1(e.target.value); - }} - /> - </div> - </div> - )} - {/* 분기처리: 하수처리구역 */} - {basicType === "tb_ms_swage_prcs_zone" && ( - <div className="mb-3"> - <div className="mb-2 flex items-center"> - <SidoSigunguSelectBox - sido={시도} - sigungu={시군구} - onSidoChange={(sidoId) => { - set시도(sidoId); - set시군구(0); - }} - onSigunguChange={set시군구} - /> - <Label className="mr-2 w-auto text-xl text-gray-600"> - 하수처리구역명 - </Label> - <Input - className="mr-8 w-[100px] !text-xl placeholder:text-center" - placeholder="입력" - value={변수1} - onChange={(e) => { - set변수1(e.target.value); - }} - /> - <Label className="mr-2 w-auto text-xl text-gray-600"> - 하수처리시설명 - </Label> - <Input - className='!text-xl w-[120px] placeholder:text-center mr-8' - placeholder="입력" - value={변수2} - onChange={(e) => { - set변수2(e.target.value); - }} - /> - </div> - </div> - )} - {/* 분기처리: 하수맨홀 */} - {basicType === "tb_ms_swage_mh" && ( - <div className="mb-3"> - <div className="mb-2 flex items-center"> - {/* 설치년도 */} - <YearRangeSelectBox - start={yearFilter.시작} - end={yearFilter.종료} - onChange={(range) => - setYearFilter({ - ...yearFilter, - 시작: range.start, - 종료: range.end, - }) - } - startLabel="설치년도" - endLabel="" - /> - {/* 경과년도 */} - <Label className="mr-2 w-auto text-xl text-gray-600"> - 경과년도 - </Label> - <Input - className="mr-8 w-[100px] !text-xl placeholder:text-center" - placeholder="입력" - value={yearFilter.경과년도} - onChange={(e) => { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - setYearFilter({ ...yearFilter, 경과년도: val }); - }} - /> - </div> - <div className='flex items-center mb-2'> - {/* 용도코드 */} - <Label className="mr-2 w-auto text-xl text-gray-600"> - 용도코드 - </Label> - <Input - className="mr-8 w-[100px] !text-xl placeholder:text-center" - placeholder="입력" - value={변수1} - onChange={(e) => { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set변수1(val); - }} - /> - {/* 하수맨홀 구경 */} - <Label className="mr-2 w-auto text-xl text-gray-600"> - 구경(mm) - </Label> - <Input - className="w-[100px] !text-xl placeholder:text-center" - placeholder="입력" - value={시작1} - onChange={(e) => { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set시작1(val); - }} - /> - <span className="mx-2 text-xl font-semibold">~</span> - <Input - className="mr-8 w-[100px] !text-xl placeholder:text-center" - placeholder="입력" - value={종료1} - onChange={(e) => { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set종료1(val); - }} - /> - </div> - </div> - )} - {/* 분기처리: 하수펌프장 */} - {(basicType === "tb_ms_swage_psmtn_1" || - basicType === "tb_ms_swage_psmtn_2") && ( - <div className="mb-3"> - {/* 설치년도 */} - <div className="flex items-center mb-2"> - <YearRangeSelectBox - start={yearFilter.시작} - end={yearFilter.종료} - onChange={range => setYearFilter({ ...yearFilter, 시작: range.start, 종료: range.end })} - startLabel="설치년도" - endLabel="" - /> - {/* 경과년도 */} - <Label className="w-auto text-xl text-gray-600 mr-2">경과년도</Label> - <Input - className='!text-xl w-[100px] placeholder:text-center mr-8' - placeholder="입력" - value={yearFilter.경과년도} - onChange={(e) => { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ''); - setYearFilter({ ...yearFilter, 경과년도: val }); - }} - /> - </div> - <div className="flex items-center mb-2"> - {/* 일일처리용량 - 오수맨홀 */} - <Label className="mr-2 w-auto text-xl text-gray-600"> - 일일처리용량 - </Label> - <Input - className="w-[100px] !text-xl placeholder:text-center" - placeholder="입력" - value={시작1} - onChange={(e) => { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set시작1(val); - }} - /> - <span className="mx-2 text-xl font-semibold">~</span> - <Input - className="mr-8 w-[100px] !text-xl placeholder:text-center" - placeholder="입력" - value={종료1} - onChange={(e) => { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set종료1(val); - }} - /> - {/* 우수양수능력 - 우수맨홀 */} - <Label className="mr-2 w-auto text-xl text-gray-600"> - 우수양수능력 - </Label> - <Input - className="w-[100px] !text-xl placeholder:text-center" - placeholder="입력" - value={시작2} - onChange={(e) => { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set시작2(val); - }} - /> - <span className="mx-2 text-xl font-semibold">~</span> - <Input - className="mr-8 w-[100px] !text-xl placeholder:text-center" - placeholder="입력" - value={종료2} - onChange={(e) => { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set종료2(val); - }} - /> - </div> - </div> - )} - {/* 분기처리: 우수토실? 토구? */} - {basicType === "tb_ms_otfl" && ( - <div className="mb-3"> - {/* 설치년도 */} - <div className="flex items-center mb-2"> - <YearRangeSelectBox - start={yearFilter.시작} - end={yearFilter.종료} - onChange={range => setYearFilter({ ...yearFilter, 시작: range.start, 종료: range.end })} - startLabel="설치년도" - endLabel="" - /> - {/* 경과년도 */} - <Label className="w-auto text-xl text-gray-600 mr-2">경과년도</Label> - <Input - className='!text-xl w-[100px] placeholder:text-center mr-8' - placeholder="입력" - value={yearFilter.경과년도} - onChange={(e) => { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ''); - setYearFilter({ ...yearFilter, 경과년도: val }); - }} - /> - </div> - <div className="flex items-center mb-2"> - {/* 하천명 */} - <Label className="mr-2 w-auto text-xl text-gray-600"> - 하천명 - </Label> - <Input - className="mr-8 w-[100px] !text-xl placeholder:text-center" - placeholder="입력" - value={변수1} - onChange={(e) => { - set변수1(e.target.value); - }} - /> - {/* 계획방류량 */} - <Label className="mr-2 w-auto text-xl text-gray-600"> - 계획방류량 - </Label> - <Input - className="w-[100px] !text-xl placeholder:text-center" - placeholder="입력" - value={시작1} - onChange={(e) => { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set시작1(val); - }} - /> - <span className="mx-2 text-xl font-semibold">~</span> - <Input - className="mr-8 w-[100px] !text-xl placeholder:text-center" - placeholder="입력" - value={종료1} - onChange={(e) => { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set종료1(val); - }} - /> - </div> - </div> - )} - - {/* 조회 버튼 중앙 정렬 */} - <div className="absolute bottom-4 right-4 mr-2"> - <Button className="text-base py-2" onClick={handleSearch}>조회</Button> - </div> - </div> - )} - {/* 운영정보 탭 */} - {activeTab === 'operation' && ( - <div className="relative flex flex-col border rounded-lg p-6 mb-4 w-full h-[23vh]"> - <div className="flex items-center mb-6"> - <SelectBox2 - className="w-60 mr-8" - selectClass="rounded-lg bg-white text-xl" - label="업무구분" - selectArray={WORK_TYPES.map((w) => ({ - val: w.val, - text: w.text, - }))} - onSelectValue={(val) => setWorkType(String(val))} - value={workType} - /> - - {/* 설치년도 */} - <YearRangeSelectBox - start={yearFilter.시작} - end={yearFilter.종료} - onChange={(range) => - setYearFilter({ - ...yearFilter, - 시작: range.start, - 종료: range.end, - }) - } - startLabel="설치년도" - endLabel="" - /> - <div className="flex items-center"> - {/* 경과년도 */} - <Label className="mr-2 w-auto text-xl text-gray-600"> - 경과년도 - </Label> - <Input - className="mr-8 w-[100px] !text-xl placeholder:text-center" - placeholder="입력" - value={yearFilter.경과년도} - onChange={(e) => { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - setYearFilter({ ...yearFilter, 경과년도: val }); - }} - /> - </div> - </div> - {/* 조회 버튼 중앙 정렬 */} - <div className="absolute bottom-4 right-4 mr-2"> - <Button className="text-base py-2" onClick={handleSearch}>조회</Button> - </div> - </div> - )} - {/* 하수도정비기본계획보고서 탭 */} - {activeTab === 'report' && ( - <div className="relative flex flex-col border rounded-lg p-6 mb-4 w-full h-[23vh]"> - {/* 시도, 시군구, 하수처리구역 한 줄 */} - <div className="mb-6 flex items-center"> - <SidoSigunguSelectBox - sido={시도} - sigungu={시군구} - onSidoChange={(sidoId) => { - set시도(sidoId); - set시군구(0); - }} - onSigunguChange={set시군구} - /> - </div> - <div className="flex justify-between items-center mb-6"> - <div className="flex items-center gap-4"> - <SelectBox2 - className="w-auto mr-2" - selectClass="rounded-lg bg-white text-xl" - label="하수처리구역" - selectArray={처리구역List.map(opt => ({ val: opt.val, text: opt.text }))} - onSelectValue={val => set처리구역(String(val))} - value={처리구역} - /> - <Input - className='!text-xl w-[100px] placeholder:text-center mr-8' - placeholder="입력" - value={변수1} - onChange={(e) => { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ''); - set변수1(val); - }} - /> - <YearRangeSelectBox - start={yearFilter.시작} - end={yearFilter.종료} - onChange={range => setYearFilter({ ...yearFilter, 시작: range.start, 종료: range.end })} - startLabel="승인년도" - endLabel="" - /> - </div> - {/* 조회 버튼 중앙 정렬 */} - <div className="absolute bottom-4 right-4 mr-2"> - <Button className="text-base py-2" onClick={handleSearch}>조회</Button> - </div> - </div> - </div> - )} - </UITab2> - - {/* 📋 결과 리스트 */} - <div className="h-[60vh] w-full space-y-2 overflow-hidden rounded-lg border"> - {/* 테이블 영역 (가로/세로 스크롤 가능) */} - <div className="flex-1 px-2 pb-2"> - <LoadingOverlay isLoading={isLoading}/> - <UiTable2 - headName={ - (() => { - const resultCount = tableData.length; - const label = - activeTab === "basic" - ? BASIC_TYPES.find((type) => type.val === basicType)?.text - : activeTab === "operation" - ? WORK_TYPES.find((type) => type.val === workType)?.text - : activeTab === "report" - ? "하수도정비기본계획보고서" - : TABS.find((tab) => tab.val === activeTab)?.text; - return ( - <> - {label} 조회 결과 ({tableCount})건 - </> - ); - })() - } - tableData={tableData} - headlist={dynamicHeadlist} - pageSize={tableData.length < 10 ? tableData.length : 10} - total={tableCount} - children={undefined} - cellClick={handleCellClick} - onPageChange={setCurPage} - resetPageNo={false} - cur={curPage} - /> - </div> - </div> - </> - ); - - return ( - <UiModal - open={open} - onOpenChange={onOpenChange} - bodyClass={` - h-[100vh] - w-[60vw] - max-w-none - max-h-none - overflow-visible - z-[101] - `} - body={renderBody()} - /> - ); -}; - -export default React.memo(SearchModal); diff --git a/apps/sn/app/map/components/InputSection.tsx b/apps/sn/app/map/components/InputSection.tsx deleted file mode 100644 index 6c411e2cf..000000000 --- a/apps/sn/app/map/components/InputSection.tsx +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index b72ec26d5..000000000 --- a/apps/sn/app/map/components/common/index.ts +++ /dev/null @@ -1,62 +0,0 @@ -import MsSwagePpl from "../popupComponent/MsSwagePpl"; -import sewagePump from "../popupComponent/sewagePump"; -import smh from "../popupComponent/smh"; -import Ssc from "../popupComponent/Ssc"; -import StormWaterPump from "../popupComponent/stormWaterPump"; -import SwtrmtFclt from "../popupComponent/SwtrmtFclt"; - -type LayerComponentProps = { - featureData: any; - chkMainSsc?: boolean; - additionalData?: any; -}; -type LayerContentMap = { - [key: string]: { - component?: React.FC<LayerComponentProps>; - props?: any; - }; -}; -// 레이어 이름과 컴포넌트를 매핑 -// 레이어 선택시 변경되는 팝업 컨텐츠 동적으로 변경하도록 작업 -const layerContentMap: LayerContentMap = { - // TODO: gis데이터 추가되면 추가 작업 필요 - "하수처리시설": { component: SwtrmtFclt, }, - "하수처리구역": { }, - "우수토실":{component: Ssc,}, - "중점우수토실":{component: Ssc, }, // 중점우수토실 여부 설정 - "하수관로":{component: MsSwagePpl,}, - "하수맨홀":{component: smh,}, - "빗물펌프":{component:StormWaterPump}, - "오수펌프":{component:sewagePump}, -}; - -// 동적 컴포넌트 반환 함수 -export const getLayerContentComponent = (layerNm: string): React.FC<LayerComponentProps> | null => { - 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 deleted file mode 100644 index 6c641f9be..000000000 --- a/apps/sn/app/map/components/common/table.ts +++ /dev/null @@ -1,128 +0,0 @@ -// 하수처리시설 -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: '하수처리시설명' }, -]; - -// 하수맨홀 -export const MsSwageMhlCol = [ - { id: 'fid', title: 'FID' }, - { id: 'geom', title: 'geometry' }, - { id: 'ftreSmblCd', title: '지형지물부호코드' }, - { id: 'mngNo', title: '관리번호' }, - // { id: 'pbadmsEmdCd', title: '행정읍면동코드' }, - { id: 'mapshtNo', title: '도엽번호' }, - // { id: 'mngInstCd', title: '관리기관코드' }, - { id: 'instlYmd', title: '설치일자' }, - { id: 'lastDrdgnYmd', title: '최종준설일자' }, - { id: 'usgCd', title: '용도코드' }, - // { id: 'fcltyShpCd', title: '시설물형태코드' }, - { id: 'mhlKndCd', title: '맨홀종류코드' }, - { id: 'swageMhDiam', title: '하수맨홀구경' }, - { id: 'wdthLen', title: '가로길이' }, - { id: 'vrtcLen', title: '세로길이' }, - // { id: 'mqltCd', title: '재질코드' }, - { id: 'altdVl', title: '표고값' }, - { id: 'dpthlVl', title: '심도값' }, - { id: 'instMngNo', title:'기관관리번호'} -]; - -// 중점우수토실 측정지점 -export const untrtSwgRainCol1 = [ - { id: 'manage_no', title: '관리번호' }, - { id: 'swr_ppl_eqp_nm', title: '처리구역' }, - { id: 'swr_ppl_trt_nm', title: '처리분구' }, - { id: 'addr', title: '인근주소' }, - { id: 'chmbr_xcnts', title: '토실 위도' }, - { id: 'chmbr_ydnts', title: '토실 경도' } -]; - -// 중점우수토실 측정지점 시설현황 -export const untrtSwgRainCol2 = [ - { id: 'flwmtr_mesure_tp_etc', title: '구분' }, - { id: 'discharge_pipe_spec', title: '방류관로 규격' }, - { id: 'swr_ppl_eqp_ar', title: '배수구역 면적(m²)' }, - { id: 'swr_trt_popl', title: '하수처리 인구(인)' }, - { id: 'screen_instl_at', title: '스크린설치 여부' }, - { id: 'weir_hg', title: '위어 높이(m)' }, - { id: 'weir_lt', title: '위어 길이(m)' } -]; - -// 중점우수토실 강우정보 (id값 틀릴 수 있음) -export const untrtSwgRainCol3 = [ - { id: 'rain_begin_dt', title: '강우시작일시' }, - { id: 'rain_end_dt', title: '강우종료일시' }, - { id: 'precdng_dry', title: '선행건기(h)' }, - { id: 'rain_info', title: '강우정보' }, - { id: 'rain_fall', title: '누적강우량(mm)' }, - { id: 'cntnc_time', title: '지속시간(h)' }, - { id: 'rain_in', title: '강우강도(mm/h)' } -]; - -// 중점우수토실 측정정보 (id값 틀릴 수 있음) -export const untrtSwgRainCol4 = [ - { id: 'mesure_begin_dt', title: '측정시작일시' }, - { id: 'mesure_end_dt', title: '측정종료일시' }, - { id: 'mesure_intrvl', title: '측정간격(분)' }, - { id: 'mesure_mhrls_instl_lc', title: '측정기기 설치위치' }, - { id: 'flwmtr_combined', title: '유량계 종류 및 모델명' }, // 합쳐진 컬럼 - { id: 'qltwtr_begin_dt', title: '채수시작일시' }, - { id: 'qltwtr_end_dt', title: '채수종료일시' }, - { id: 'qltwtr_co', title: '채수횟수' } -]; - -// 중점우수토실 수량/수질측정 정보 -export const untrtSwgRainCol5 = [ - { id: 'odr', title: '회차' }, - { id: 'mesureDt', title: '측정일자' }, - { id: 'mesureQy', title: '수량(m)' }, - { id: 'mesureBod', title: 'BOD(mg/L)' }, - { id: 'mesureToc', title: 'TOC(mg/L)' }, - { id: 'mesureLoad', title: '오염부하량(kg)' } -]; \ No newline at end of file diff --git a/apps/sn/app/map/components/popupComponent/MsSwagePpl.tsx b/apps/sn/app/map/components/popupComponent/MsSwagePpl.tsx deleted file mode 100644 index 1db284c8e..000000000 --- a/apps/sn/app/map/components/popupComponent/MsSwagePpl.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { VerticalTable } from '@/components/ui/verticalTable'; -import React, { useState } from 'react' -import InputSection from '../InputSection'; -import { handleInputChange } from '../common'; -import { Button } from '@common/components/ui'; -import InfilChart from './chart/infilChart'; -// 하수관로 -// TODO: RDII 버튼인가 그거 팝업 두개 구현 필요 -const inputColumn = [ - { id: "title", title: "설치주제", type: "select", options: ["BTL", "재정사업"] }, - { id: "swtrmt_zone_cd", title: "처리구역", type: "select", options: ["구역1", "구역2"] }, - { id: "basu", title: "배수구역", type: "input" }, - { id: "mltv_dtdv_nm", title: "배수분구", type: "input" }, - { id: "wltjs", title: "지선/간선", type: "select", options: ["지선", "간선"] }, - { id: "flow", title: "흐름방식", type: "select", options: ["자연유하", "압송"] }, - { id: "place", title: "매설위치", type: "select", options: ["도로", "하상"] }, -]; -const SwtrmtFclt = ({ featureData }: { featureData: any }) => { - console.log("featureData", featureData); - const [isModalOpen, setIsModalOpen] = React.useState(false); - const [btnTxt, setBtnTxt] = useState<string>(""); - const openModal = (e: React.MouseEvent<HTMLButtonElement>) => { - e.currentTarget.textContent && setBtnTxt(e.currentTarget.textContent) - setIsModalOpen(true); - }; - const closeModal = () => { - setIsModalOpen(false); - }; - 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 }, - }; - const keysToExtract = [ - "swtrmt_zone_cd", - "mltv_dtdv_nm", - "fid", - "lyrTypeNm", - "reg_dt", - "mdfcn_dt", - "rgtr", - "mdfr", - ]; - // const filteredData = Object.entries(featureData) - // .filter(([key]) => keysToExtract.includes(key)) // 필요한 키만 필터링 - // .map(([key, value]) => ({ key, value: value ?? "N/A" })); // 테이블 데이터 형식으로 변환 - - 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 className="absolute bottom-0 left-0 w-full flex justify-start space-x-2 p-4"> - <Button - className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700" - onClick={openModal} - > - Infil - </Button> - <Button - className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700" - onClick={openModal} - > - RDII - </Button> - </div> - <InfilChart btnTxt={btnTxt} isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} closeModal={closeModal} /> - </div> - ) -} - -export default SwtrmtFclt \ No newline at end of file diff --git a/apps/sn/app/map/components/popupComponent/Ssc.tsx b/apps/sn/app/map/components/popupComponent/Ssc.tsx deleted file mode 100644 index 2ca7c174d..000000000 --- a/apps/sn/app/map/components/popupComponent/Ssc.tsx +++ /dev/null @@ -1,139 +0,0 @@ -// 우수토실 -import { UiModal, UiTable } from '@common/business_components/ui'; -import { Button, TableHead, TableRow } from '@common/components/ui'; -import React from 'react'; -import { untrtSwgRainCol3, untrtSwgRainCol4, untrtSwgRainCol5 } from '../common/table'; - -const Ssc = ({ featureData, chkMainSsc, additionalData }: { - featureData: any, - chkMainSsc?: boolean, - additionalData?: any -}) => { - console.log('Additional data received:', additionalData); // 추가 데이터 확인용 - - const [isModalOpen, setIsModalOpen] = React.useState(false); - const openModal = () => { - setIsModalOpen(true); - }; - const closeModal = () => { - setIsModalOpen(false); - }; - - // 데이터 전처리: 합쳐진 필드 추가 - const preprocessedData = featureData ? [{ - ...featureData, - flwmtr_combined: `${featureData.flwmtr_mesure_tp_etc || '-'} / ${featureData.flwmtr_knd || '-'}` - }] : []; - - const renderModalBody = () => ( - <div className='grid grid-cols-1 gap-4 p-4'> - {/* 강우정보 테이블 */} - <UiTable - headName='강우정보' - tableData={preprocessedData} - headlist={untrtSwgRainCol3} - pageSize={10} - total={0} - children={undefined} - cellClick={undefined} - onPageChange={function (page: number): void { - console.log('Rain data page change:', page); - }} - /> - - {/* 측정정보 테이블 */} - <UiTable - headName='측정정보' - tableData={preprocessedData} - headlist={untrtSwgRainCol4} - pageSize={10} - total={0} - cellClick={undefined} - onPageChange={function (page: number): void { - console.log('Measure data page change:', page); - }} - > - {/* 커스텀 헤더 */} - <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={additionalData} - headlist={untrtSwgRainCol5} - pageSize={10} - total={additionalData.length} - children={undefined} - cellClick={undefined} - onPageChange={function (page: number): void { - console.log('Quality data page change:', page); - }} - /> - </div> - ); - - 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.'); - }} - /> */} - - {!chkMainSsc ? null : - <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> - - <UiModal - open={isModalOpen} - onOpenChange={setIsModalOpen} - bodyClass={` - h-[100vh] - w-[60vw] - max-w-none - max-h-none - overflow-visible - z-[101] - `} - body={renderModalBody()} - noHeaderYn='Y' - noFooterYn='N' - // footerContent={renderFooterContent()} - /> - </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 deleted file mode 100644 index eef14ea5e..000000000 --- a/apps/sn/app/map/components/popupComponent/SwtrmtFclt.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import Modal from '@/components/ui/modal'; -import { VerticalTable } from '@/components/ui/verticalTable'; -import { UiTable } from '@common/business_components/ui'; -import { Button, TableCell } from '@common/components/ui'; -import React, { useState } from 'react' - -// 하수처리시설 -const headList = [ - { id: "title", title: "하수도정비 기본계획보고서" }, - { id: "cad", title: "하수도대장 평면도" },] -const modalHeadList = [ - { id: "no", title: "번호" }, - { id: "year", title: "승인연도" }, - { id: "cad", title: '평면도'}, - { id: "fileNm", title: "파일명" }, - { id: "date", title: "입력시간" }, -] - -const SwtrmtFclt = () => { - const [isModalOpen, setIsModalOpen] = useState(false); - const [modalTitle, setModalTitle] = useState<string>(''); - const openModal = (title:string) => { - setIsModalOpen(true); - setModalTitle(title) - }; - const closeModal = () => { - setIsModalOpen(false); - }; - return ( - <div> - <VerticalTable children={{ - title: <Button onClick={() => openModal('하수도정비 기본계획보고서')}>보고서</Button>, - cad: <Button onClick={() => openModal('하수도대장 평면도')}>평면도</Button>, - }} headName='디지털 자료관 연계정보' tableData={[]} headlist={headList} pageSize={20} total={0} cellClick={undefined} isEditable={true} onPageChange={function (page: number): void { - throw new Error("Function not implemented."); - }}></VerticalTable> - <Modal isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} closeModal={closeModal}> - <div className='grid grid-col-2'> - <UiTable headName={modalTitle} tableData={[]} headlist={modalHeadList} pageSize={20} total={0} cellClick={undefined} children={undefined} onPageChange={function (page: number): void { - throw new Error("Function not implemented."); - } - }></UiTable> - </div> - </Modal> - </div> - ) -} - -export default SwtrmtFclt \ No newline at end of file diff --git a/apps/sn/app/map/components/popupComponent/chart/infilChart.tsx b/apps/sn/app/map/components/popupComponent/chart/infilChart.tsx deleted file mode 100644 index ba0b70455..000000000 --- a/apps/sn/app/map/components/popupComponent/chart/infilChart.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import React, { useState } from 'react' -import { type ChartConfig, MainChart, CHART_FORM } from "@common/business_components"; -import Modal from '@/components/ui/modal'; -import { Button } from '@common/components/ui'; -type ModalOpenProp = { - isModalOpen: boolean, - setIsModalOpen: (open: boolean) => void, - closeModal: () => void, - btnTxt?: string, -} -const InfilChart = ({ btnTxt, isModalOpen, setIsModalOpen, closeModal }: ModalOpenProp) => { - const [popupOpen, setPopupOpen] = React.useState(false); - const buttonRef = React.useRef<HTMLButtonElement>(null); // 버튼 참조 - const [popupPosition, setPopupPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 }); // 팝업 위치 - const openPopup = () => { - if (buttonRef.current) { - const rect = buttonRef.current.getBoundingClientRect(); // 버튼 위치 계산 - setPopupPosition({ - top: rect.bottom + window.scrollY, // 버튼 아래쪽 - left: rect.left + window.scrollX, // 버튼 왼쪽 - }); - } - setPopupOpen(true); // 팝업 열기 - }; - console.log("btnTxt", btnTxt); - const closePopup = () => {setPopupOpen(false);}; // 팝업 닫기 - const chartForm = CHART_FORM.LINE; - const chartData = { - chartData1: [ - { label: "1", val: 20 }, - { label: "3", val: 40 }, - { label: "5", val: 60 }, - ], - chartData2: [ - { label: "1", val: 15 }, - { label: "3", val: 35 }, - { label: "5", val: 55 }, - ], - } - const chartConfig = Object.values(chartData).flat().reduce((config, element) => { - config[element.label] = { - label: element.label, - color: "hsl(var(--chart-1))", - }; - return config; - }, {} as Record<string, { label: string; color: string }>) satisfies ChartConfig; - const getCurrentDate = () => { - const today = new Date(); - const year = today.getFullYear(); - const month = String(today.getMonth() + 1).padStart(2, "0"); // 월은 0부터 시작하므로 +1 - return `${year}-${month}`; - }; - return ( - <Modal isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} closeModal={closeModal}> - {/* 분석 입력현황 조회 */} - <div className='grid grid-cols-4 gap-4'> - <div className="grid grid-rows-4 gap-4 mb-4"> - <Button className="bg-white text-white w-[130px] font-bold px-4 mt-2 py-2" onClick={openPopup}> - 분석 입력현황 조회 - </Button> - - {/* 침입수량 분석결과 */} - <div className="bg-white text-red-600 font-bold px-4 py-2 "> - 침입수량 분석결과 - <div className="flex items-center mt-2"> - <span className="text-black text-xl font-bold">120</span> - <span className="ml-2 text-black">m³/d</span> - </div> - </div> - </div> - {/* 차트 */} - <div className="w-[500px] h-[300px] bg-white p-4"> - <MainChart chartConfig={chartConfig} chartForm={chartForm} chartData={chartData} chartTitle={'관로유량'} /> - </div> - </div> - {popupOpen && ( - <div - className="absolute bg-gray-100 border border-gray-300 w-[320px] shadow-lg p-4 rounded" - style={{ - top: popupPosition.top, - left: popupPosition.left, - zIndex: 1000, - }} - > - <div className="text-lg font-bold mb-2">분석 입력현황</div> - <div className="grid grid-cols-2 gap-2"> - <div>상수사용량 정보:</div> - <div> - <input type='month' defaultValue={getCurrentDate()} className="border border-gray-300 rounded px-2 py-1"/> - </div> - <div>오수 전환율:</div> - <div> - <select className="border border-gray-300 rounded px-2 py-1"> - <option>0.85</option> - <option>0.90</option> - </select> - </div> - <div>수용가-맨홀 매칭:</div> - <div> - <select className="border border-gray-300 rounded px-2 py-1"> - <option>매칭(양평).shp</option> - <option>매칭(서울).shp</option> - </select> - </div> - <div>관망도 자료:</div> - <div> - <select className="border border-gray-300 rounded px-2 py-1"> - <option>양평.inp</option> - <option>서울.inp</option> - </select> - </div> - {btnTxt === 'RDII' ? <><div>강우 자료:</div> - <div> - <select className="border border-gray-300 rounded px-2 py-1"> - <option>R20240815.xlsx</option> - </select> - </div> - <div>RTX 계수 출처:</div> - <div> - <select className="border border-gray-300 rounded px-2 py-1"> - <option>2015년 하수도정비기본계획</option> - </select> - </div> - </> : null} - </div> - <div className="flex justify-end mt-4 space-x-2"> - <button className="px-4 py-2 bg-gray-600 text-white rounded" onClick={closePopup}> - 저장 - </button> - <button className="px-4 py-2 bg-gray-600 text-white rounded" onClick={closePopup}> - 종료 - </button> - </div> - </div> - )} - </Modal> - ) -} - -export default InfilChart \ No newline at end of file diff --git a/apps/sn/app/map/components/popupComponent/sewagePump.tsx b/apps/sn/app/map/components/popupComponent/sewagePump.tsx deleted file mode 100644 index 5776df5b2..000000000 --- a/apps/sn/app/map/components/popupComponent/sewagePump.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { VerticalTable } from '@/components/ui/verticalTable'; -import React from 'react' -const inputColumn = [ - { id: "swtrmt_zone_cd", title: "처리구역", type: "select", options: ["구역1", "구역2"] }, - { id: "mltv_dtdv_nm", title: "배수분구", type: "input" }, - { id: "freq", title: "설계빈도", type: "input" }, -] -// 오수펌프 -const sewagePump = ({ featureData }: { featureData: any }) => { - 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> - ) -} - -export default sewagePump diff --git a/apps/sn/app/map/components/popupComponent/smh.tsx b/apps/sn/app/map/components/popupComponent/smh.tsx deleted file mode 100644 index e693a28ce..000000000 --- a/apps/sn/app/map/components/popupComponent/smh.tsx +++ /dev/null @@ -1,21 +0,0 @@ -// 하수맨홀 -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/components/popupComponent/stormWaterPump.tsx b/apps/sn/app/map/components/popupComponent/stormWaterPump.tsx deleted file mode 100644 index eefef8b3b..000000000 --- a/apps/sn/app/map/components/popupComponent/stormWaterPump.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { VerticalTable } from '@/components/ui/verticalTable'; -import React from 'react' -const inputColumn = [ - { id: "swtrmt_zone_cd", title: "처리구역", type: "input" }, - { id: "mltv_dtdv_nm", title: "배수분구", type: "input" }, -] -// 빗물펌프 -const StormWaterPump = ({ featureData }: { featureData: Record<string, any> }) => { - const validKey = inputColumn.map((item) => item.id); - const filteredData = featureData && Object.entries(featureData).filter(([key]) => validKey.includes(key)).reduce((obj: Record<string, any>, [key, value]) => {obj[key] = value; return obj;}, {}); - return ( - <div><VerticalTable headName='자체 입력정보' tableData={[filteredData]} headlist={inputColumn} pageSize={0} total={0} children={undefined} cellClick={undefined} isEditable={true} onPageChange={function (page: number): void { - throw new Error('Function not implemented.'); - }} /> - </div> - ) -} - -export default StormWaterPump \ No newline at end of file diff --git a/apps/sn/app/map/featureClickEventPopup.tsx b/apps/sn/app/map/featureClickEventPopup.tsx deleted file mode 100644 index 24174585c..000000000 --- a/apps/sn/app/map/featureClickEventPopup.tsx +++ /dev/null @@ -1,210 +0,0 @@ -'use client' -import { getData, putData } from "@/components/lib/api"; -import Modal from "@/components/ui/modal"; -import { VerticalTable } from "@/components/ui/verticalTable"; -import { useGisContext } from "@common/business_components/ui/map/Gis"; -import { Button } from "@common/components/ui"; -import { useEffect, useRef, useState } from "react"; -import { getLayerContentComponent } from "./components/common"; -import { SwtrmtFcltCol, SwtrmtPrcsZoneCol, untrtSwgRainCol1, untrtSwgRainCol2 } from "./components/common/table"; -// 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 featureId = typeof featureData.getId === 'function' - ? featureData.getId() - : featureData.fid; - const [layerNm, setLayerNm] = useState<string>(""); // 레이어 이름 상태 - const [tableData, setTableData] = useState<any>(null); // 피처 데이터 - const [mntrngsData, setMntrngsData] = useState(false); // 수량/수질 측정 정보 - const [headlist, setHeadlist] = useState<{ id: string; title: string; }[]>([]); // 컬럼리스트 변경상태 - const [headlist2, setHeadlist2] = useState<{ id: string; title: string; }[]>([]); // 컬럼리스트 변경상태 - const [buttonChk, setButtonChk] = useState(true) - const [fetchData, setfetchData] = useState<any>(); // api 호출여부 - const contentRef = useRef<HTMLDivElement>(null); - const [isModalOpen, setIsModalOpen] = useState(false); // 모달 열기 상태 - const [chkMainSsc, setChkMainSsc] = useState(false); // 중점우수토실 여부 - - // 피쳐데이터의 현재 레이어 이름 가져오는 함수 - const getSelectedLayers = () => { - let result = null; - 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 && featureData - && typeof featureData.getId === 'function' && source.hasFeature(featureData)) { - featureLayer = layer; // 피처가 속한 레이어 찾기 - } - }); - // SearchModal 클릭 이벤트와 Gis 클릭 이벤트 구분하기 - if (featureLayer.get('lyrNm') === null) { - if (featureData.lyrTypeNm === 'tb_il_swage_prcs_fclt') result = '하수처리시설'; - else if (featureData.lyrTypeNm === 'tb_ms_swage_prcs_zone') result = '하수처리구역'; - else if (featureData.lyrTypeNm === 'om_dt_untrt_swg_rain') result = '중점우수토실'; - } - else result = featureLayer.get('lyrNm'); - - return result; - // return featureLayer.get('lyrNm'); // 레이어 이름 출력 - }; - - useEffect(() => { - const fetchData = async () => { - if (featureId) { - const selectedLayerName = getSelectedLayers(); // 레이어 이름 가져오기 - setLayerNm(selectedLayerName); // 상태 업데이트 - const layerId = featureId.split('.')[0] - let data = await getData(`v1/fclts/${featureId}?lyrTypeNm=${layerId}`); - setfetchData(data) - switch (selectedLayerName) { - case '하수처리시설': - setHeadlist(SwtrmtFcltCol); - setButtonChk(false) - break; - case '하수처리구역': - setHeadlist(SwtrmtPrcsZoneCol) - setButtonChk(false) - break; - case '중점우수토실': - setHeadlist(untrtSwgRainCol1) - setHeadlist2(untrtSwgRainCol2) - setButtonChk(false) - setChkMainSsc(true) - - // 데이터 전처리: 합쳐진 필드 추가 - data = data ? { - ...data, - discharge_pipe_spec: `직경: ${data.pipe_diameter || '-'} / 높이: ${data.pipe_hg || '-'} / 길이: ${data.pipe_lt || '-'}` - } : {}; - - // 수량/수질측정 정보 데이터 호출 - const additionalResponse = await getData(`v1/onsemiro/swgRain/${data.adst_dstr_cd}/${data.manage_no}/mntrngs`); - setMntrngsData(additionalResponse.content); - break; - case '우수토실': - setButtonChk(true) - break; - case '하수관로': - setButtonChk(true) - break; - case '오수펌프': - setButtonChk(true) - break; - case '빗물펌프': - setButtonChk(true) - break; - case '하수맨홀': - setButtonChk(true) - break; - default: - break; - } - setTableData(data); - }; - } - fetchData(); - }, [featureId]); - - // 저장 시 모달 띄우기 - const handleSaveClick = () => { - setIsModalOpen(true); // 모달 열기 - }; - - const handleModalSave = () => { - setIsModalOpen(false); // 모달 닫기 - btnClickEvent(); // 저장 이벤트 실행 - }; - - const handleModalClose = () => { - setIsModalOpen(false); // 모달 닫기 - }; - - const btnClickEvent = async () => { - const bodyData: Record<string, any> = {} - if (buttonChk && contentRef.current) { - const inputElements = contentRef.current.querySelectorAll('input'); - const selectElements = contentRef.current.querySelectorAll('select'); - // 카멜케이스 변환 함수 - const toCamelCase = (str: string) => - str.replace(/_([a-z])/g, (_, char) => char.toUpperCase()); - // select 태그의 값 가져오기 - selectElements.forEach((select) => { - const camelCaseId = toCamelCase(select.id); - bodyData[camelCaseId] = select.value; - }); - // input 태그의 값 가져오기 - inputElements.forEach((input) => { - const camelCaseId = toCamelCase(input.id); - bodyData[camelCaseId] = input.value; - }); - if (layerNm === '하수관로') bodyData['lyrTypeNm'] = 'tb_ms_swage_ppl' - else if (layerNm === '우수토실') bodyData['lyrTypeNm'] = 'tb_ms_stofcbr' - else if (layerNm === '오수펌프') bodyData['lyrTypeNm'] = 'tb_ms_swage_pmstn' - else if (layerNm === '빗물펌프') bodyData['lyrTypeNm'] = 'tb_ms_swage_pmstn' - else if (layerNm === '하수맨홀') bodyData['lyrTypeNm'] = 'tb_ms_swage_mnhl' - bodyData['fid'] = featureId - const data = await putData(`v1/fclts/${featureId}`, bodyData); - if (data.status == 200) { - alert('저장되었습니다') - setTableData(data) - setfetchData(data) - } - } - } - 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-[50]"> - <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 overflow-x-hidden flex-1" ref={contentRef}> - <VerticalTable - headName='LX 연계정보' - tableName1={layerNm === '중점우수토실' ? "측정지점" : undefined} - tableData={[tableData]} - headlist={headlist} - pageSize={20} - total={0} - children={undefined} - cellClick={undefined} - onPageChange={function (page: number): void { - throw new Error("Function not implemented."); - }} - // 두 번째 테이블 조건부 props - showSecondTable={layerNm === '중점우수토실' ? true : false} - tableName2={layerNm === '중점우수토실' ? "시설현황" : undefined} - tableData2={layerNm === '중점우수토실' && tableData ? [tableData] : []} - headlist2={layerNm === '중점우수토실' ? headlist2 : undefined} - pageSize2={20} - total2={0} - isEditable2={buttonChk} - /> - <div className="w-full mx-5 flex items-center justify-end space-x-2"> - {buttonChk ? <Button onClick={handleSaveClick} 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> - <Modal isModalOpen={isModalOpen} title="자체입력정보 저장" setIsModalOpen={setIsModalOpen} closeModal={handleModalClose} saveModal={handleModalSave}><div className="text-lg">정보를 저장하시겠습니까?</div></Modal> - {ContentComponent && - <ContentComponent - featureData={fetchData} - chkMainSsc={chkMainSsc} - additionalData={mntrngsData} - /> - } - </div> - </div> - ) -} -export default FeatureClickEventPopup; \ No newline at end of file diff --git a/apps/sn/app/oper/floodPre/page.tsx b/apps/sn/app/oper/floodPre/page.tsx deleted file mode 100644 index 013b1a36c..000000000 --- a/apps/sn/app/oper/floodPre/page.tsx +++ /dev/null @@ -1,162 +0,0 @@ -'use client' -import { SearchDiv, SearchFormLeft, SearchFormRight, SelectBox, UiTable } from '@common/business_components/ui' -import { Button, DatePicker, Input, Label, LoadingWithAnimation } from '@common/components/ui' -import React, { useEffect, useState } from 'react' -import OperFileUploadForm from '../../components/ui/operFileUploadForm' -import ListDiv from '@common/business_components/ui/list_div' -import ContentLayout from '@/components/ui/contentLayout' -import { searchCmmnCd, searchData } from '@/components/lib/common/apiHelpers' -import { tableHeadlist } from '..' -import { handleDownload } from '@/components/lib/utils/fileDownload' -import { formatDate, yearOptions } from '@/components/lib/utils/date' -import { deleteData, postData } from '@/components/lib/api' -import LoadingOverlay from '@/components/ui/LoadingBar' - -const Page = () => { - // 조회조건 - const [fromDate, setFromDate] = useState(() => { const date = new Date(); date.setDate(date.getDate() - 7); return date }) - const [toDate, setToDate] = useState(new Date()) - const [manageYear, setManageYear] = useState(yearOptions[0]?.val || ''); - const [mngQu, setMngQu] = useState(yearOptions[0]?.val || ''); - const [usr, setUsr] = useState('') - const [pjNm, setpjNm] = useState('') - // 파일업로드 관련 상태 - const [fileNm, setFileNm] = useState('') //파일명 - const [atflId, setAtflId] = useState('') //파일 ID - const [sbmrgPrevntBizSn, setSbmrgPrevntBizSn] = useState('') //파일 일련번호 - const [tableData, setTableData] = useState<any>([{}]) //테이블 데이터 - const [curPage, setCurPage] = useState(1) //현재 페이지 - const [totalData, setTotalData] = useState(0) //총 데이터 수 - const [isLoading, setIsLoading] = useState(true) //로딩 상태 - const fetchData = async () => { - setIsLoading(true) - searchCmmnCd('SITEKNDCD') - try { - const { data, total } = await searchData('v1/sbmrgPrevnt', { - fromRegDt: formatDate(fromDate), - fromMngYr: manageYear, - toMngYr: mngQu, - toRegDt: formatDate(toDate), - atflNm: pjNm, - rgtrNm: usr, - page: curPage - 1, - size: '10' - }) - setTableData(data) - setTotalData(total) - } - catch (error) { console.error('Error fetching data:', error) } - finally { setIsLoading(false); } - } - useEffect(() => { - setCurPage(1) - fetchData() - }, []) - // 파일 다운로드 - const isChckFile = () => { - if (fileNm === '' || fileNm === undefined) { - alert('파일을 선택해주세요') - return false - } - handleDownload(`/attach-service/api/v1/download/${atflId}`, fileNm) - } - // 선택 파일 삭제 - const isDeleteFile = async () => { - if (sbmrgPrevntBizSn === '' || sbmrgPrevntBizSn === undefined) { - alert('삭제할 파일을 선택해주세요') - return false - } - const data = await deleteData(`/attach-service/api/v1/atfl/attachments/${atflId}`) - if (data) { - const delData = await deleteData(`v1/sbmrgPrevnt/${sbmrgPrevntBizSn}`) - if (delData.status === 200) { - alert('삭제되었습니다.') - fetchData() - } - } - } - // 현황 등록 - const registFile = async (fileName: string, year: string, quater: string, cd: string, id: string) => { - const data = await postData('v1/sbmrgPrevnt', { - atflNm: fileName, - mngYr: year, - mngQu: quater, - atflCd: cd, - atflId: id, - sbmrgPrevntBizNm: fileName - }) - if (data) { alert('파일이 등록되었습니다.'); fetchData() } - } - return ( - <ContentLayout title='도시침수 예방사업' isLoading={isLoading}> - <SearchDiv> - <SearchFormLeft> - <div className='flex items-center gap-5'> - <div className='flex items-center gap-2'> - <Label className="w-20 text-xl text-gray-600">등록기간</Label> - <DatePicker mode={'single'} - selected={fromDate} - onSelect={(date: Date | undefined) => { if (date) setFromDate(date) }} /> - <span>~</span> - <DatePicker mode={'single'} - selected={toDate} - onSelect={(date: Date | undefined) => { if (date) setToDate(date) }} /> - </div> - <div className='flex items-center gap-2'> - <Label className="w-20 text-xl text-gray-600">관리년도</Label> - <SelectBox - className="w-[90px] mr-3 ml-4" - selectClass="rounded-lg bg-white" - label="" - selectArray={yearOptions} - onSelectValue={(value: string | number) => setManageYear(String(value))} - /> - <span>~</span> - <SelectBox - className="w-[90px] mr-3 ml-4" - selectClass="rounded-lg bg-white" - label="" - selectArray={yearOptions} - onSelectValue={(value: string | number) => setMngQu(String(value))} - /> - </div> - <div className='flex items-center'> - <Label className="w-20 text-xl text-gray-600">등록자</Label> - <Input - className='w-[100px]' - value={usr} - onChange={(e) => setUsr(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" onClick={async () => await fetchData()} >조회</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" onClick={isChckFile} >파일 다운로드</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" onClick={isDeleteFile} >삭제</Button> - </div> - </div> - <UiTable headName='' tableData={tableData} headlist={tableHeadlist} pageSize={10} total={totalData} cellClick={(index) => { if (index != null) { const fileName = tableData[index]?.sbmrgPrevntBizNm ?? ""; setFileNm(fileName); const id = tableData[index]?.atflId ?? ''; setAtflId(id); setSbmrgPrevntBizSn(tableData[index]?.sbmrgPrevntBizSn); } }} onPageChange={async (page: number) => { setCurPage(page); await fetchData() }}></UiTable> - </ListDiv> - <ListDiv className="mt-4"> - <OperFileUploadForm title='도시침수 예방사업 사업추진현황 등록' onSubmit={async (file, year, quater, cd, id) => { await registFile(file, year, quater, cd, id) }} /> - </ListDiv> - </ContentLayout> - ) -} - -export default Page \ No newline at end of file diff --git a/apps/sn/app/oper/index.ts b/apps/sn/app/oper/index.ts deleted file mode 100644 index 54ba00374..000000000 --- a/apps/sn/app/oper/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const tableHeadlist = [ - { id: 'sbmrgPrevntBizSn', title: '일련번호' }, - { id: 'sbmrgPrevntBizNm', title: '파일명' }, - { id: 'mngYr', title: '년도' }, - { id: 'mngQu', title: '분기' }, - { id: 'regDt', title: '등록 일자' }, - { id: 'rgtr', title: '등록자' } - -] -export const tableHeadlistOldPp = [ - { id: 'dcrpPplMtncBizSn', title: '일련번호' }, - { id: 'dcrpPplMtncBizNm', title: '파일명' }, - { id: 'mngYr', title: '년도' }, - { id: 'mngQu', title: '분기' }, - { id: 'regDt', title: '등록 일자' }, - { id: 'rgtr', title: '등록자' } -] \ No newline at end of file diff --git a/apps/sn/app/oper/oldPp/page.tsx b/apps/sn/app/oper/oldPp/page.tsx deleted file mode 100644 index 92d7fe6aa..000000000 --- a/apps/sn/app/oper/oldPp/page.tsx +++ /dev/null @@ -1,161 +0,0 @@ -'use client' -import { SearchDiv, SearchFormLeft, SearchFormRight, SelectBox, UiTable } from '@common/business_components/ui' -import { Button, DatePicker, Input, Label } from '@common/components/ui' -import React, { useEffect, useState } from 'react' -import OperFileUploadForm from '../../components/ui/operFileUploadForm' -import ListDiv from '@common/business_components/ui/list_div' -import ContentLayout from '@/components/ui/contentLayout' -import { tableHeadlistOldPp } from '..' -import { handleDownload } from '@/components/lib/utils/fileDownload' -import { searchData } from '@/components/lib/common/apiHelpers' -import { formatDate, yearOptions } from '@/components/lib/utils/date' -import { deleteData, postData } from '@/components/lib/api' -const Page = () => { - const [fromDate, setFromDate] = useState<Date>(()=>{const date = new Date(); date.setDate(date.getDate()-7); return date}); - const [toDate, setToDate] = useState<Date>(new Date()); - const [manageYear, setManageYear] = useState(yearOptions[0]?.val || ''); - const [mngQu, setMngQu] = useState(yearOptions[0]?.val || ''); - const [usr, setUsr] = useState('') - const [pjNm, setpjNm] = useState('') - const [fileNm, setFileNm] = useState('') - const [atflId, setAtflId] = useState('') - const [dcrpPplMtncBizSn, setDcrpPplMtncBizSn] = useState('') - const [tableData, setTableData] = useState<any>([{}]) - const [curPage, setCurPage] = useState(1) - const [totalData, setTotalData] = useState(0) //총 데이터 수 - const [isLoading, setIsLoading] = useState(true) //로딩 상태 - // 데이터 조회 - const fetchData = async () => { - setIsLoading(true) - try { - const {data, total} = await searchData('v1/dcrpPpl', { - fromRegDt: formatDate(fromDate), - fromMngYr: manageYear, - toMngYr: mngQu, - toRegDt: formatDate(toDate), - dcrpPplMtncBizNm: pjNm, - rgtrNm: usr, - page: curPage - 1, - size: '10' - }) - setTableData(data) - setTotalData(total) - console.log(data, total) - }catch (error) { - console.error('Error fetching data:', error)}finally{setIsLoading(false)} - } - useEffect(() => { - setCurPage(1) - fetchData() - }, []) - // 파일 다운로드 - const isChckFile = () => { - if (fileNm === '' || fileNm === undefined) { - alert('파일을 선택해주세요') - return false - } - handleDownload(`/attach-service/api/v1/download/${atflId}`, fileNm) - return true - } - // 파일 삭제 - const isDeleteFile = async () => { - if (dcrpPplMtncBizSn === '' || dcrpPplMtncBizSn === undefined) { - alert('삭제할 파일을 선택해주세요') - return false - } - const data = await deleteData(`/attach-service/api/v1/atfl/attachments/${atflId}`) - if (data) { - const delData = await deleteData(`v1/dcrpPpl/${dcrpPplMtncBizSn}`) - if (delData.status == 200) { - alert('삭제되었습니다.') - fetchData() - } else { - alert('삭제에 실패했습니다.') - } - } - } - // 파일 등록 - const registFile = async (fileNm: string, year: string, quater: string, cd: string, id: string) => { - const data = await postData('v1/dcrpPpl', { - atflNm: fileNm, - mngYr: year, - mngQu: quater, - atflCd: cd, - atflId: id, - dcrpPplMtncBizNm: fileNm - }) - if (data) {alert('파일이 등록되었습니다.');fetchData() } - } - return ( - <ContentLayout title='노후 하수관로 정비사업' isLoading={isLoading}> - <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={fromDate} - onSelect={(date: Date | undefined) => { if (date) setFromDate(date) }} /> - <span>~</span> - <DatePicker mode={'single'} - selected={toDate} - onSelect={(date: Date | undefined) => { if (date) setToDate(date) }} /> - <div className='flex items-center gap-2'> - <Label className="w-20 text-xl text-gray-600">관리년도</Label> - <SelectBox - className="w-[90px] mr-3 ml-4" - selectClass="rounded-lg bg-white" - label="" - selectArray={yearOptions} - onSelectValue={(value: string | number) => setManageYear(String(value))} - /> - <span>~</span> - <SelectBox - className="w-[90px] mr-3 ml-4" - selectClass="rounded-lg bg-white" - label="" - selectArray={yearOptions} - onSelectValue={(value: string | number) => setMngQu(String(value))} - /> - </div> - <div className='flex items-center'> - <Label className="w-20 text-xl text-gray-600">등록자</Label> - <Input - className='w-[100px] !text-2xl' - value={usr} - onChange={(e) => setUsr(e.target.value)} - /> - </div> - <div className='flex items-center'> - <Label className="w-20 text-xl text-gray-600">파일명</Label> - <Input - className='w-[100px] !text-2xl' - 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" onClick={async () => {await fetchData()}}>조회</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" onClick={isChckFile}>파일 다운로드</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" onClick={isDeleteFile}>삭제</Button> - </div> - </div> - <UiTable headName='' tableData={tableData} headlist={tableHeadlistOldPp} pageSize={10} total={totalData} children={undefined} cellClick={(index) => { if (index != null) { const fileName = tableData[index]?.dcrpPplMtncBizNm ?? ""; setFileNm(fileName); const id = tableData[index]?.atflId ?? ''; setAtflId(id); setDcrpPplMtncBizSn(tableData[index]?.dcrpPplMtncBizSn) } }} onPageChange={async (page:number)=>{setCurPage(page); await fetchData()}}></UiTable> - </ListDiv> - <ListDiv className="mt-4"> - <OperFileUploadForm title='노후 하수관로 정비사업 주진현황 등록' onSubmit={async (file, year, quater, cd, id) => { await registFile(file, year, quater, cd, id) }} /> - </ListDiv> - </ContentLayout> - ) -} - -export default Page \ No newline at end of file diff --git a/apps/sn/app/page.tsx b/apps/sn/app/page.tsx index 36295f554..27dd1de1c 100644 --- a/apps/sn/app/page.tsx +++ b/apps/sn/app/page.tsx @@ -1,29 +1,29 @@ -'use client' -import Link from "next/link"; -import { Button } from "@common/components/ui"; +"use client"; +import { LoginApi } from "@/entities/auth"; +import { useEffect } from "react"; +import { getCookie } from "@/shared/lib"; +import { AUTH_USER_ID } from "@/shared/config"; +import { useRouter } from "next/navigation"; 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> - <Link href={"/ii/rdii_result"}><Button>RDII 분석 결과</Button></Link> - </div> - </div> - </div> - ); + const router = useRouter(); + const userId = "CLEANWATER"; + const password = "gktneh12!@"; + const checkToken = getCookie(AUTH_USER_ID); + + useEffect(() => { + if (!checkToken) { + async function doLogin() { + try { + await LoginApi.login({ userId, password }); + window.location.href = "/map"; + } catch (err) { + console.error("로그인 실패", err); + } + } + doLogin(); + } else { + router.push("/map"); + } + }, []); } diff --git a/apps/sn/app/store/index.tsx b/apps/sn/app/store/index.tsx deleted file mode 100644 index a9fc1288d..000000000 --- a/apps/sn/app/store/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { useUserStore, type IUserInfo } from "./user-store"; diff --git a/apps/sn/app/store/user-store.tsx b/apps/sn/app/store/user-store.tsx deleted file mode 100644 index 8719dc842..000000000 --- a/apps/sn/app/store/user-store.tsx +++ /dev/null @@ -1,74 +0,0 @@ -"use client"; - -import { create } from "zustand"; -import { persist } from "zustand/middleware"; -import { createStore } from "zustand/vanilla"; - -export interface IUserInfo { - regDt: string; - mdfcnDt: string; - rgtr: string; - mdfr: string; - adminRoleYn: string; - userUnqId: string; - userId: string; - userName: string; - email: string; - encryptedPassword: string; - refreshToken: string; - userStateCode: string; - lastLoginDate: string; - loginFailCount: number; - pswdHnt: string; - pswdAns: string; - mphn: string; - telno: string; - zip: string; - addr: string; - daddr: string; - laeSeCd: string; - ogdpSeCd: string; - aprvYn: string; - autzrId: string; - expryYmd: string; - lastPswdChgDt: string; - googleId: string; - kakaoId: string; - naverId: string; - isSocialUser: boolean; - hasPassword: boolean; - } - -export type UserState = { - userInfo: IUserInfo | null; -}; - -type UserStore = { - userInfo: IUserInfo | null; - setUserInfo: (userInfo: IUserInfo | null) => void; -}; - -// ✅ zustand 기본 create + persist 적용 -export const useUserStore = create<UserStore>()( - persist( - (set) => ({ - userInfo: null, - setUserInfo: (userInfo) => set({ userInfo }), - }), - { - name: "user-store", // localStorage 키 - partialize: (state) => ({ userInfo: state.userInfo }), // 선택적 저장 - }, - ), -); - -export const defaultInitState: UserState = { - userInfo: null, -}; - -export const createUserStore = (initState: UserState = defaultInitState) => { - return createStore<UserStore>()((set) => ({ - ...initState, - setUserInfo: (userInfo: IUserInfo | null) => set(() => ({ userInfo })), - })); -}; diff --git a/apps/sn/middleware.ts b/apps/sn/middleware.ts index f76696f53..6ddf29f9f 100644 --- a/apps/sn/middleware.ts +++ b/apps/sn/middleware.ts @@ -1,10 +1,13 @@ import { NextRequest, NextResponse } from "next/server"; +import path from "path"; const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL || ""; -const GEOSERVER_URL = process.env.NEXT_PUBLIC_GEOSERVER_URL || ""; const SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ""; +const GEOSERVER_URL = process.env.NEXT_PUBLIC_GEOSERVER_URL || ""; +const LX_GEOSERVER_URL = process.env.NEXT_PUBLIC_LX_GEOSERVER_URL || ""; +const DC_URL = process.env.NEXT_PUBLIC_DC_URL || ""; export function middleware(req: NextRequest) { console.log("Middleware 진입:", req.nextUrl.pathname); @@ -12,33 +15,63 @@ export function middleware(req: NextRequest) { const searchParams = req.nextUrl.searchParams; if (pathname.startsWith("/server")) { - const queryParams = searchParams.toString(); + + const decodedParams = Array.from(searchParams.entries()) + .map(([key, value]) => `${key}=${decodeURIComponent(value)}`) + .join("&"); + const newUrl = `${SERVER_API_URL}${pathname.replace("/server", "")}${ - queryParams ? `?${queryParams}` : "" + decodedParams ? `?${decodedParams}` : "" }`; - console.log(`[Middleware] Rewriting from '/server' path to: ${newUrl}`); return NextResponse.rewrite(new URL(newUrl)); } - + if (pathname.startsWith("/geoserver")) { - const queryParams = searchParams.toString(); - const newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${ - queryParams ? `?${queryParams}` : "" - }`; + let queryParams = searchParams.toString(); + const typename = searchParams.get("TYPENAME"); + + let newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; + + if ( + typename == "hasudo:tb_il_swage_prcs_fclt" //하수처리시설 + || typename == "hasudo:tb_ms_swage_prcs_zone" //하수처리구역 + || typename == "hasudo:tb_ms_sbmrg_dngr_map" //침수위험지도 + || typename == "hasudo:tb_ms_flood_dngr_map" //도시침수지도 + ) { + newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; + } + else if ( + typename == "hasudo:tb_ms_swage_ppl" //관로 + || typename == "hasudo:tb_ms_swage_mhl" //맨홀 + || typename == "hasudo:tb_ms_swage_pmstn" //펌프장 + || typename == "hasudo:tb_ms_stofcbr" //우수토실 + ) { + newUrl = `${LX_GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; + } + else if ( + typename == "hasudo:om_dt_untrt_swg_rain" //중점우수토실 + || typename == "hasudo:om_wqlt_station" //관측소 + || typename == "hasudo:tb_wr_bdst_ruse_fclt" //재이용시설 + || typename == "hasudo:tb_wr_rnwtr_utztn_fclt" //빗물이용시설 + || typename == "hasudo:tb_wr_rswtr_fclt" //중수도시설 + || typename == "hasudo:tb_wr_htdfl_fclt" //온배수시설 + || typename == "hasudo:tb_wr_ruse_fclt_dmd_co" //수요처 + || typename == "hasudo:tb_wr_tmpr_dmd_co" //잠재수요처 + ){ + const url = req.nextUrl.clone(); + url.searchParams.set('TYPENAME', typename.replace("hasudo:","")); + queryParams = url.searchParams.toString(); + newUrl = `${SEWERAGE_API_URL}/v1/map/geometry${queryParams ? `?${queryParams}` : ""}`; + } + console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`); return NextResponse.rewrite(new URL(newUrl)); } - - if (pathname.startsWith("/sewerage-service/api/v1/map/geometry")) { - const queryParams = searchParams.toString(); - const newUrl = `${SEWERAGE_API_URL}${pathname.replace("/sewerage-service/api", "")}${ - queryParams ? `?${queryParams}` : "" - }`; - console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`); - return NextResponse.rewrite(new URL(newUrl)); - } - - if (pathname.startsWith("/attach-service") || pathname.startsWith("/cms-service")) { + + if ( + pathname.startsWith("/attach-service") || + pathname.startsWith("/cms-service") + ) { const requestHeaders = new Headers(req.headers); const token = req.cookies.get(CLAIM_NAME)?.value; const userId = req.cookies.get(AUTH_USER_ID)?.value; @@ -54,7 +87,7 @@ export function middleware(req: NextRequest) { headers: requestHeaders, }); } - + if (pathname.startsWith("/user-service")) { const requestHeaders = new Headers(req.headers); const newUrl = `${SERVER_API_URL}${pathname}`; @@ -66,7 +99,21 @@ export function middleware(req: NextRequest) { headers: requestHeaders, }); } - + + if (pathname.startsWith("/dc-service")) { + const requestHeaders = new Headers(req.headers); + const decodedParams = Array.from(searchParams.entries()) + .map(([key, value]) => `${key}=${decodeURIComponent(value)}`) + .join("&"); + + const newUrl = `${DC_URL}${pathname}${decodedParams ? `?${decodedParams}` : ""}`; + console.log( + `[Middleware] Rewriting from '/dc-service' path to: ${newUrl}`, + ); + return NextResponse.rewrite(new URL(newUrl), { + headers: requestHeaders, + }); + } return NextResponse.next(); } @@ -77,6 +124,7 @@ export const config = { "/attach-service/:path*", "/user-service/:path*", "/cms-service/:path*", - "/sewerage-service/api/v1/map/geometry/:path*", + "/dc-service/:path*", ], + }; diff --git a/apps/sn/next.config.ts b/apps/sn/next.config.ts index be845eeeb..789da58b4 100644 --- a/apps/sn/next.config.ts +++ b/apps/sn/next.config.ts @@ -1,30 +1,13 @@ import type { NextConfig } from "next"; -const geoServerUrl = - process.env.NEXT_PUBLIC_GEOSERVER_URL?.replace(/\/$/, "") ?? ""; -const cnServerUrl = - process.env.NEXT_PUBLIC_SERVER_API_URL?.replace(/\/$/, "") ?? ""; - const nextConfig: NextConfig = { - // basePath: "/sn", + output: "standalone", typescript: { ignoreBuildErrors: true, // 이 줄 추가 }, eslint: { ignoreDuringBuilds: true, }, - // async rewrites() { - // return [ - // { - // source: '/geoserver/:hasudo*', - // destination: `${geoServerUrl}/:hasudo*`, - // }, - // { - // source: '/attach-service/:path*', - // destination: `${cnServerUrl}/attach-service/:path*`, - // }, - // ]; - // }, }; export default nextConfig; diff --git a/apps/sn/package.json b/apps/sn/package.json index 42c19afa7..140a1d3ab 100644 --- a/apps/sn/package.json +++ b/apps/sn/package.json @@ -5,33 +5,35 @@ "scripts": { "dev": "next dev -p 3002", "build": "next build", - "start": "next start", + "start:sn": "next start -p 3002", "lint": "next lint" }, "dependencies": { "@common/assets": "workspace:*", "@common/business_components": "workspace:*", "@common/components": "workspace:*", + "@fontsource-variable/noto-sans-kr": "^5.2.5", "@frontend-opensource/use-react-hooks": "^1.1.4", "@hookform/resolvers": "^4.1.3", + "@minoru/react-dnd-treeview": "3.5.1-alpha.1", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-slot": "^1.1.2", "@tanstack/react-query": "^5.67.1", + "@tanstack/react-query-devtools": "^5.75.5", "@tanstack/react-table": "^8.21.2", "axios": "^1.8.1", "chart.js": "^4.4.8", "dayjs": "^1.11.13", "js-cookie": "^3.0.5", + "moment": "^2.30.1", "next": "15.1.7", "react-chartjs-2": "^5.3.0", + "react-dnd": "^16.0.1", "react-hook-form": "^7.54.2", "tailwindcss-animate": "^1.0.7", + "tus-js-client": "^4.3.1", "zod": "^3.24.2", - "zustand": "^5.0.3", - "ol": "^7.3.0", - "ol-ext": "^4.0.11", - "proj4": "^2.9.0", - "tus-js-client": "^4.3.1" + "zustand": "^5.0.3" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/apps/sn/public/images/map/layericon/SWL_MANH_PS_ORANGE.png b/apps/sn/public/images/map/layericon/SWL_MANH_PS_ORANGE.png new file mode 100644 index 000000000..288d1afa8 Binary files /dev/null and b/apps/sn/public/images/map/layericon/SWL_MANH_PS_ORANGE.png differ diff --git a/apps/sn/public/images/map/layericon/WTL_RSRV_PS_BLACK.png b/apps/sn/public/images/map/layericon/WTL_RSRV_PS_BLACK.png new file mode 100644 index 000000000..395abeb78 Binary files /dev/null and b/apps/sn/public/images/map/layericon/WTL_RSRV_PS_BLACK.png differ diff --git a/apps/sn/public/images/map/layericon/WTL_RSRV_PS_GREEN.png b/apps/sn/public/images/map/layericon/WTL_RSRV_PS_GREEN.png new file mode 100644 index 000000000..33933b9e0 Binary files /dev/null and b/apps/sn/public/images/map/layericon/WTL_RSRV_PS_GREEN.png differ diff --git a/apps/sn/public/images/map/layericon/WTL_RSRV_PS_ORANGE.png b/apps/sn/public/images/map/layericon/WTL_RSRV_PS_ORANGE.png new file mode 100644 index 000000000..7a78c3466 Binary files /dev/null and b/apps/sn/public/images/map/layericon/WTL_RSRV_PS_ORANGE.png differ diff --git a/apps/sn/public/images/map/layericon/WTL_RSRV_PS_PURPLE.png b/apps/sn/public/images/map/layericon/WTL_RSRV_PS_PURPLE.png new file mode 100644 index 000000000..ef25607cf Binary files /dev/null and b/apps/sn/public/images/map/layericon/WTL_RSRV_PS_PURPLE.png differ diff --git a/apps/sn/public/images/map/layericon/WTL_RSRV_PS_RED.png b/apps/sn/public/images/map/layericon/WTL_RSRV_PS_RED.png new file mode 100644 index 000000000..26fd5f68e Binary files /dev/null and b/apps/sn/public/images/map/layericon/WTL_RSRV_PS_RED.png differ diff --git a/apps/sn/public/images/map/layericon/WTL_RSRV_PS_YELLOW.png b/apps/sn/public/images/map/layericon/WTL_RSRV_PS_YELLOW.png new file mode 100644 index 000000000..76718e865 Binary files /dev/null and b/apps/sn/public/images/map/layericon/WTL_RSRV_PS_YELLOW.png differ diff --git a/apps/sn/src/app/index.tsx b/apps/sn/src/app/index.tsx new file mode 100644 index 000000000..692b55ef0 --- /dev/null +++ b/apps/sn/src/app/index.tsx @@ -0,0 +1,20 @@ +import "./styles/base.css"; +import "./styles/components.css"; +import "@fontsource-variable/noto-sans-kr"; + +import { AppProvider } from "./providers"; +import { cookies } from "next/headers"; + +const App = async ({ children }: Readonly<{ children: React.ReactNode }>) => { + const theme = (await cookies()).get("site-theme-key"); + + return ( + <html lang="ko" data-theme={theme?.value}> + <body> + <AppProvider>{children}</AppProvider> + </body> + </html> + ); +}; + +export { App }; diff --git a/apps/sn/src/app/providers/app-provider.tsx b/apps/sn/src/app/providers/app-provider.tsx new file mode 100644 index 000000000..81821a635 --- /dev/null +++ b/apps/sn/src/app/providers/app-provider.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { DndProvider } from "react-dnd"; +import { MultiBackend, getBackendOptions } from "@minoru/react-dnd-treeview"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { ConfirmProvider } from "@frontend-opensource/use-react-hooks"; +import { AuthWrapper } from "@/entities/auth"; +import { MenuWrapper } from "@/entities/menu"; +import { PromptProvider, StoreProvider } from "@/shared/providers"; +import { AlertDialog } from "@/shared/ui/alert-dialog"; +import { PromptDialog } from "@/shared/ui/prompt-dialog"; +import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; + + +const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { + const queryClient = new QueryClient(); + return ( + <QueryClientProvider client={queryClient}> + <StoreProvider> + <PromptProvider> + <ConfirmProvider> + <AuthWrapper> + <MenuWrapper> + <DndProvider + backend={MultiBackend} + options={getBackendOptions()} + > + {children} + </DndProvider> + </MenuWrapper> + <AlertDialog /> + <PromptDialog /> + <ConfirmDialog /> + </AuthWrapper> + </ConfirmProvider> + </PromptProvider> + </StoreProvider> + <div className="text-2xl"> + <ReactQueryDevtools initialIsOpen={false} /> + </div> + </QueryClientProvider> + ); +}; + +export { AppProvider }; diff --git a/apps/sn/src/app/providers/index.ts b/apps/sn/src/app/providers/index.ts new file mode 100644 index 000000000..15f99b7fd --- /dev/null +++ b/apps/sn/src/app/providers/index.ts @@ -0,0 +1 @@ +export { AppProvider } from "./app-provider"; diff --git a/apps/sn/app/styles/base.css b/apps/sn/src/app/styles/base.css similarity index 100% rename from apps/sn/app/styles/base.css rename to apps/sn/src/app/styles/base.css diff --git a/apps/sn/app/styles/components.css b/apps/sn/src/app/styles/components.css similarity index 100% rename from apps/sn/app/styles/components.css rename to apps/sn/src/app/styles/components.css diff --git a/apps/sn/app/components/lib/auth/api/login-service.tsx b/apps/sn/src/entities/auth/api/login-service.ts similarity index 69% rename from apps/sn/app/components/lib/auth/api/login-service.tsx rename to apps/sn/src/entities/auth/api/login-service.ts index 44b7da488..c1968fe36 100644 --- a/apps/sn/app/components/lib/auth/api/login-service.tsx +++ b/apps/sn/src/entities/auth/api/login-service.ts @@ -1,31 +1,14 @@ import { AxiosError } from "axios"; +import { + ENDPOINT, + JWT_EXPIRATION_TIME, + ACCESS_TOKEN, + AUTH_USER_ID, + CLAIM_NAME, +} from "@/shared/config"; +import type { LoginService, ILogin } from "../model/login-interface"; import { tokenManager } from "../lib/token-manager"; -const ENDPOINT = { - USER_SERVICE: { - LOGIN: "/user-service/login", - USERS: "/user-service/api/v1/users", - SILENT_REFRESH: "/user-service/api/v1/users/token/refresh", - AUTHORIZATIONS: "/user-service/api/v1/authorizations", - ROLE_AUTHORIZATIONS: "/user-service/api/v1/role-authorizations", - }, -}; -const JWT_EXPIRATION_TIME = 1800000; -const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; -const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; -const ACCESS_TOKEN = process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token"; - - -export interface ILogin { - userId: string; - password: string; -} - -export interface LoginService { - login(payload: ILogin): Promise<void>; - silentRefresh(): Promise<void>; -} - export class LoginApiService implements LoginService { private static instance: LoginApiService; @@ -51,14 +34,13 @@ export class LoginApiService implements LoginService { const data = await response.json(); const accessToken = data[ACCESS_TOKEN]; const tokenId = data[AUTH_USER_ID]; - + if (!accessToken || !tokenId) { throw new Error("Missing required authentication tokens"); } tokenManager.setToken(CLAIM_NAME, accessToken); tokenManager.setToken(AUTH_USER_ID, tokenId); - tokenManager.setupRefreshTimer( () => this.silentRefresh(), JWT_EXPIRATION_TIME, @@ -75,14 +57,12 @@ export class LoginApiService implements LoginService { try { const response = await fetch( `/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`, - { - method: "PUT", - }, + { method: "PUT" }, ); const data = await response.json(); - const accessToken = response.headers.get("access-token"); - const tokenId = response.headers.get("token-id"); + const accessToken = data[ACCESS_TOKEN]; + const tokenId = data[AUTH_USER_ID]; if (!accessToken || !tokenId) { throw new Error("Missing required authentication tokens"); diff --git a/apps/sn/app/components/lib/auth/index.tsx b/apps/sn/src/entities/auth/index.ts similarity index 100% rename from apps/sn/app/components/lib/auth/index.tsx rename to apps/sn/src/entities/auth/index.ts diff --git a/apps/sn/src/entities/auth/lib/auth-wrapper.tsx b/apps/sn/src/entities/auth/lib/auth-wrapper.tsx new file mode 100644 index 000000000..f0710e249 --- /dev/null +++ b/apps/sn/src/entities/auth/lib/auth-wrapper.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { client } from "@/shared/api"; +import type { IUserInfo } from "@/shared/store"; +import { useUserStore } from "@/shared/lib"; +import { ENDPOINT, AUTH_USER_ID } from "@/shared/config"; +import { getCookie } from "@/shared/lib"; +import { LoginApi } from "../api/login-service"; + +const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; + +const AuthWrapper = ({ children }: { children: React.ReactNode }) => { + const router = useRouter(); + const { userInfo, setUserInfo } = useUserStore((state) => state); + + async function authCheck() { + const userId = getCookie(AUTH_USER_ID); + if (!userId) { + throw new Error("Missing required authentication tokens"); + } + + const response = await client.get<IUserInfo>( + `${ENDPOINT.USER_SERVICE.USERS}/info/me`, + ); + if (!userInfo) setUserInfo(response.data); + } + + useEffect(() => { + (async () => { + try { + await authCheck(); + } catch (error) { + console.error(error); + + try { + await LoginApi.silentRefresh(); + await authCheck(); + } catch (refreshError) { + console.error("Token refresh failed:", refreshError); + setUserInfo(null); + router.push("/"); + } + } + })(); + }, []); + + return <>{children}</>; +}; + +export { AuthWrapper }; diff --git a/apps/sn/src/entities/auth/lib/token-manager.ts b/apps/sn/src/entities/auth/lib/token-manager.ts new file mode 100644 index 000000000..6387f06e5 --- /dev/null +++ b/apps/sn/src/entities/auth/lib/token-manager.ts @@ -0,0 +1,59 @@ +import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config"; +import { getCookie, setCookie, removeCookie } from "@/shared/lib";; + +export class TokenManager { + private static instance: TokenManager; + private refreshTimer: NodeJS.Timeout | null = null; + + private constructor() {} + + public static getInstance(): TokenManager { + if (!TokenManager.instance) { + TokenManager.instance = new TokenManager(); + } + return TokenManager.instance; + } + + public getToken(key: string): string | undefined { + return getCookie(key); + } + + public setToken(key: string, value: string): void { + setCookie(key, value, { + secure: window.location.protocol === "https:", + sameSite: "lax", + }); + } + + public removeToken(key: string): void { + removeCookie(key); + } + + public setupRefreshTimer( + callback: () => Promise<void>, + expirationTime: number, + ): void { + if (this.refreshTimer) { + clearTimeout(this.refreshTimer); + } + + this.refreshTimer = setTimeout(() => { + callback().catch((error) => { + console.error("Silent refresh failed:", error); + this.clearAuthToken(); + }); + }, expirationTime - 60000); + } + + public clearAuthToken(): void { + if (this.refreshTimer) { + clearTimeout(this.refreshTimer); + this.refreshTimer = null; + } + + this.removeToken(CLAIM_NAME); + this.removeToken(AUTH_USER_ID); + } +} + +export const tokenManager = TokenManager.getInstance(); diff --git a/apps/sn/src/entities/auth/model/login-interface.ts b/apps/sn/src/entities/auth/model/login-interface.ts new file mode 100644 index 000000000..5aaf53044 --- /dev/null +++ b/apps/sn/src/entities/auth/model/login-interface.ts @@ -0,0 +1,9 @@ +export interface ILogin { + userId: string; + password: string; +} + +export interface LoginService { + login(payload: ILogin): Promise<void>; + silentRefresh(): Promise<void>; +} diff --git a/apps/sn/src/entities/menu-authority/@x/menu.ts b/apps/sn/src/entities/menu-authority/@x/menu.ts new file mode 100644 index 000000000..86479f859 --- /dev/null +++ b/apps/sn/src/entities/menu-authority/@x/menu.ts @@ -0,0 +1 @@ +export { MenuAuthorityApi } from "../api/menu-authority-service"; diff --git a/apps/sn/src/entities/menu-authority/@x/posting.ts b/apps/sn/src/entities/menu-authority/@x/posting.ts new file mode 100644 index 000000000..bfe84fa0d --- /dev/null +++ b/apps/sn/src/entities/menu-authority/@x/posting.ts @@ -0,0 +1 @@ +export { AuthorityButton } from "../ui/authority-button"; diff --git a/apps/sn/src/entities/menu-authority/api/menu-authority-service.ts b/apps/sn/src/entities/menu-authority/api/menu-authority-service.ts new file mode 100644 index 000000000..14988eee0 --- /dev/null +++ b/apps/sn/src/entities/menu-authority/api/menu-authority-service.ts @@ -0,0 +1,40 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + MenuAuthorityList, + MenuAuthorityParams, + MenuAuthorityPayload, + MenuAuthorityService, + PageMenuAuthority, +} from "../model/menu-authority-interface"; + +export class MenuAuthorityApiService implements MenuAuthorityService { + public async getMenuAuthorityList( + params: MenuAuthorityParams, + ): Promise<MenuAuthorityList> { + const response = await client.get<MenuAuthorityList>( + `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`, + { params }, + ); + return response.data; + } + + public async getPageMenuAuthority( + menuCd: number, + ): Promise<PageMenuAuthority[]> { + const response = await client.get<PageMenuAuthority[]>( + `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}/pageinfo/${menuCd}`, + ); + return response.data; + } + + public async saveMenuAuthority(data: MenuAuthorityPayload[]): Promise<void> { + const response = await client.post<void>( + `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`, + data, + ); + return response.data; + } +} + +export const MenuAuthorityApi = new MenuAuthorityApiService(); diff --git a/apps/sn/src/entities/menu-authority/index.ts b/apps/sn/src/entities/menu-authority/index.ts new file mode 100644 index 000000000..2a555b77e --- /dev/null +++ b/apps/sn/src/entities/menu-authority/index.ts @@ -0,0 +1,2 @@ +export { AuthorityButton } from "./ui/authority-button"; +export { ManagementAuthorityDialog } from "./ui/management-authority-dialog"; diff --git a/apps/sn/src/entities/menu-authority/model/menu-authority-interface.ts b/apps/sn/src/entities/menu-authority/model/menu-authority-interface.ts new file mode 100644 index 000000000..b59574c2f --- /dev/null +++ b/apps/sn/src/entities/menu-authority/model/menu-authority-interface.ts @@ -0,0 +1,48 @@ +import { z } from "zod"; +import type { PageableList, PageableParams } from "@/shared/model"; + +export interface MenuAuthorityParams extends PageableParams { + groupId?: string; + menuCd?: number; + useYn?: string; +} + +export interface PageMenuAuthority { + mngAuthrtYn: string; + inqAuthrtYn: string; + regAuthrtYn: string; + mdfcnAuthrtYn: string; + delAuthrtYn: string; +} + +export const menuAuthoritySchema = z.object({ + groupId: z.string(), + groupNm: z.string(), + menuCd: z.number(), + mngAuthrtYn: z.string(), + inqAuthrtYn: z.string(), + regAuthrtYn: z.string(), + mdfcnAuthrtYn: z.string(), + delAuthrtYn: z.string(), + useYn: z.string(), + mode: z.string(), +}); + +export const menuAuthorityPayloadSchema = menuAuthoritySchema.omit({ + groupNm: true, +}); + +export const menuAuthorityFormSchema = z.object({ + menuAuthority: z.array(menuAuthorityPayloadSchema), +}); + +export type MenuAuthority = z.infer<typeof menuAuthoritySchema>; +export type MenuAuthorityList = PageableList<MenuAuthority>; +export type MenuAuthorityPayload = z.infer<typeof menuAuthorityPayloadSchema>; +export type MenuAuthorityFormData = z.infer<typeof menuAuthorityFormSchema>; + +export interface MenuAuthorityService { + getMenuAuthorityList(params: MenuAuthorityParams): Promise<MenuAuthorityList>; + getPageMenuAuthority(menuCd: number): Promise<PageMenuAuthority[]>; + saveMenuAuthority(data: MenuAuthorityPayload[]): Promise<void>; +} diff --git a/apps/sn/src/entities/menu-authority/model/menu-authority-table-columns.tsx b/apps/sn/src/entities/menu-authority/model/menu-authority-table-columns.tsx new file mode 100644 index 000000000..6969a85e9 --- /dev/null +++ b/apps/sn/src/entities/menu-authority/model/menu-authority-table-columns.tsx @@ -0,0 +1,117 @@ +import type { ColumnDef } from "@tanstack/react-table"; +import type { UseFormReturn } from "react-hook-form"; +import { Checkbox } from "@common/components"; +import type { + MenuAuthority, + MenuAuthorityFormData, + MenuAuthorityPayload, +} from "./menu-authority-interface"; + +export const createColumns = ( + form: UseFormReturn<MenuAuthorityFormData>, +): ColumnDef<MenuAuthority>[] => { + const getCurrentRow = (i: number) => form.watch("menuAuthority")[i]; + + const handleCheckedChange = + (i: number, key: keyof MenuAuthorityPayload) => (checked: boolean) => { + if (getCurrentRow(i)?.mode === "R") { + form.setValue(`menuAuthority.${i}.mode`, "U"); + } + form.setValue(`menuAuthority.${i}.${key}`, checked ? "Y" : "N"); + }; + + return [ + { + accessorKey: "groupNm", + header: "그룹명", + }, + { + accessorKey: "mngAuthrtYn", + header: "관리권한", + cell: ({ row }) => { + return ( + <Checkbox + checked={getCurrentRow(row.index)?.mngAuthrtYn === "Y"} + onCheckedChange={(checked) => { + if (checked) { + handleCheckedChange( + row.index, + "regAuthrtYn", + )(checked as boolean); + handleCheckedChange( + row.index, + "mdfcnAuthrtYn", + )(checked as boolean); + handleCheckedChange( + row.index, + "delAuthrtYn", + )(checked as boolean); + handleCheckedChange( + row.index, + "inqAuthrtYn", + )(checked as boolean); + } + handleCheckedChange(row.index, "mngAuthrtYn")(checked as boolean); + }} + /> + ); + }, + }, + { + accessorKey: "regAuthrtYn", + header: "등록권한", + cell: ({ row }) => { + return ( + <Checkbox + checked={getCurrentRow(row.index)?.regAuthrtYn === "Y"} + onCheckedChange={(checked) => { + if (checked) { + handleCheckedChange( + row.index, + "inqAuthrtYn", + )(checked as boolean); + } + handleCheckedChange(row.index, "regAuthrtYn")(checked as boolean); + }} + /> + ); + }, + }, + { + accessorKey: "mdfcnAuthrtYn", + header: "수정권한 ", + cell: ({ row }) => { + return ( + <Checkbox + checked={getCurrentRow(row.index)?.mdfcnAuthrtYn === "Y"} + onCheckedChange={handleCheckedChange(row.index, "mdfcnAuthrtYn")} + /> + ); + }, + }, + { + accessorKey: "delAuthrtYn", + header: "삭제권한", + cell: ({ row }) => { + return ( + <Checkbox + checked={getCurrentRow(row.index)?.delAuthrtYn === "Y"} + onCheckedChange={handleCheckedChange(row.index, "delAuthrtYn")} + /> + ); + }, + }, + { + accessorKey: "inqAuthrtYn", + header: "조회권한", + cell: ({ row }) => { + return ( + <Checkbox + checked={getCurrentRow(row.index)?.inqAuthrtYn === "Y"} + onCheckedChange={handleCheckedChange(row.index, "inqAuthrtYn")} + /> + ); + }, + }, + ]; +}; diff --git a/apps/sn/src/entities/menu-authority/ui/authority-button.tsx b/apps/sn/src/entities/menu-authority/ui/authority-button.tsx new file mode 100644 index 000000000..257ab6e5d --- /dev/null +++ b/apps/sn/src/entities/menu-authority/ui/authority-button.tsx @@ -0,0 +1,26 @@ +import { useMenuStore } from "@/shared/lib"; +import { Button, cn, type ButtonProps } from "@common/components"; +import type { PageMenuAuthority } from "../model/menu-authority-interface"; + +type AuthorityButtonProps = { + authority: keyof PageMenuAuthority; +} & ButtonProps; + +function AuthorityButton({ + authority, + className, + children, + ...props +}: AuthorityButtonProps) { + const pageMenuAuthority = useMenuStore((state) => state.pageMenuAuthority); + + const isAuthority = pageMenuAuthority?.[authority] === "Y"; + + return ( + <Button className={cn(className, !isAuthority && "hidden")} {...props}> + {children} + </Button> + ); +} + +export { AuthorityButton }; diff --git a/apps/sn/src/entities/menu-authority/ui/management-authority-dialog.tsx b/apps/sn/src/entities/menu-authority/ui/management-authority-dialog.tsx new file mode 100644 index 000000000..4a73cb596 --- /dev/null +++ b/apps/sn/src/entities/menu-authority/ui/management-authority-dialog.tsx @@ -0,0 +1,103 @@ +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; +import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Button, + DataTable, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@common/components"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; +import { + menuAuthorityFormSchema, + type MenuAuthorityFormData, + type MenuAuthorityPayload, +} from "../model/menu-authority-interface"; +import { createColumns } from "../model/menu-authority-table-columns"; +import { MenuAuthorityApi } from "../api/menu-authority-service"; + +type ManagementAuthorityDialogProps = { + menuCd?: number; +} & React.ComponentProps<typeof Dialog>; + +function ManagementAuthorityDialog({ + menuCd, + ...props +}: ManagementAuthorityDialogProps) { + const { confirm } = useConfirm(); + const alert = useAlertStore((state) => state.setMessage); + + const { data: menuAuthorityList } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.MENU_AUTHRTS, menuCd], + queryFn: () => MenuAuthorityApi.getMenuAuthorityList({ menuCd }), + enabled: !!menuCd, + }); + + const { mutateAsync: saveMenuAuthority } = useMutationWrapper({ + mutationFn: (data: MenuAuthorityPayload[]) => + MenuAuthorityApi.saveMenuAuthority(data), + onSuccess: () => { + alert("저장되었습니다."); + }, + }); + + const form = useForm<MenuAuthorityFormData>({ + resolver: zodResolver(menuAuthorityFormSchema), + values: { + menuAuthority: menuAuthorityList?.content ?? [], + }, + }); + + const table = useReactTable({ + data: menuAuthorityList?.content ?? [], + columns: createColumns(form), + getCoreRowModel: getCoreRowModel(), + }); + + const handleSave = async () => { + if (await confirm("저장하시겠습니까?")) { + const data = form.getValues("menuAuthority"); + const filteredData = data.filter((item) => item.mode !== "R"); + await saveMenuAuthority(filteredData); + } + }; + + return ( + <Dialog {...props}> + <DialogTrigger asChild> + <Button className="gap-3" disabled={!menuCd}> + <i className="diveicon di-personal-information" /> + 권한관리 + </Button> + </DialogTrigger> + <DialogContent className="max-w-[60rem]" aria-describedby={undefined}> + <DialogHeader> + <DialogTitle>그룹별 메뉴 권한</DialogTitle> + </DialogHeader> + <div className="max-h-[40rem] overflow-auto"> + <DataTable table={table} /> + </div> + <DialogFooter className="!justify-center"> + <Button size="lg" onClick={handleSave}> + 저장 + </Button> + <DialogClose asChild> + <Button size="lg" color="white"> + 취소 + </Button> + </DialogClose> + </DialogFooter> + </DialogContent> + </Dialog> + ); +} + +export { ManagementAuthorityDialog }; diff --git a/apps/sn/src/entities/menu/api/menu-service.ts b/apps/sn/src/entities/menu/api/menu-service.ts new file mode 100644 index 000000000..4424f8d37 --- /dev/null +++ b/apps/sn/src/entities/menu/api/menu-service.ts @@ -0,0 +1,14 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { MenuItem, MenuService } from "../model/menu-interface"; + +export class MenuApiService implements MenuService { + async getMenuRoles(siteId: string): Promise<MenuItem[]> { + const response = await client.get<MenuItem[]>( + `${ENDPOINT.CMS_SERVICE.MENU_ROLES}/${siteId}`, + ); + return response.data; + } +} + +export const MenuApi = new MenuApiService(); diff --git a/apps/sn/src/entities/menu/index.ts b/apps/sn/src/entities/menu/index.ts new file mode 100644 index 000000000..04f63e38b --- /dev/null +++ b/apps/sn/src/entities/menu/index.ts @@ -0,0 +1,2 @@ +export { MenuWrapper } from "./lib/menu-wrapper"; +export type { MenuItem } from "./model/menu-interface"; diff --git a/apps/sn/src/entities/menu/lib/menu-wrapper.tsx b/apps/sn/src/entities/menu/lib/menu-wrapper.tsx new file mode 100644 index 000000000..79a29a09c --- /dev/null +++ b/apps/sn/src/entities/menu/lib/menu-wrapper.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { useEffect } from "react"; +import { usePathname, useRouter } from "next/navigation"; +import { MenuAuthorityApi } from "@/entities/menu-authority/@x/menu"; +import { useAlertStore, useMenuStore } from "@/shared/lib"; +import { getCookie } from "@/shared/lib";; +import { SITE_ID } from "@/shared/config"; +import { MenuApi } from "../api/menu-service"; +import { getCurrentMenu } from "./utils"; + +const MenuWrapper = ({ children }: { children: React.ReactNode }) => { + const router = useRouter(); + const pathname = usePathname(); + + const alert = useAlertStore((state) => state.setMessage); + const { pageMenuAuthority, setCurrentMenuInfo, setPageMenuAuthority } = + useMenuStore((state) => state); + + const getCurrentMenuInfo = async () => { + const menuRoles = await MenuApi.getMenuRoles(SITE_ID); + if (!menuRoles) return; + return getCurrentMenu(menuRoles, pathname ?? ""); + }; + + const getPageMenuAuthority = async () => { + const menuCd = getCookie("menu-code-key"); + if (!menuCd) return; + const response = await MenuAuthorityApi.getPageMenuAuthority( + parseInt(menuCd), + ); + return response; + }; + + const checkInquiryAuthority = () => { + return pageMenuAuthority?.inqAuthrtYn === "Y"; + }; + + useEffect(() => { + (async () => { + const currentMenu = await getCurrentMenuInfo(); + if (currentMenu) setCurrentMenuInfo(currentMenu); + + const response = await getPageMenuAuthority(); + if (response?.length) setPageMenuAuthority(response[0]!); + })(); + }, []); + + useEffect(() => { + if (!pageMenuAuthority) return; + const inquiryAuthority = checkInquiryAuthority(); + if (!inquiryAuthority) + alert("페이지에 대한 조회 권한이 없습니다.", () => router.push("/")); + }, [pageMenuAuthority]); + + return <>{children}</>; +}; + +export { MenuWrapper }; diff --git a/apps/sn/src/entities/menu/lib/utils.ts b/apps/sn/src/entities/menu/lib/utils.ts new file mode 100644 index 000000000..1cf4514d9 --- /dev/null +++ b/apps/sn/src/entities/menu/lib/utils.ts @@ -0,0 +1,20 @@ +import type { MenuItem } from "../model/menu-interface"; + +export const getCurrentMenu = ( + menus: MenuItem[], + pathname: string, +): MenuItem | null => { + for (const menu of menus) { + if ( + menu.urlPath !== "" && + (menu.urlPath === pathname || pathname.startsWith(menu.urlPath)) + ) + return menu; + + if (menu.children) { + const children = getCurrentMenu(menu.children, pathname); + if (children) return children; + } + } + return null; +}; diff --git a/apps/sn/src/entities/menu/model/menu-interface.ts b/apps/sn/src/entities/menu/model/menu-interface.ts new file mode 100644 index 000000000..12c7d007a --- /dev/null +++ b/apps/sn/src/entities/menu/model/menu-interface.ts @@ -0,0 +1,23 @@ +export interface MenuItem { + menuCd: number; + menuKornNm: string; + menuEngNm: string; + parentId: number | null; + sortSeq: number; + iconNm?: string; + children: MenuItem[] | null; + levelNo: number; + urlPath: string; + menuLnkgTypeCd: string; + menuLnkgSn: number; + menuPath: string; + qrcd: string; + useYn: string; + otptYn: string; + newWndwYn: string; + requiredUrlPath: boolean; +} + +export interface MenuService { + getMenuRoles(siteId: string): Promise<MenuItem[]>; +} diff --git a/apps/sn/src/shared/api/client.ts b/apps/sn/src/shared/api/client.ts new file mode 100644 index 000000000..cf37e6671 --- /dev/null +++ b/apps/sn/src/shared/api/client.ts @@ -0,0 +1,11 @@ +import { Client } from "@common/network"; +import { BASE_URL } from "../config/api"; +import { SITE_ID } from "../config/env"; +import { setCookie } from "../lib"; + +setCookie("site-id-key", SITE_ID); + +export const client = Client.getInstance({ + baseURL: BASE_URL, + siteId: SITE_ID, +}); diff --git a/apps/sn/src/shared/api/index.ts b/apps/sn/src/shared/api/index.ts new file mode 100644 index 000000000..b7b9d6121 --- /dev/null +++ b/apps/sn/src/shared/api/index.ts @@ -0,0 +1 @@ +export { client } from "./client"; diff --git a/apps/sn/src/shared/config/api.ts b/apps/sn/src/shared/config/api.ts new file mode 100644 index 000000000..3eba9fa91 --- /dev/null +++ b/apps/sn/src/shared/config/api.ts @@ -0,0 +1,56 @@ +import { PROXY_HOST } from "./env"; + +export const BASE_URL = `${PROXY_HOST}/server`; + +export const ENDPOINT = { + ATTACH_SERVICE: { + TUS_UPLOAD: "/attach-service/api/v1/tus/upload", + EDITOR_UPLOAD: "/attach-service/api/v1/upload/editor", + IMAGES: "/attach-service/api/v1/images", + MOVIE_THUMBNAIL: "/attach-service/api/v1/movie/thumbnail", + ATTACHMENTS: "/attach-service/api/v1/atfl/attachments", + DOWNLOAD: "/attach-service/api/v1/atflcd/download", + STREAM_VIDEO: "/attach-service/api/v1/stream/video", + }, + USER_SERVICE: { + LOGIN: "/user-service/login", + USERS: "/user-service/api/v1/users", + SILENT_REFRESH: "/user-service/api/v1/users/token/refresh", + AUTHORIZATIONS: "/user-service/api/v1/authorizations", + ROLE_AUTHORIZATIONS: "/user-service/api/v1/role-authorizations", + ROLES: "/user-service/api/v1/roles", + }, + CMS_SERVICE: { + SITES: "/cms-service/api/v1/sites", + IPS: "/cms-service/api/v1/site-ips", + DOMAINS: "/cms-service/api/v1/site-dmns", + POSTINGS: "/cms-service/api/v1/site-pstgs", + CODE: "/cms-service/api/v1/code/cache", + MENUS: "/cms-service/api/v1/menus", + MENU_ROLES: "/cms-service/api/v1/menu-roles", + MENU_AUTHRTS: "/cms-service/api/v1/menu-authrts", + CONTENTS: "/cms-service/api/v1/contents", + CONTENTS_DETAILS: "/cms-service/api/v1/contents-details", + ORGANIZATIONS: "/cms-service/api/v1/ognzs", + USER_ORGANIZATION: "/cms-service/api/v1/user-ognz", + IMAGES: "/cms-service/api/v1/images", + BANNERS: "/cms-service/api/v1/bnrs", + TEMPLATE: "/cms-service/api/v1/tmplt", + TERMS: "/cms-service/api/v1/trms", + LWPO_CODE: "/cms-service/api/v1/lwpo-cds", + UP_CODE: "/cms-service/api/v1/up-codes", + POPUPS: "/cms-service/api/v1/popups", + SCHEDULES: "/cms-service/api/v1/schdls", + SURVEY: "/cms-service/api/v1/srvy", + SURVEY_QUESTION: "/cms-service/api/v1/srvy-qitem", + SURVEY_ARTICLE: "/cms-service/api/v1/srvy-artcl", + }, + BOARD_SERVICE: { + BBS: "/board-service/api/v1/bbs", + BBS_TEMPLATES: "/board-service/api/v1/bbsTmplt", + POSTS: "/board-service/api/v1/posts", + POSTS_COMMENTS: "/board-service/api/v1/posts-cmnts", + }, +}; + +export const JWT_EXPIRATION_TIME = 1800000; diff --git a/apps/sn/src/shared/config/env.ts b/apps/sn/src/shared/config/env.ts new file mode 100644 index 000000000..ad982d727 --- /dev/null +++ b/apps/sn/src/shared/config/env.ts @@ -0,0 +1,8 @@ +export const PORT = process.env.NEXT_PUBLIC_PORT || "3000"; +export const PROXY_HOST = process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`; +export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL; +export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "SN"; +export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; +export const ACCESS_TOKEN = process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token"; +export const REFRESH_TOKEN = process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token"; +export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; diff --git a/apps/sn/src/shared/config/index.ts b/apps/sn/src/shared/config/index.ts new file mode 100644 index 000000000..cd83447d5 --- /dev/null +++ b/apps/sn/src/shared/config/index.ts @@ -0,0 +1,2 @@ +export * from "./api"; +export * from "./env"; diff --git a/apps/sn/app/components/lib/utils/cookie.tsx b/apps/sn/src/shared/lib/cookie.ts similarity index 100% rename from apps/sn/app/components/lib/utils/cookie.tsx rename to apps/sn/src/shared/lib/cookie.ts diff --git a/apps/sn/src/shared/lib/index.ts b/apps/sn/src/shared/lib/index.ts new file mode 100644 index 000000000..b8b4c52c0 --- /dev/null +++ b/apps/sn/src/shared/lib/index.ts @@ -0,0 +1,6 @@ +export { useMutationWrapper } from "./use-mutation-wrapper"; +export { useAlertStore } from "./use-alert-store"; +export { useMenuStore } from "./use-menu-store"; +export { useUserStore } from "./use-user-store"; +export { usePrompt } from "./use-prompt"; +export { getCookie, setCookie, removeCookie } from "./cookie"; diff --git a/apps/sn/src/shared/lib/use-alert-store.ts b/apps/sn/src/shared/lib/use-alert-store.ts new file mode 100644 index 000000000..c21a00ed3 --- /dev/null +++ b/apps/sn/src/shared/lib/use-alert-store.ts @@ -0,0 +1,14 @@ +import { useStore } from "zustand"; +import { useContext } from "react"; +import type { AlertStore } from "../store/alert-store"; +import { StoreContext } from "../providers/store-provider"; + +export const useAlertStore = <T>(selector: (store: AlertStore) => T): T => { + const storeContext = useContext(StoreContext); + + if (!storeContext) { + throw new Error("useAlertStore must be used within StoreProvider"); + } + + return useStore(storeContext.alertStore, selector); +}; diff --git a/apps/sn/src/shared/lib/use-menu-store.ts b/apps/sn/src/shared/lib/use-menu-store.ts new file mode 100644 index 000000000..66e41c76c --- /dev/null +++ b/apps/sn/src/shared/lib/use-menu-store.ts @@ -0,0 +1,14 @@ +import { useStore } from "zustand"; +import { useContext } from "react"; +import type { MenuStore } from "../store/menu-store"; +import { StoreContext } from "../providers/store-provider"; + +export const useMenuStore = <T>(selector: (store: MenuStore) => T): T => { + const storeContext = useContext(StoreContext); + + if (!storeContext) { + throw new Error("useMenuStore must be used within StoreProvider"); + } + + return useStore(storeContext.menuStore, selector); +}; diff --git a/apps/sn/src/shared/lib/use-mutation-wrapper.tsx b/apps/sn/src/shared/lib/use-mutation-wrapper.tsx new file mode 100644 index 000000000..ce119d2a7 --- /dev/null +++ b/apps/sn/src/shared/lib/use-mutation-wrapper.tsx @@ -0,0 +1,31 @@ +import { AxiosError } from "axios"; +import { useMutation, type UseMutationOptions } from "@tanstack/react-query"; +import { useAlertStore } from "./use-alert-store"; + +export const useMutationWrapper = < + TData = unknown, + TError = unknown, + TVariables = void, + TContext = unknown, +>( + mutationOptions: UseMutationOptions<TData, TError, TVariables, TContext>, +) => { + const alert = useAlertStore((state) => state.setMessage); + return useMutation({ + ...mutationOptions, + onError: (error) => { + let errorMessage = ""; + + if (error instanceof AxiosError) { + if (error.response?.status) + errorMessage += `status: ${error.response.status}\n`; + if (error.response?.data?.message) + errorMessage += `${error.response.data.message}\n`; + if (error.response?.data?.errors[0]?.message) + errorMessage += `${error.response.data.errors[0].message}\n`; + } + + alert(`[Error] ${errorMessage}`); + }, + }); +}; diff --git a/apps/sn/src/shared/lib/use-prompt.tsx b/apps/sn/src/shared/lib/use-prompt.tsx new file mode 100644 index 000000000..f36858eb0 --- /dev/null +++ b/apps/sn/src/shared/lib/use-prompt.tsx @@ -0,0 +1,26 @@ +import { useContext } from "react"; +import { PromptContext } from "../providers/prompt-provider"; + +export const usePrompt = () => { + const { message, resolve, setMessage, setResolve } = + useContext(PromptContext); + + const prompt = (message: string) => { + setMessage(message); + return new Promise<{ resolved: boolean; value?: string }>((resolve) => { + setResolve(() => resolve); + }); + }; + + const onSubmit = (value: string) => { + resolve?.({ resolved: true, value }); + setMessage(""); + }; + + const onCancel = () => { + resolve?.({ resolved: false }); + setMessage(""); + }; + + return { message, isOpen: !!message, prompt, onSubmit, onCancel }; +}; diff --git a/apps/sn/src/shared/lib/use-user-store.ts b/apps/sn/src/shared/lib/use-user-store.ts new file mode 100644 index 000000000..748caed4c --- /dev/null +++ b/apps/sn/src/shared/lib/use-user-store.ts @@ -0,0 +1,14 @@ +import { useStore } from "zustand"; +import { useContext } from "react"; +import type { UserStore } from "../store/user-store"; +import { StoreContext } from "../providers/store-provider"; + +export const useUserStore = <T>(selector: (store: UserStore) => T): T => { + const storeContext = useContext(StoreContext); + + if (!storeContext) { + throw new Error("useUserStore must be used within StoreProvider"); + } + + return useStore(storeContext.userStore, selector); +}; diff --git a/apps/sn/src/shared/providers/index.ts b/apps/sn/src/shared/providers/index.ts new file mode 100644 index 000000000..fa8320fe7 --- /dev/null +++ b/apps/sn/src/shared/providers/index.ts @@ -0,0 +1,2 @@ +export { StoreProvider } from "./store-provider"; +export { PromptProvider } from "./prompt-provider"; diff --git a/apps/sn/src/shared/providers/prompt-provider.tsx b/apps/sn/src/shared/providers/prompt-provider.tsx new file mode 100644 index 000000000..10338c338 --- /dev/null +++ b/apps/sn/src/shared/providers/prompt-provider.tsx @@ -0,0 +1,37 @@ +import { createContext, useState } from "react"; + +interface PromptContext { + message?: string; + resolve?: ({ + resolved, + value, + }: { + resolved: boolean; + value?: string; + }) => void; + setMessage: (message: string) => void; + setResolve: (resolve: () => void) => void; +} + +export const PromptContext = createContext<PromptContext>({ + message: "", + resolve() {}, + setMessage() {}, + setResolve() {}, +}); + +export const PromptProvider = ({ children }: { children: React.ReactNode }) => { + const [message, setMessage] = useState<string>(); + const [resolve, setResolve] = + useState< + ({ resolved, value }: { resolved: boolean; value?: string }) => void + >(); + + return ( + <PromptContext.Provider + value={{ message, resolve, setMessage, setResolve }} + > + {children} + </PromptContext.Provider> + ); +}; diff --git a/apps/sn/app/store/store-provider.tsx b/apps/sn/src/shared/providers/store-provider.tsx similarity index 60% rename from apps/sn/app/store/store-provider.tsx rename to apps/sn/src/shared/providers/store-provider.tsx index 8a2e8347e..a0ec17b61 100644 --- a/apps/sn/app/store/store-provider.tsx +++ b/apps/sn/src/shared/providers/store-provider.tsx @@ -1,9 +1,17 @@ "use client"; import { createContext, useRef } from "react"; -import { createUserStore } from "../store/user-store"; +import { + createAlertStore, + createAttachmentStore, + createMenuStore, + createUserStore, +} from "../store"; type StoreContextType = { + alertStore: ReturnType<typeof createAlertStore>; + attachmentStore: ReturnType<typeof createAttachmentStore>; + menuStore: ReturnType<typeof createMenuStore>; userStore: ReturnType<typeof createUserStore>; }; @@ -15,6 +23,9 @@ export const StoreProvider = ({ children }: { children: React.ReactNode }) => { const storeRef = useRef<StoreContextType | null>(null); if (storeRef.current === null) { storeRef.current = { + alertStore: createAlertStore(), + attachmentStore: createAttachmentStore(), + menuStore: createMenuStore(), userStore: createUserStore(), }; } diff --git a/apps/sn/src/shared/store/alert-store.ts b/apps/sn/src/shared/store/alert-store.ts new file mode 100644 index 000000000..3bba65848 --- /dev/null +++ b/apps/sn/src/shared/store/alert-store.ts @@ -0,0 +1,25 @@ +import { createStore } from "zustand/vanilla"; + +export type AlertState = { + message: string; + callback?: () => void; +}; + +export type AlertActions = { + setMessage: (message: string, callback?: () => void) => void; +}; + +export type AlertStore = AlertState & AlertActions; + +export const defaultInitState: AlertState = { + message: "", + callback: undefined, +}; + +export const createAlertStore = (initState: AlertState = defaultInitState) => { + return createStore<AlertStore>()((set) => ({ + ...initState, + setMessage: (message: string, callback?: () => void) => + set(() => ({ message, callback })), + })); +}; diff --git a/apps/sn/src/shared/store/attachment-store.ts b/apps/sn/src/shared/store/attachment-store.ts new file mode 100644 index 000000000..faa700949 --- /dev/null +++ b/apps/sn/src/shared/store/attachment-store.ts @@ -0,0 +1,25 @@ +import { createStore } from "zustand/vanilla"; + +export type AttachmentState = { + attachmentFileCode: string; +}; + +export type AttachmentActions = { + setAttachmentFileCode: (attachmentFileCode: string) => void; +}; + +export type AttachmentStore = AttachmentState & AttachmentActions; + +export const defaultInitState: AttachmentState = { + attachmentFileCode: "", +}; + +export const createAttachmentStore = ( + initState: AttachmentState = defaultInitState, +) => { + return createStore<AttachmentStore>()((set) => ({ + ...initState, + setAttachmentFileCode: (attachmentFileCode: string) => + set({ attachmentFileCode }), + })); +}; diff --git a/apps/sn/src/shared/store/index.ts b/apps/sn/src/shared/store/index.ts new file mode 100644 index 000000000..a41d6f928 --- /dev/null +++ b/apps/sn/src/shared/store/index.ts @@ -0,0 +1,4 @@ +export { createAlertStore } from "./alert-store"; +export { createAttachmentStore } from "./attachment-store"; +export { createMenuStore, type BbsType } from "./menu-store"; +export { createUserStore, type IUserInfo } from "./user-store"; diff --git a/apps/sn/src/shared/store/menu-store.ts b/apps/sn/src/shared/store/menu-store.ts new file mode 100644 index 000000000..8536011ee --- /dev/null +++ b/apps/sn/src/shared/store/menu-store.ts @@ -0,0 +1,93 @@ +import { createStore } from "zustand/vanilla"; + +export const BBS_TYPE = { + BBSNOTICE: "BBSNOTICE", + BBSNORMAL: "BBSNORMAL", + BBSFAQ: "BBSFAQ", + BBSIMAGE: "BBSIMAGE", + BBSMOVIE: "BBSMOVIE", +} as const; + +export type BbsType = (typeof BBS_TYPE)[keyof typeof BBS_TYPE]; + +export interface IBbs { + bbsId: number; + bbsTypeCd: BbsType; + bbsTypeNm: string; + bbsNm: string; + bbsExpln: string; + rfrncUseYn: string; + atflUseYn: string; + atflMaxCnt: number; + atflMaxFileSz: number; + ansUseYn: string; + ansUseAuthrt: string; + cmntUseYn: string; + dgstfnEvlUseYn: string; + smsUseYn: string; + upndFixUseYn: string; + pstgPrdUseYn: string; + popupUseYn: string; + delYn: string; + useYn: string; + mdfcnDt: string; + mdfrNm: string; +} + +interface IMenu { + menuCd: number; + menuKornNm: string; + menuEngNm: string; + parentId: number | null; + sortSeq: number; + iconNm?: string; + children: IMenu[] | null; + levelNo: number; + urlPath: string; + menuLnkgTypeCd: string; + menuLnkgSn: number; + menuPath: string; + qrcd: string; + useYn: string; + otptYn: string; + newWndwYn: string; + requiredUrlPath: boolean; +} + +interface IAuthority { + mngAuthrtYn: string; + inqAuthrtYn: string; + regAuthrtYn: string; + mdfcnAuthrtYn: string; + delAuthrtYn: string; +} + +export type MenuState = { + bbsInfo: IBbs | null; + currentMenuInfo: IMenu | null; + pageMenuAuthority: IAuthority | null; +}; + +export type MenuActions = { + setBbsInfo: (bbs: IBbs) => void; + setCurrentMenuInfo: (menu: IMenu) => void; + setPageMenuAuthority: (authority: IAuthority) => void; +}; + +export type MenuStore = MenuState & MenuActions; + +export const defaultInitState: MenuState = { + currentMenuInfo: null, + bbsInfo: null, + pageMenuAuthority: null, +}; + +export const createMenuStore = (initState: MenuState = defaultInitState) => { + return createStore<MenuStore>()((set) => ({ + ...initState, + setBbsInfo: (bbs: IBbs) => set({ bbsInfo: bbs }), + setCurrentMenuInfo: (menu: IMenu) => set({ currentMenuInfo: menu }), + setPageMenuAuthority: (authority: IAuthority) => + set({ pageMenuAuthority: authority }), + })); +}; diff --git a/apps/sn/src/shared/store/user-store.ts b/apps/sn/src/shared/store/user-store.ts new file mode 100644 index 000000000..65d897cf1 --- /dev/null +++ b/apps/sn/src/shared/store/user-store.ts @@ -0,0 +1,57 @@ +import { createStore } from "zustand/vanilla"; + +export interface IUserInfo { + regDt: string; + mdfcnDt: string; + rgtr: string; + mdfr: string; + adminRoleYn: string; + userUnqId: string; + userId: string; + userName: string; + email: string; + encryptedPassword: string; + refreshToken: string; + userStateCode: string; + lastLoginDate: string; + loginFailCount: number; + pswdHnt: string; + pswdAns: string; + mphn: string; + telno: string; + zip: string; + addr: string; + daddr: string; + laeSeCd: string; + ogdpSeCd: string; + aprvYn: string; + autzrId: string; + expryYmd: string; + lastPswdChgDt: string; + googleId: string; + kakaoId: string; + naverId: string; + isSocialUser: boolean; + hasPassword: boolean; +} + +export type UserState = { + userInfo: IUserInfo | null; +}; + +export type UserActions = { + setUserInfo: (userInfo: IUserInfo | null) => void; +}; + +export type UserStore = UserState & UserActions; + +export const defaultInitState: UserState = { + userInfo: null, +}; + +export const createUserStore = (initState: UserState = defaultInitState) => { + return createStore<UserStore>()((set) => ({ + ...initState, + setUserInfo: (userInfo: IUserInfo | null) => set(() => ({ userInfo })), + })); +}; diff --git a/apps/sn/src/shared/ui/alert-dialog.tsx b/apps/sn/src/shared/ui/alert-dialog.tsx new file mode 100644 index 000000000..6160d5301 --- /dev/null +++ b/apps/sn/src/shared/ui/alert-dialog.tsx @@ -0,0 +1,34 @@ +import { + Dialog, + DialogContent, + DialogFooter, + DialogClose, + Button, +} from "@common/components"; +import { useAlertStore } from "../lib/use-alert-store"; + +function AlertDialog() { + const { message, callback, setMessage } = useAlertStore((state) => state); + const handleOpenChange = () => { + setMessage(""); + callback?.(); + }; + return ( + <Dialog open={!!message} onOpenChange={handleOpenChange}> + <DialogContent aria-describedby={undefined}> + <div className="py-10"> + <p className="whitespace-pre-line text-center text-[1.4rem] text-foreground"> + {message} + </p> + </div> + <DialogFooter className="!justify-center"> + <DialogClose asChild> + <Button>확인</Button> + </DialogClose> + </DialogFooter> + </DialogContent> + </Dialog> + ); +} + +export { AlertDialog }; diff --git a/apps/sn/src/shared/ui/confirm-dialog.tsx b/apps/sn/src/shared/ui/confirm-dialog.tsx new file mode 100644 index 000000000..6a4996bfb --- /dev/null +++ b/apps/sn/src/shared/ui/confirm-dialog.tsx @@ -0,0 +1,48 @@ +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogClose, + Button, + type ButtonProps, +} from "@common/components"; + +type ConfirmDialogProps = { + isOpen?: boolean; + confirmButtonProps?: ButtonProps & { label?: string }; + cancelButtonProps?: ButtonProps & { label?: string }; +}; + +function ConfirmDialog({ + isOpen, + confirmButtonProps, + cancelButtonProps, +}: ConfirmDialogProps) { + const { message, isOpen: isGlobalOpen, onConfirm, onCancel } = useConfirm(); + return ( + <Dialog open={isOpen ?? isGlobalOpen} onOpenChange={onCancel}> + <DialogContent aria-describedby={undefined}> + <div className="py-10"> + <p className="whitespace-pre-line text-center text-[1.4rem] text-foreground"> + {message} + </p> + </div> + <DialogFooter className="!justify-center"> + <DialogClose asChild> + <Button onClick={onConfirm} {...confirmButtonProps}> + {confirmButtonProps?.label ?? "확인"} + </Button> + </DialogClose> + <DialogClose asChild> + <Button onClick={onCancel} color="white" {...cancelButtonProps}> + {cancelButtonProps?.label ?? "취소"} + </Button> + </DialogClose> + </DialogFooter> + </DialogContent> + </Dialog> + ); +} + +export { ConfirmDialog }; diff --git a/apps/sn/src/shared/ui/map-layout.tsx b/apps/sn/src/shared/ui/map-layout.tsx new file mode 100644 index 000000000..e05666401 --- /dev/null +++ b/apps/sn/src/shared/ui/map-layout.tsx @@ -0,0 +1,13 @@ +import { useMenuStore } from "../lib/use-menu-store"; + +function MapLayout({ children }: { children: React.ReactNode }) { + const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo); + + return ( + <div className="h-auto min-h-full bg-container pb-2"> + {children} + </div> + ); +} + +export { MapLayout }; diff --git a/apps/sn/src/shared/ui/page-layout.tsx b/apps/sn/src/shared/ui/page-layout.tsx new file mode 100644 index 000000000..358d3b5db --- /dev/null +++ b/apps/sn/src/shared/ui/page-layout.tsx @@ -0,0 +1,22 @@ +import { PageTitle } from "@common/components"; +import { useMenuStore } from "../lib/use-menu-store"; + +function PageLayout({ children }: { children: React.ReactNode }) { + const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo); + + const menuPaths = currentMenuInfo?.menuPath + ?.split(">") + .map((path) => path.trim()); + + return ( + <div className="h-auto min-h-full bg-container pb-2"> + <PageTitle + title={currentMenuInfo?.menuKornNm ?? ""} + menuPaths={menuPaths ?? []} + /> + {children} + </div> + ); +} + +export { PageLayout }; diff --git a/apps/sn/src/shared/ui/prompt-dialog.tsx b/apps/sn/src/shared/ui/prompt-dialog.tsx new file mode 100644 index 000000000..54c2f3fcc --- /dev/null +++ b/apps/sn/src/shared/ui/prompt-dialog.tsx @@ -0,0 +1,45 @@ +import { useState } from "react"; +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogClose, + Input, +} from "@common/components"; +import { usePrompt } from "@/shared/lib/use-prompt"; + +function PromptDialog() { + const { message, isOpen, onSubmit, onCancel } = usePrompt(); + const [value, setValue] = useState(""); + + return ( + <Dialog open={isOpen} onOpenChange={onCancel}> + <DialogContent aria-describedby={undefined}> + <div className="flex flex-col gap-6 pb-4 pt-8"> + <p className="whitespace-pre-line text-center text-[1.4rem] text-foreground"> + {message} + </p> + <Input + type="text" + value={value} + className="!text-[1.3rem]" + onChange={(e) => setValue(e.target.value)} + /> + </div> + <DialogFooter className="!justify-center"> + <DialogClose asChild> + <Button onClick={() => onSubmit(value)}>확인</Button> + </DialogClose> + <DialogClose asChild> + <Button onClick={onCancel} color="white"> + 취소 + </Button> + </DialogClose> + </DialogFooter> + </DialogContent> + </Dialog> + ); +} + +export { PromptDialog }; diff --git a/apps/sn/tsconfig.json b/apps/sn/tsconfig.json index a65fab364..078e6ae2b 100644 --- a/apps/sn/tsconfig.json +++ b/apps/sn/tsconfig.json @@ -22,19 +22,17 @@ "name": "next" } ], - "baseUrl": "app", + "baseUrl": "./src", "paths": { - "@/*": ["./*"], - "@common/components/*": ["../../../packages/common/components/src/*"], + "@/*": ["*"], "@common/components": ["../../../packages/common/components/src"], - "@common/business_components/*": ["../../../packages/common/business_components/src/*"], + "@common/components/*": ["../../../packages/common/components/src/*"], "@common/business_components": ["../../../packages/common/business_components/src"], - "@common/network/*": ["../../../packages/common/network/*"], + "@common/business_components/*": ["../../../packages/common/business_components/src/*"], "@common/network": ["../../../packages/common/network/"], - "@store": ["./store"], - "@service": ["./service"], - "@utils": ["./components/lib/utils"], - "@common/config": ["../../../packages/common/config/dist/"] + "@common/config": ["../../../packages/common/config/dist/"], + "@utils": ["../app/components/lib/utils"], + "@/components/*": ["../app/components/*"] } }, "include": [ diff --git a/apps/wr/.env.development b/apps/wr/.env.development new file mode 100644 index 000000000..00673e11b --- /dev/null +++ b/apps/wr/.env.development @@ -0,0 +1,28 @@ +# NEXT_PUBLIC_SITE_ID = WR +# NEXT_PUBLIC_PORT = 11301 +# NEXT_PUBLIC_PROXY_HOST = http://localhost:3001 +# NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 +# NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 +# NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 +# NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload +# NEXT_PUBLIC_CLAIM_NAME = Authorization +# NEXT_PUBLIC_AUTH_USER_ID = token-id +# NEXT_PUBLIC_ACCESS_TOKEN = access-token +# NEXT_PUBLIC_REFRESH_TOKEN = refresh-token + +NEXT_PUBLIC_SITE_ID = WR +NEXT_PUBLIC_PORT = 11301 +NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11301 + NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 +NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload +NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver +NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver +NEXT_PUBLIC_CLAIM_NAME = Authorization +NEXT_PUBLIC_AUTH_USER_ID = token-id +NEXT_PUBLIC_ACCESS_TOKEN = access-token +NEXT_PUBLIC_REFRESH_TOKEN = refresh-token + +NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API diff --git a/apps/wr/.env.local b/apps/wr/.env.local new file mode 100644 index 000000000..2543a96ad --- /dev/null +++ b/apps/wr/.env.local @@ -0,0 +1,20 @@ +NEXT_PUBLIC_SITE_ID = WR +NEXT_PUBLIC_PORT = 3001 +NEXT_PUBLIC_PROXY_HOST = http://localhost:3001 +NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 +NEXT_PUBLIC_SERVER_API_URL=http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload +NEXT_PUBLIC_CLAIM_NAME = Authorization +NEXT_PUBLIC_AUTH_USER_ID = token-id +NEXT_PUBLIC_ACCESS_TOKEN = access-token +NEXT_PUBLIC_REFRESH_TOKEN = refresh-token + + + +NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API +#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성 +#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정 + + diff --git a/apps/wr/.env.production b/apps/wr/.env.production new file mode 100644 index 000000000..97342cf20 --- /dev/null +++ b/apps/wr/.env.production @@ -0,0 +1,39 @@ +NEXT_PUBLIC_SITE_ID = WR +NEXT_PUBLIC_PORT = 3000 +NEXT_PUBLIC_PROXY_HOST = http://wr.hasuon.or.kr +NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/api/v1 +NEXT_PUBLIC_FO_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/operation-service/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000 +NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload +NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver +NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl +NEXT_PUBLIC_CLAIM_NAME = Authorization +NEXT_PUBLIC_AUTH_USER_ID = token-id +NEXT_PUBLIC_ACCESS_TOKEN = access-token +NEXT_PUBLIC_REFRESH_TOKEN = refresh-token + +NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/sewerage-service/api + + + + + + + + +# NEXT_PUBLIC_SITE_ID = WR +# NEXT_PUBLIC_PORT = 11301 +# NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11301 +# # NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 +# NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 +# NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 +# NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 +# NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload +# NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver +# NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver +# NEXT_PUBLIC_CLAIM_NAME = Authorization +# NEXT_PUBLIC_AUTH_USER_ID = token-id +# NEXT_PUBLIC_ACCESS_TOKEN = access-token +# NEXT_PUBLIC_REFRESH_TOKEN = refresh-token + +# NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API diff --git a/apps/wr/.gitignore b/apps/wr/.gitignore index 5ef6a5207..8c5b2ee3a 100644 --- a/apps/wr/.gitignore +++ b/apps/wr/.gitignore @@ -31,7 +31,7 @@ yarn-error.log* .pnpm-debug.log* # env files (can opt-in for committing if needed) -.env* +#.env* # vercel .vercel diff --git a/apps/wr/app/base.tsx b/apps/wr/app/base.tsx new file mode 100644 index 000000000..34452e20b --- /dev/null +++ b/apps/wr/app/base.tsx @@ -0,0 +1,25 @@ +"use client"; +import { BaseLayout } from "@common/components/ui"; +import { useWrStore } from "./store"; +import { MainDiv } from "@common/business_components/ui"; +import type { MenuItem } from "./store/common/type/menu"; +import { usePathname, useRouter } from "next/navigation"; +export default function Base({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + const { getMenuAuthrts } = useWrStore((state) => state); + const router = useRouter(); + const pathname = usePathname(); + + const changeMenu = (path: MenuItem) => { + getMenuAuthrts(path.menuCd); + router.push(path.urlPath); + }; + return ( + <BaseLayout currentPath={pathname ?? ""} handleMenuClick={changeMenu}> + <MainDiv>{children}</MainDiv> + </BaseLayout> + ); +} diff --git a/apps/wr/app/layout.tsx b/apps/wr/app/layout.tsx index cb930d6f7..add988817 100644 --- a/apps/wr/app/layout.tsx +++ b/apps/wr/app/layout.tsx @@ -1,24 +1,36 @@ -'use client' -import { BaseLayout } from "@common/components"; +"use client"; import "./globals.css"; -import { StoreProvider } from "./store"; -import { MainDiv } from "@common/business_components"; +import { StoreProvider} from "./store"; +import koBase from "../public/locales/ko/message.json"; +import i18next from "i18next"; +import { I18nextProvider } from "react-i18next"; +import Base from "./base"; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + i18next.init({ + lng: "ko", + fallbackLng: "ko", + resources: { + ko: { + base: koBase, + }, + }, + }); + return ( <html lang="en"> <body> + <I18nextProvider i18n={i18next}> <StoreProvider> - <BaseLayout> - <MainDiv> - {children} - </MainDiv> - </BaseLayout> + <Base> + {children} + </Base> </StoreProvider> + </I18nextProvider> </body> </html> ); diff --git a/apps/wr/app/map/mainMap/page.tsx b/apps/wr/app/map/mainMap/page.tsx new file mode 100644 index 000000000..1e9afd74b --- /dev/null +++ b/apps/wr/app/map/mainMap/page.tsx @@ -0,0 +1,171 @@ +"use client"; + +import Gis from "@common/business_components/ui/map/Gis"; +import { useEffect, useRef, useState, useCallback } from "react"; +import { mapService } from "../service"; +import NewWindowPopup from "../newWindowPopup"; +import GisApp from "@common/business_components/ui/map/lib/gis.core"; + +const Map = () => { + const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 + const gisMaps = useRef<any | null>(null); + const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 + const [isSearchModalOpen, setIsSearchModalOpen] = useState(true); // SearchModal 열림 상태 + const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 + const [checkedLayers, setCheckedLayers] = useState<string[]>([]); + const [checkedLayersData, setCheckedLayersData] = useState<any[]>([]); + + const fetchData = async () => { + try { + const response = await mapService.getLyrStngs(); + setTreeData(response); // API 응답 저장 + } catch (error) { + console.error("데이터 불러오기 실패:", error); + } + }; + + // 팝업 닫기 핸들러를 useCallback으로 메모이제이션 + const handlePopupClose = useCallback(() => { + setIsOpen(false); + setFeatureData(null); + }, []); + + // 컴포넌트 마운트 시 1회 실행 + useEffect(() => { + fetchData(); + }, []); + + // 체크된 레이어 정보를 받는 핸들러 + const handleCheckedLayersChange = ( + checkedIds: string[], + checkedNodesData: any[] + ) => { + setCheckedLayers(checkedIds); + setCheckedLayersData(checkedNodesData); + }; + + // SearchModal, GisContent 등에서 호출 + const onFeatureSelect = (data: any) => { + const map = gisMaps.current; + if (!map) { + console.warn("지도가 초기화되지 않았습니다."); + return; + } + + // 항상 같은 select 객체 사용 + const select = GisApp.Module.select as any; + if (!select) return; + + // 1. 기존 선택 초기화 + select.resetSelectedFeature(); + + // 2. data가 OpenLayers Feature 인스턴스인지 확인 + let foundFeature: any = null; + let featureId = data.fid; + const layerName = data.layerNm; + if (layerName === "tb_wr_bdst_ruse_fclt") { + // 재이용시설 + featureId = `${layerName}.${data.searchPk}`; + } else if ( //빗물이용시설, 중수도시설 + layerName === "tb_wr_rnwtr_utztn_fclt" || + layerName == "tb_wr_rswtr_fclt" + ) { + featureId = `${layerName}.${data.bdstInfoSn}`; + } else if (layerName === "tb_il_swage_prcs_fclt") { //하수처리시설 + featureId = `${layerName}.${data.bscFctCd}`; + } else if (layerName === "tb_wr_htdfl_fclt") { //온배수시설 + featureId = `${layerName}.${data.bdstRuseFcltSn}`; + } else if (layerName == "tb_wr_ruse_fclt_dmd_co") { //수요처 + featureId = `${layerName}.${data.dmdInfoId.replaceAll("-", "")}`; + } else { + console.warn("해당 레이어에 대한 featureId 매핑 없음", layerName); + featureId = `${layerName}.${data.fid || data.id || data.ftrIdn}`; + } + + map + .getLayers() + .getArray() + .forEach((layer: any) => { + const source = layer.getSource?.(); + if (source?.getFeatureById) { + const feature = source.getFeatureById(String(featureId).trim()); + if (feature) foundFeature = feature; + } + }); + + // 3. 피처 선택 + if (foundFeature) { + const geometry = foundFeature.getGeometry(); + if (geometry) { + const extent = geometry.getExtent(); + + map.getView().fit(extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 16, + constrainResolution: true, + nearest: true, + }); + } + select.selectFeature(foundFeature, true); + } else { + console.warn("OpenLayers Feature를 찾을 수 없습니다:", data); + } + + setFeatureData(data); + setIsOpen(true); + setIsSearchModalOpen(false); + }; + + // MapSearchBar 래퍼 컴포넌트 + // const CustomMapSearchBar = (props: any) => { + // return ( + // <MapSearchBar + // {...props} + // searchType="공급처 후보지 조회" + // showApplyButton={false} + // /> + // ); + // }; + + return ( + <div className="bg-container"> + <Gis + treeData={treeData} + isSearch={false} + gisMaps={gisMaps} + isMiniMap={false} + searchView={undefined} + // searchView={(props: any) => ( + // <MapSearchBar + // searchType="공급처 후보지 조회" + // showApplyButton={false} + // checkedLayers={checkedLayers} + // checkedLayersData={checkedLayersData} + // {...props} + // /> + // ) + // } + detailView={(props: any) => ( + <NewWindowPopup + onClose={handlePopupClose} // 메모이제이션된 함수 사용 + checkedLayers={checkedLayers} + checkedLayersData={checkedLayersData} + {...props} + /> + )} + onFeatureSelect={onFeatureSelect} + setIsOpen={setIsOpen} + setFeatureData={setFeatureData} + featureData={featureData} + setIsSearchModalOpen={setIsSearchModalOpen} + isOpen={isOpen} + isSearchModalOpen={isSearchModalOpen} + onCheckedLayersChange={handleCheckedLayersChange} + imgVisble={true} + /> + </div> + ); +}; + +export default Map; diff --git a/apps/wr/app/map/newWindowPopup.tsx b/apps/wr/app/map/newWindowPopup.tsx new file mode 100644 index 000000000..973ad6aee --- /dev/null +++ b/apps/wr/app/map/newWindowPopup.tsx @@ -0,0 +1,795 @@ +"use client" +import NewWindowPortal from '@/app/map/newWindowPortal'; +import { useWrStore } from "@/app/store"; +import type { customHtdflBdst } from "@/app/store/ruseFcltMng/fcltReg/htdflReg"; +import type { customPbnst, pubHasuType } from "@/app/store/ruseFcltMng/fcltReg/pbnstReg/type"; +import type { customRainBdst } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; +import type { customRswtBdst } from "@/app/store/ruseFcltMng/fcltReg/rswtReg"; +import type { WaterReuseFacility } from "@/app/store/ruseFcltMng/fcltSttn/pbnstSttn/type"; +import { fetchType, getFetch, postFectch } from "@/components/lib/api"; +import { formatPhoneNumber, tableData } from "@/components/lib/func/commom"; +import { PbnstSttnModal } from "@/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal"; +import SttnDetailModal from "@/components/ui/modal/sttn/sttnDetailModal/sttnDetailModal"; +import { Title, UITab, UiTable } from '@common/business_components'; +import { useGisContext } from "@common/business_components/ui/map/Gis"; +import type { DemandCompType } from '@common/business_components/ui/sdmInsertForm/form'; +import { HeadMakeColSpan } from "@common/business_components/ui/table/HeadMakeColSpan"; +import moment from 'moment'; +import React, { useCallback, useEffect, useState } from 'react'; + +interface NewWindowPopupProps { + featureData: any; + seletectedLayerNm?: string + viewMode?: 'list' | 'detail' | 'nothing' + onFeatureSelect: (data: any) => void; + onClose: () => void; +} + +const NewWindowPopup: React.FC<NewWindowPopupProps> = ({ + featureData, + seletectedLayerNm, + onFeatureSelect, + onClose +}) => { + const { gisMap } = useGisContext() as unknown as { checkedLayerIds: React.MutableRefObject<number[]>; treeData: { lyrSn: number }[] | null; gisCore: any; gisMap: any; }; + const [tabData, setTabData] = useState<{ id: string; name: string; data: any }[]>([]); + const [activeTab, setActiveTab] = useState<string>('0'); + const [viewMode, setViewMode] = useState<'list' | 'detail' | 'nothing'>('list'); + const [fcltPk, setFcltPk] = useState<string>(''); + + + // onClose 함수를 useCallback으로 메모이제이션 + const memoizedOnClose = useCallback(() => { + onClose(); + }, [onClose]); + + // 하수재이용시설 변수 + const { + code, + pbnstSttn, + pbnstSttnSearch, + pbnSttnInit, + getFlcts + } = useWrStore((state) => state); + const { fclts, pbnstSttnResCount } = pbnstSttn + const [bdstTable, setBdstTable] = useState<WaterReuseFacility[]>() + + // 빗물재이용시설 변수 + const { rainSttn, rainSttnSearch, rainSttnInit } = useWrStore( + (state) => state + ); + const { rainSttnRes, rainSttnResCount } = rainSttn; + const [rainTable, setRainTable] = useState<customRainBdst[]>(); + const [rainPage, setRainPage] = useState(1) + const [rainDetailModal, setRainDetailModal] = useState<{ isOpen: boolean, data: customRainBdst | undefined }>({ data: undefined, isOpen: false }); + const [rainList, setRainList] = useState<customRainBdst[]>(rainSttnRes || []); + + // 중수도시설 변수 + const { rswtSttn, rswtSttnSearch, rswtSttnInit } = useWrStore( + (state) => state + ); + const { rswtSttnRes, rswtSttnResCount } = rswtSttn; + const [rswtrTable, setRswtrTable] = useState<customRswtBdst[]>(); + const [rswtrDetailModal, setRswtrDetailModal] = useState<{ isOpen: boolean, data: customRswtBdst | undefined }>({ data: undefined, isOpen: false }); + const [rswtrPage, setRswtrPage] = useState(1); + const [rswtrList, setRswtrList] = useState<customRswtBdst[]>(rswtSttnRes || []); + + // 온배수시설 변수 + const { htdflSttn, htdflSttnSearch, initHtdfl } = useWrStore((state) => state); + const { htdflSttnRes, htdflSttnResCount } = htdflSttn; + const [htdflTable, setHtdflTable] = useState<customHtdflBdst[]>(); + const [htdflDetailModal, setHtdflDetailModal] = useState<{ isOpen: boolean, data: customHtdflBdst | undefined }>({ data: undefined, isOpen: false }); + const [htdflPage, setHtdflPage] = useState(1) + const [htdflList, setHtdflList] = useState<customHtdflBdst[]>(htdflSttnRes || []); + + //하수처리시설 변수 + const { + pbnst, + pubHasuInit, + } = useWrStore((state) => state); + const { pubFacHeadList, pbnstHasuRes } = pbnst; + const [hasuList, setHasuList] = useState<pubHasuType[]>(pbnstHasuRes || []); + const [isRuseReg, setIsRuseReg] = useState<{ + data: any | undefined; + }>({ data: undefined }); + const [hasuTable, setHasuTable] = useState<pubHasuType[]>(); + const [hasuPage, setHasuPage] = useState<number>(1); + + // 수요처관리 변수 + const { + coSttn, + coSttnInit, + coSttnSearch, + // getDemandCompList, + // regDemandCompany, + // updateDemandCompany, + // coSttnTxtSearch, + // demandCompList, + } = useWrStore((state) => state); + const [mngTable, setMngTable] = useState<DemandCompType[]>(); + const [mngPage, setMngPage] = useState(1); + const { coSttnRes, coSttnResCount, coSttHeadList } = coSttn; + const [isDmdModal, setIsDmdModal] = useState<{ + isMoal: boolean; + data: any | undefined; + }>({ isMoal: false, data: undefined}); + const [mngList, setMngList] = useState<DemandCompType[]>(coSttnRes || []); + ///////////////////////////////////////////// 피쳐데이터 선택 시 각 시설별 데이터 처리 /////////////////////////////////////////////// + const getSelectedLayers = () => { + let result = null; + const layers = gisMap ? gisMap.getLayers().getArray().filter((l: any) => l.getVisible()) : []; // gisMap이 null인지 확인 후 레이어 가져오기 + let featureLayer: { get: (key: string) => any } = { get: () => null }; + layers.forEach((layer: any) => { + const source = layer.getSource(); + if (source && source.hasFeature && featureData + && typeof featureData.getId === 'function' && source.hasFeature(featureData)) { + featureLayer = layer; // 피처가 속한 레이어 찾기) + } + }); + result = featureLayer.get('lyrNm'); + + return result; + }; + const fetchpbnstDetail = async (featureCd: string) => { + try { + const response = await getFetch("wr", `/reuseFc/pubFc/${featureCd}`); + // 중복 데이터 추가 방지 + setTabDataFunc(response, featureCd); + } catch (error) { + console.error("Error fetching data:", error); + } + }; + const fetchHasuDetail = async (featureCd: string, data: string) => { + try { + const rainRes = await postFectch(`/prcsFclt/ruseInfo/1/10/A/0/0/1/SW/${featureCd}`, [data], fetchType.COMMON) + setHasuList( + rainRes.data.map((item: any) => { + return { + ...item, + fctCpc: Number(item.fctCpc) || 0, + ruse: item.ruse === false ? "N" : "Y", + }; + }) + ) + } catch (error) { + console.error("Error fetching data:", error); + } + }; + const detainSttn = async (featureCd: string, layerNm: string) => { + if (layerNm == 'tb_wr_rnwtr_utztn_fclt') { //빗물이용시설 + const res = await getFetch(fetchType.WR, `/reuseFc/rainyInfoList/1/20/0/${featureCd}`) + setRainList(res) + } + else if (layerNm == 'tb_wr_rswtr_fclt') { //중수도 시설 + const res = await getFetch(fetchType.WR, `/reuseFc/waterreuseFc/1/20/0/${featureCd}`) + setRswtrList(res) + } + else if(layerNm =='tb_wr_ruse_fclt_dmd_co'){ //수요처 + const res = await getFetch(fetchType.WR, `/reuseFc/request/1/20/0/${featureCd}`) + setMngList(res) + } + else { + const res = await getFetch(fetchType.WR, `/drain/facilityList/1/20/0/${featureCd}`) + setHtdflList(res) + setHtdflDetailModal({ isOpen: true, data: htdflSttnRes[0] }); + } + } + // 상세 목록 데이터 상태 처리 + useEffect(() => { + if (hasuList.length > 0) setIsRuseReg({ data: [hasuList[0]] }); + }, [hasuList]) + useEffect(() => { + if (rainList.length > 0) setRainDetailModal({ isOpen: true, data: rainList[0] }); + }, [rainList]) + useEffect(() => { + if (rswtrList.length > 0) setRswtrDetailModal({ isOpen: true, data: rswtrList[0] }); + }, [rswtrList]) + useEffect(() => { + if (htdflList.length > 0) setHtdflDetailModal({ isOpen: true, data: htdflList[0] }); + }, [htdflList]) + useEffect(() => { + if(mngList.length > 0) setIsDmdModal({ isMoal: true, data: [mngList[0]] }); + },[mngList]) + + const layerId = getSelectedLayers(); + if (featureData && typeof featureData.getProperties === "function") { + const selectedVal = featureData.getProperties(); + let featureCd = layerId == '하수재이용시설' ? selectedVal?.swtrmt_fclt_cd : layerId == '빗물이용시설' || layerId == '중수도시설' || layerId == '온배수시설' || layerId == '수요처' ? selectedVal?.bdst_nm : layerId == '하수처리시설' ? selectedVal?.swtrmt_fclt_nm : null; + if (featureCd && featureCd !== fcltPk) { + if (layerId == '하수재이용시설') { + featureCd = selectedVal?.swtrmt_fclt_cd; + setFcltPk(featureCd); // 현재 호출된 featureCd 저장 + setActiveTab(featureCd) + fetchpbnstDetail(featureCd); // 상세정보 + } + else if (layerId == '하수처리시설') { + featureCd = selectedVal?.swtrmt_fclt_nm; + setFcltPk(featureCd); // 현재 호출된 featureCd 저장 + fetchHasuDetail(featureCd, selectedVal?.swtrmt_fclt_cd); + } + else { + featureCd = selectedVal?.bdst_nm; + setFcltPk(featureCd) + if (layerId) { + detainSttn(featureCd, layerId); + } + } + setViewMode("detail") + } + } else if (layerId === null && seletectedLayerNm) { setViewMode("list") } + ///////////////////////////////////////////// 피쳐데이터 선택 시 각 시설별 데이터 처리 /////////////////////////////////////////////// + + ///////////////////////////////////////////// 하수재이용시설 ////////////////////////////////////////////////// + // 재이용시설 cellClick 이벤트 (상세정보) + const pbnstCellClick = (index: number | undefined) => { + if ( + index !== undefined && + pbnstSttn.pbnstSttnRes && + pbnstSttn.pbnstSttnRes[index] + ) { + setFcltPk(pbnstSttn.pbnstSttnRes[index].pbnstRuseFcltId); + onFeatureSelect(pbnstSttn.pbnstSttnRes[index]); + onClose() + } + }; + + // 재이용 현황 초기값 + useEffect(() => { + if (!pbnstSttn.isInit) { + getFlcts() + } + }, [seletectedLayerNm, pbnstSttn.isInit, getFlcts]); + useEffect(() => { + pbnSttnInit("1", "15", { + partValue: "A", + sidoValue: '0', + sigunValue: "0", + regionValue: "", + unitWaterShedValue: "", + middleRegionValue: "", + envPartValue: "", + sidoText: "", + sigunText: "" + }, String(new Date().getFullYear()), pbnstSttn.fclts); + }, [fclts, pbnSttnInit, pbnstSttn.fclts]) + useEffect(() => { + setBdstTable(pbnstSttn.pbnstSttnRes); + }, [seletectedLayerNm, pbnstSttn.pbnstSttnRes]); + + // 재이용시설 상세정보 + const setTabDataFunc = (res: any, pk: string) => { + setTabData((prev) => { + if (prev.some((tab) => tab.id === pk)) { + return prev; // 이미 존재하는 탭이면 추가하지 않음 + } + return [...prev, { id: pk, name: `재이용시설 ${prev.length + 1}`, data: res }]; + }); + } + // 재이용시설 조회 + // 재이용시설 페이징처리 관련 함수 + const onPageChangeHandler = useCallback((page: number) => { + pbnstSttnSearch( + false, + String(page), + String(13), + { + partValue: "A", + sidoValue: "0", + sigunValue: "0", + regionValue: "", + unitWaterShedValue: "", + middleRegionValue: "", + envPartValue: "", + sidoText: "", + sigunText: "", + }, + "", + String(new Date().getFullYear()), fclts + ); + }, [fclts, pbnstSttnSearch]); + // FIXME: 윤선화차장님 요구사항으로 엑셀 다운로드 삭제. + // const excelClick = useCallback(() => { + // // if (codeData) pbnstSttnSearch(1, "10000", codeData); + // excelDownload( + // [[]], + // "하수재이용시설 현황", + // "sample_pbnstSttn.xlsx", + // "A1:U2" + // ); + // }, [pbnstSttnSearch]); + ///////////////////////////////////////////// 하수재이용시설 ////////////////////////////////////////////////// + + ///////////////////////////////////////////// 빗물 재이용 ////////////////////////////////////////////////// + // 빗물재이용시설 cellClick 이벤트 (상세정보) + const rainCellClick = (index: number | undefined, headId: string) => { + if ( + index !== undefined && + rainTable !== undefined && + headId !== 'chBuildingYn') { + // setRainDetailModal({ isOpen: true, data: rainTable[index] }) + // setViewMode('detail') + onFeatureSelect(rainTable[index]); + onClose(); + } + } + + + useEffect(() => { + if (!rainSttn.isInit) { + rainSttnInit(false, 1, "20", "0", '', code.cityCd, code.sggCd); + } + }, [code.cityCd, code.sggCd, rainSttn.isInit, rainSttnInit]); + + useEffect(() => { + const data = tableData(rainSttnRes, { code }); + const chData = data + .map((item: any) => { + return { + ...item, + ...item.reuseBuilding[0], + ...item.reuseBuilding[0].rainyObj, + dtfcltCpct: item.reuseBuilding[0].rainyObj.dtfcltCpct === -1 + ? "확인불가" + : item.reuseBuilding[0].rainyObj.dtfcltCpct, + instlCst: item.reuseBuilding[0].instlCst === -1 + ? "확인불가" + : item.reuseBuilding[0].instlCst, + instlCstSprtAmt: item.reuseBuilding[0].instlCstSprtAmt === -1 + ? "확인불가" + : item.reuseBuilding[0].instlCstSprtAmt, + chBuildingYn: item.chgBuilding[0]?.bdstChgNm !== null ? "Y" : "N", + pntfColArea: item.reuseBuilding[0].rainyObj.pntfColArea === -1 + ? "확인불가" + : item.reuseBuilding[0].rainyObj.pntfColArea, + filtrTrtmfctCpct: item.reuseBuilding[0].rainyObj.filtrTrtmfctCpct === -1 + ? "확인불가" + : item.reuseBuilding[0].rainyObj.filtrTrtmfctCpct, + archArea: Math.floor(item.archArea) === -1 ? "확인불가" : item.archArea, + }; + }) + .filter((item: any) => item.rainyObj !== undefined); + setRainTable(chData); + }, [code, rainSttnRes, rainSttn.rainSttHeadList]); + + useEffect(() => { + rainSttnSearch(false, rainPage, "20", '', '', code.cityCd, code.sggCd); + }, [code.cityCd, code.sggCd, rainPage, rainSttnSearch]); + ///////////////////////////////////////////// 빗물 재이용 ////////////////////////////////////////////////// + + ///////////////////////////////////////////// 중수도 시설 ////////////////////////////////////////////////// + // 중수도시설 cellClick 이벤트 (상세정보) + const rswtCellClick = (index: number | undefined, headId: string) => { + if (headId !== 'chBuildingYn' && index !== undefined && rswtrTable !== undefined) { + // setRswtrDetailModal({ isOpen: true, data: rswtrTable[index] }); + // setViewMode('detail') + onFeatureSelect(rswtrTable[index]); + onClose(); + } + } + useEffect(() => { + if (!rswtSttn.isInit) { + rswtSttnInit(false, 1, "20", "0", '', code.cityCd, code.sggCd); + } + }, [code.cityCd, code.sggCd, rswtSttn.isInit, rswtSttnInit]); + + useEffect(() => { + const data = tableData(rswtSttnRes, { code }); + const chData = data + .map((item: customRswtBdst) => { + return { + ...item, + ...item.reuseBuilding[0], + ...item.reuseBuilding[0]?.rswtrObj, + chBuildingYn: item?.chgBuilding[0]?.bdstChgNm !== null && item?.chgBuilding.length > 0 ? 'Y' : 'N', + instlCst: item.reuseBuilding[0]?.instlCst === -1 ? "확인불가" : item.reuseBuilding[0]?.instlCst, + instlCstSprtAmt: item.reuseBuilding[0]?.instlCstSprtAmt === -1 ? "확인불가" : item.reuseBuilding[0]?.instlCstSprtAmt, + archArea: item?.archArea.split('.')[0] !== '-1' ? item?.archArea.split('.')[0] : '확인불가' + } + }) + .filter( + (item: customRswtBdst) => + item.reuseBuilding[0]?.rswtrObj !== undefined + ); + setRswtrTable(chData); + }, [code, rswtSttnRes]); + // FIXME: 윤선화 차장님 요구사항으로 엑셀 다운로드 삭제. + // const rswtSttnExcelDownload = useCallback(() => { + // if (rswtSttn.excelList.length > 0) { + // if (rswtSttn.excelList.length === 0) return; + // excelDownload( + // rswtSttn.excelList, + // "중수도시설 현황", + // "sample_rswtSttn.xlsx", + // "A1:W3", + // ); + // } + // }, [rswtSttn.excelList]) + + useEffect(() => { + rswtSttnSearch(false, rswtrPage, "20", '', '', code.cityCd, code.sggCd); + }, [code.cityCd, code.sggCd, rswtSttnSearch, rswtrPage]); + ///////////////////////////////////////////// 중수도 시설 ////////////////////////////////////////////////// + + ///////////////////////////////////////////// 온배수 시설 ////////////////////////////////////////////////// + // 온배수시설 cellClick 이벤트 (상세정보) + const htdflCellClick = (index: number | undefined) => { + if (index !== undefined && htdflTable !== undefined) { + onFeatureSelect(htdflTable[index]); + onClose(); + } + }; + useEffect(() => { + if (!htdflSttn.isInit) { + initHtdfl(false, 1, "20", "0", '', code.cityCd, code.sggCd); + } + }, [code.cityCd, code.sggCd, htdflSttn.isInit, initHtdfl]); + + useEffect(() => { + const data = tableData(htdflSttnRes, { code }); + const chData = data + .map((item: any) => ({ + ...item, + ...item.reuseBuilding[0], + ...item.reuseBuilding[0].htdflObj, + chBuildingYn: item.chgBuilding[0]?.bdstChgNm !== null ? "Y" : "N", + })) + .filter((item: any) => item.htdflObj !== undefined); + setHtdflTable(chData); + }, [code, htdflSttnRes, htdflSttn.htdflSttHeadList]); + + useEffect(() => { + htdflSttnSearch(true, htdflPage, "20", '', '', code.cityCd, code.sggCd); + }, [code.cityCd, code.sggCd, htdflPage, htdflSttnSearch]); + ///////////////////////////////////////////// 온배수 시설 ////////////////////////////////////////////////// + + ///////////////////////////////////////////// 하수처리 시설 ////////////////////////////////////////////////// + useEffect(() => { + if (code.sggCd.length && pbnstHasuRes.length > 0) { + setHasuTable(tableData(pbnstHasuRes, { code })); + } + }, [code, pbnstHasuRes]); + useEffect(() => { + if (!pbnst.isInit) { + pubHasuInit("1", "0"); + } + }, [pbnst.isInit, pubHasuInit]); + useEffect(() => { + pubHasuInit(String(hasuPage), "0"); + }, [hasuPage, pubHasuInit]); + + const hasuCellClick = (index: number | undefined) => { + if (index !== undefined && pbnstHasuRes !== undefined) { + onFeatureSelect(pbnstHasuRes[index]); + onClose(); + } + }; + ///////////////////////////////////////////// 하수처리 시설 ////////////////////////////////////////////////// + + ///////////////////////////////////////////// 수요처 관리 ////////////////////////////////////////////////// + useEffect(() => { + if (!coSttn.isInit) { + coSttnInit("1", "13", "0"); + } + }, [coSttn.isInit, coSttnInit]); + const coMngCellClick = (index: number | undefined) => { + if (index !== undefined && mngTable !== undefined) { + onFeatureSelect(mngTable[index]); + onClose(); + } + }; + useEffect(() => { + if (coSttnRes) { + if (coSttnRes?.length > 0 && code.cityCd.length > 0) { + const data = tableData(coSttnRes, { code }); + const chData = data.map((item: any) => ({ + ...item, + telno: formatPhoneNumber(item.telno || undefined), + regDt: moment(item.regDt).format("YYYY-MM-DD"), + dmdInfoId: `${item.dmdInfoId.slice(0, 3)}-${item.dmdInfoId.slice(3, 5)}-${item.dmdInfoId.slice(5, 10)}-${item.dmdInfoId.slice(10)}` + })); + // .filter((item: any) => item !== undefined); + setMngTable(chData); + } + } + }, [coSttnRes, code]); + useEffect(() => { + coSttnSearch(String(mngPage), "13", String(0)); + }, [coSttnSearch, mngPage]); + ///////////////////////////////////////////// 수요처 관리 ////////////////////////////////////////////////// + + // 체크된 레이어에 따른 조건부 컴포넌트 렌더링 함수 + const renderConditionalContent = () => { + const actTabData = tabData.find((tab) => tab.id === activeTab); + let layerId: string | undefined | null = getSelectedLayers(); + if (layerId === null || layerId === undefined) { + layerId = seletectedLayerNm || ''; + } + //하수처리시설 + const isPrcsFclt = layerId == 'tb_il_swage_prcs_fclt' + // 재이용시설 + const isPbnst = layerId == 'tb_wr_bdst_ruse_fclt'; + // 빗물이용시설 + const isRain = layerId == 'tb_wr_rnwtr_utztn_fclt'; + // 중수도시설 + const isRswt = layerId == 'tb_wr_rswtr_fclt'; + // 온배수시설 + const isHtdfl = layerId == 'tb_wr_htdfl_fclt'; + // 수요처관리 + const isReuseMng = layerId == 'tb_wr_ruse_fclt_dmd_co'; + if (isPrcsFclt) { + if (viewMode == 'list') { + return ( + <div style={{ overflowY: 'auto' }}> + <div className="flex justify-between items-center mb-4"> + <Title title="하수처리시설 현황" /> + </div> + {/* 높이 조정 */} + <UiTable + tableData={hasuTable || []} + headlist={pubFacHeadList} + pageSize={10} + total={5000} + headName={""} + cellClick={hasuCellClick} + onPageChange={setHasuPage} + /> + </div> + ) + } else { + return ( + <div style={{ overflowX: 'hidden' }}> + <div className="flex justify-between items-center mb-4"> + <Title title="하수처리시설 상세정보" /> + </div> + {/* 높이 조정 */} + <UiTable + tableData={isRuseReg?.data || []} + headlist={pubFacHeadList.slice(0, 4)} + pageSize={1} + total={0} + headName={"위치정보"} + cellClick={() => { }} + onPageChange={() => { }} + /> + <UiTable + tableData={isRuseReg?.data || []} + headlist={pubFacHeadList.slice(4)} + pageSize={1} + total={0} + headName={"기본정보"} + cellClick={() => { }} + onPageChange={() => { }} + /> + </div> + ) + } + } + if (isPbnst) { + if (viewMode == 'detail') { + // 재이용시설 상세 + return ( + <> + <div className="flex justify-between items-center mb-4"> + <Title title="하수재이용시설 상세정보" /> + </div> + <div className="flex justify-between items-center mb-8"> + <UITab + defaultValue={activeTab} + tabList={tabData.map((tab) => ({ + value: tab.id, + label: tab.name, + onClick: () => setActiveTab(tab.id), + }))} + /> + </div> + <PbnstSttnModal + data={actTabData?.data[0] || ({} as customPbnst)} + titleVisible={false} + /> + </> + ); + } else { + // 재이용시설 목록 + return ( + <div style={{ overflowY: 'auto' }}> + <div className="flex justify-between items-center"> + <Title title="하수재이용시설 현황" /> + {/* <div className="flex gap-2 mt-8"> + <Button className={btnClass} color="primary" onClick={excelClick}> + 엑셀다운로드 + </Button> + </div> */} + </div> + <UiTable + headName={[]} + tableData={bdstTable || []} + headlist={pbnstSttn.pbnstSttHeadList} + pageSize={20} + total={pbnstSttnResCount || 0} + cellClick={(index: number | undefined) => { + pbnstCellClick(index); + }} + onPageChange={onPageChangeHandler} + > + <HeadMakeColSpan upHeadList={pbnstSttn.pbnstSttNodeList} /> + </UiTable> + </div> + ); + } + } + + if (isRain) { + if (viewMode == 'detail') { + // 빗물이용시설 상세 + return ( + <SttnDetailModal type={"RNB"} formObj={rainDetailModal.data as customRainBdst} /> + ); + } else { + // 빗물이용시설 목록 + return ( + <> + <div className="flex justify-between items-center"> + <Title title="빗물이용시설 현황" /> + {/* <div className="flex gap-2 mt-8"> + <Button className={btnClass} color="primary" onClick={excelClick}> + 엑셀다운로드 + </Button> + </div> */} + </div> + <UiTable + headName={[]} + tableData={rainTable || []} + headlist={rainSttn.rainSttHeadList} + pageSize={20} + total={rainSttnResCount} + cellClick={(index: number | undefined, headId: string) => { + rainCellClick(index ?? undefined, headId); + }} + onPageChange={setRainPage} + > + <HeadMakeColSpan upHeadList={rainSttn.rainSttNodeList} /> + </UiTable> + </> + ); + } + } + + if (isRswt) { + if (viewMode == 'detail') { + // 중수도시설 상세 + return ( + <SttnDetailModal type={"RST"} formObj={rswtrDetailModal.data as customRswtBdst} /> + ); + } else { + // 중수도시설 목록 + return ( + <div style={{ overflowY: 'auto' }}> + <div className="flex justify-between items-center"> + <Title title="중수도시설 현황" /> + {/* <div className="flex gap-2 mt-8"> + <Button className={btnClass} color="primary" onClick={rswtSttnExcelDownload}> + 엑셀다운로드 + </Button> + </div> */} + </div> + <UiTable + headName={[]} + tableData={rswtrTable || []} + headlist={rswtSttn.rswtSttHeadList} + pageSize={20} + total={rswtSttnResCount} + cellClick={(index: number | undefined, headId: string) => { + rswtCellClick(index, headId); + }} + onPageChange={setRswtrPage} + > + <HeadMakeColSpan upHeadList={rswtSttn.rswtSttNodeList} /> + </UiTable> + </div> + ); + } + } + + if (isHtdfl) { + if (viewMode == 'detail') { + // 온배수시설 상세 + return ( + <SttnDetailModal type={"HTB"} formObj={htdflDetailModal.data as customHtdflBdst} /> + ); + } else { + // 온배수시설 목록 + return ( + <div style={{ overflowY: 'auto' }}> + <div className="flex justify-between items-center"> + <Title title="온배수시설 현황" /> + {/* <div className="flex gap-2 mt-8"> + <Button className={btnClass} color="primary" onClick={excelClick}> + 엑셀다운로드 + </Button> + </div> */} + </div> + <UiTable + headName={[]} + tableData={htdflTable || []} + headlist={htdflSttn.htdflSttHeadList} + pageSize={20} + total={htdflSttnResCount || 0} + cellClick={(index: number | undefined) => { + htdflCellClick(index ?? undefined); + }} + onPageChange={setHtdflPage} + > + <HeadMakeColSpan upHeadList={htdflSttn.htdflSttNodeList} /> + </UiTable> + </div> + ); + } + } + if (isReuseMng) { + if (viewMode == 'detail') { + return ( + <div style={{ overflowX: 'hidden' }}> + <div className="flex justify-between items-center mb-4"> + <Title title="수요처 상세정보" /> + </div> + {/* 높이 조정 */} + <UiTable + tableData={isDmdModal?.data || []} + headlist={coSttHeadList.slice(0, 3)} + pageSize={1} + total={0} + headName={"위치정보"} + cellClick={() => { }} + onPageChange={() => { }} + /> + <UiTable + tableData={isDmdModal?.data || []} + headlist={coSttHeadList.slice(3)} + pageSize={1} + total={0} + headName={"기본정보"} + cellClick={() => { }} + onPageChange={() => { }} + /> + </div> + ) + } else if(viewMode == 'list') { + return ( <div> + <div className="flex justify-between items-center"> + <Title title="수요처 현황" /> + </div> + <UiTable + headName={[]} + tableData={mngTable || []} + headlist={coSttHeadList} + pageSize={20} + total={coSttnResCount + 10 || 0} + cellClick={(index: number | undefined) => { + coMngCellClick(index); + // setUpdateData(coSttnRes[index as number]); + }} + onPageChange={(page) => { + setMngPage(page); + }} + ></UiTable></div>) + } + } + return null; // 조건에 맞는 콘텐츠가 없을 경우 + }; + + return ( + viewMode == 'nothing' ? <></> : + <NewWindowPortal + onClose={memoizedOnClose} + title="시설 상세 정보" + width={viewMode == 'list' ? window.screen.availWidth : 1100} + height={viewMode == 'list' ? window.screen.availHeight : layerId == '하수처리시설' && viewMode == 'detail' ? 450 : 900} + scrollbars='yes' + resizable='no'> + {/* 체크된 레이어에 따른 조건부 렌더링 */} + {renderConditionalContent()} + </NewWindowPortal> + ); +}; + +export default NewWindowPopup; \ No newline at end of file diff --git a/apps/wr/app/map/newWindowPortal.tsx b/apps/wr/app/map/newWindowPortal.tsx new file mode 100644 index 000000000..b8da17d7b --- /dev/null +++ b/apps/wr/app/map/newWindowPortal.tsx @@ -0,0 +1,103 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { createPortal } from 'react-dom'; + +// 현재 문서의 스타일(CSS)을 새 창으로 복사하는 함수 +const copyStyles = (sourceDoc: Document, targetDoc: Document) => { + Array.from(sourceDoc.styleSheets).forEach(styleSheet => { + try { + if (styleSheet.cssRules) { // <style> 태그로 주입된 CSS + const newStyleEl = sourceDoc.createElement('style'); + Array.from(styleSheet.cssRules).forEach(cssRule => { + newStyleEl.appendChild(sourceDoc.createTextNode(cssRule.cssText)); + }); + targetDoc.head.appendChild(newStyleEl); + } else if (styleSheet.href) { // <link> 태그로 연결된 CSS 파일 + const newLinkEl = sourceDoc.createElement('link'); + newLinkEl.rel = 'stylesheet'; + newLinkEl.href = styleSheet.href; + targetDoc.head.appendChild(newLinkEl); + } + } catch (e) { + console.warn('스타일시트를 복사할 수 없습니다:', e); + } + }); +}; + +interface NewWindowPortalProps { + children: React.ReactNode; + onClose: () => void; + title?: string; + width?: number; + height?: number; + resizable?: string; + scrollbars?: string; +} + +const NewWindowPortal: React.FC<NewWindowPortalProps> = ({ children, onClose, title = "", width = 900, height = 800, resizable = "yes", scrollbars = "yes" }) => { + const [container, setContainer] = useState<HTMLElement | null>(null); + const newWindowRef = useRef<Window | null>(null); + const onCloseRef = useRef(onClose); + + // onClose 함수를 ref로 저장하여 의존성 문제 해결 + useEffect(() => { + onCloseRef.current = onClose; + }, [onClose]); + + const handleUnload = useCallback(() => { + onCloseRef.current(); + }, []); + + useEffect(() => { + // 이미 창이 열려있으면 새로 열지 않음 + if (newWindowRef.current && !newWindowRef.current.closed) return; + // 브라우저 화면 정보 + const screenLeft = window.screenLeft ?? window.screenX; + const screenTop = window.screenTop ?? window.screenY; + const screenWidth = window.innerWidth ?? document.documentElement.clientWidth; + const screenHeight = window.innerHeight ?? document.documentElement.clientHeight; + + // 중앙 위치 계산 + const left = screenLeft + (screenWidth - width) / 2; + const top = screenTop + (screenHeight - height) / 2; + + // 1. 새 창을 열고 참조를 저장합니다. + const popupOptions = `width=${width},height=${height},left=${left}, top=${top}, resizable=${resizable},scrollbars=${scrollbars}`; + newWindowRef.current = window.open('', title, popupOptions); + + if (newWindowRef.current) { + newWindowRef.current.document.title = title; + + // 2. React 컴포넌트를 마운트할 컨테이너 div를 생성하여 새 창의 body에 추가합니다. + const div = newWindowRef.current.document.createElement('div'); + newWindowRef.current.document.body.appendChild(div); + setContainer(div); + + // 3. 메인 창의 스타일을 새 창으로 복사합니다. (매우 중요) + copyStyles(document, newWindowRef.current.document); + // 4. 사용자가 창을 닫을 때 부모에게 알리기 위한 이벤트 리스너를 설정합니다. + // const handleUnload = () => { + // onClose(); + // }; + newWindowRef.current.addEventListener('unload', handleUnload); + + // 5. 컴포넌트가 사라질 때 창을 닫는 클린업 함수를 반환합니다. + return () => { + if (newWindowRef.current) { + newWindowRef.current.removeEventListener('unload', handleUnload); + if (!newWindowRef.current.closed) { + newWindowRef.current.close(); + } + } + }; + } else { + alert('팝업이 차단되었습니다. 팝업 차단을 해제하고 다시 시도해주세요.'); + onCloseRef.current(); + // onClose(); + } + }, [handleUnload, onClose, title]); + + // `createPortal`을 사용해 children을 새 창의 container div에 렌더링합니다. + return container ? createPortal(children, container) : null; +}; + +export default NewWindowPortal; \ No newline at end of file diff --git a/apps/wr/app/map/popup/page.tsx b/apps/wr/app/map/popup/page.tsx new file mode 100644 index 000000000..80830aa90 --- /dev/null +++ b/apps/wr/app/map/popup/page.tsx @@ -0,0 +1,158 @@ +"use client"; + +import Gis from "@common/business_components/ui/map/Gis"; +import { useEffect, useRef, useState, useCallback } from "react"; +import { mapService } from "../service"; +import NewWindowPopup from "../newWindowPopup"; +import GisApp from "@common/business_components/ui/map/lib/gis.core"; + +const Map = () => { + const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 + const gisMaps = useRef<any | null>(null); + const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 + const [isSearchModalOpen, setIsSearchModalOpen] = useState(true); // SearchModal 열림 상태 + const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 + const [checkedLayers, setCheckedLayers] = useState<string[]>([]); + const [checkedLayersData, setCheckedLayersData] = useState<any[]>([]); + + const fetchData = async () => { + try { + const response = await mapService.getLyrStngs(); + setTreeData(response); // API 응답 저장 + } catch (error) { + console.error("데이터 불러오기 실패:", error); + } + }; + + // 팝업 닫기 핸들러를 useCallback으로 메모이제이션 + const handlePopupClose = useCallback(() => { + setIsOpen(false); + setFeatureData(null); + }, []); + + // 컴포넌트 마운트 시 1회 실행 + useEffect(() => { + fetchData(); + }, []); + + // 체크된 레이어 정보를 받는 핸들러 + const handleCheckedLayersChange = (checkedIds: string[], checkedNodesData: any[]) => { + setCheckedLayers(checkedIds); + setCheckedLayersData(checkedNodesData); + }; + + // SearchModal, GisContent 등에서 호출 + const onFeatureSelect = (data: any) => { + const map = gisMaps.current; + if (!map) { + console.warn("지도가 초기화되지 않았습니다."); + return; + } + + // 항상 같은 select 객체 사용 + const select = GisApp.Module.select as any; + if (!select) return; + + // 1. 기존 선택 초기화 + select.resetSelectedFeature(); + + // 2. data가 OpenLayers Feature 인스턴스인지 확인 + let foundFeature: any = null; + if ( + data && + typeof data.getGeometry === "function" && + typeof data.setStyle === "function" + ) { + // 이미 Feature 인스턴스라면 그대로 사용 + foundFeature = data; + } else { + // 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용 + const featureId = data.fid || data.ftrIdn || data.id; + map + .getLayers() + .getArray() + .forEach((layer: any) => { + const source = layer.getSource?.(); + if (source?.getFeatureById) { + const feature = source.getFeatureById(String(featureId).trim()); + if (feature) foundFeature = feature; + } + }); + } + + // 3. 피처 선택 + if (foundFeature) { + const geometry = foundFeature.getGeometry(); + if (geometry) { + const extent = geometry.getExtent(); + + map.getView().fit(extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 16, + constrainResolution: true, + nearest: true, + }); + } + select.selectFeature(foundFeature, true); + } else { + console.warn("OpenLayers Feature를 찾을 수 없습니다:", data); + } + + setFeatureData(data); + setIsOpen(true); + setIsSearchModalOpen(false); + }; + + // MapSearchBar 래퍼 컴포넌트 + // const CustomMapSearchBar = (props: any) => { + // return ( + // <MapSearchBar + // {...props} + // searchType="공급처 후보지 조회" + // showApplyButton={false} + // /> + // ); + // }; + + return ( + <div className="bg-container"> + <Gis + treeData={treeData} + isSearch={false} + gisMaps={gisMaps} + isMiniMap={false} + searchView={undefined} + // searchView={(props: any) => ( + // <MapSearchBar + // searchType="공급처 후보지 조회" + // showApplyButton={false} + // checkedLayers={checkedLayers} + // checkedLayersData={checkedLayersData} + // {...props} + // /> + // ) + // } + detailView={(props: any) =>( + <NewWindowPopup + onClose={handlePopupClose} // 메모이제이션된 함수 사용 + checkedLayers={checkedLayers} + checkedLayersData={checkedLayersData} + {...props} + /> + )} + onFeatureSelect={onFeatureSelect} + setIsOpen={setIsOpen} + setFeatureData={setFeatureData} + featureData={featureData} + setIsSearchModalOpen={setIsSearchModalOpen} + isOpen={isOpen} + isSearchModalOpen={isSearchModalOpen} + onCheckedLayersChange={handleCheckedLayersChange} + imgVisble={true} + /> + </div> + ); +}; + +export default Map; diff --git a/apps/wr/app/map/service/index.ts b/apps/wr/app/map/service/index.ts new file mode 100644 index 000000000..c6bfcff72 --- /dev/null +++ b/apps/wr/app/map/service/index.ts @@ -0,0 +1 @@ +export * from './map' diff --git a/apps/wr/app/map/service/map.ts b/apps/wr/app/map/service/map.ts new file mode 100644 index 000000000..5002afef7 --- /dev/null +++ b/apps/wr/app/map/service/map.ts @@ -0,0 +1,47 @@ + +/** + * 관망도 API + */ + const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}/v1/map` +//const API_URL = "http://con.mind-one.co.kr:11083/sewerage-service/api/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: async () => { + const response = await fetch(`${API_URL}/lyrStngs/WR_NETWORK`, { + method: 'GET', + headers: { + 'Accept': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return response.json(); + } +} diff --git a/apps/wr/app/matMng/coMat/coMatRvw/page.tsx b/apps/wr/app/matMng/coMat/coMatRvw/page.tsx index c1e601320..ff35b95c5 100644 --- a/apps/wr/app/matMng/coMat/coMatRvw/page.tsx +++ b/apps/wr/app/matMng/coMat/coMatRvw/page.tsx @@ -1,51 +1,51 @@ "use client"; import { useWrStore } from "@/app/store"; +import { usageProcData } from "@/app/store/matDmd/coMatRvw/headList"; import { DmdCoReqSchema, - type DedMatUpdateType, type DmdCoReqType, type DmdMatTableType, } from "@/app/store/matDmd/coMatRvw/type"; -import { formH1Class, formTableName } from "@/components/lib/class/comClass"; +import { formH1Class } from "@/components/lib/class/comClass"; import { + formatPhoneNumber, getCity, getdmdMatFlag, tableData, yearOptions, } from "@/components/lib/func/commom"; +import ConCostForm from "@/components/ui/form/coMat/conCostForm/conCostForm"; +import type { objSearchType } from "@/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal"; import { ContentsDiv, Form, FormField, FormUi, - ScrollArea, SearchDiv, SearchFormLeft, SearchFormRight, SearchInput, SelectBox, Title, - UiModal, UiTable, type SelectDataType, } from "@common/business_components/ui"; import ListDiv from "@common/business_components/ui/list_div"; import { Button, Input, Label } from "@common/components/ui"; +import { zodResolver } from "@hookform/resolvers/zod"; +import moment from "moment"; import { useEffect, useRef, useState } from "react"; -import { useForm } from "react-hook-form"; +import { useForm, useWatch } from "react-hook-form"; import toast, { Toaster } from "react-hot-toast"; import { z } from "zod"; -import moment from "moment"; -import { usageProcData } from "@/app/store/matDmd/coMatRvw/headList"; -import { zodResolver } from "@hookform/resolvers/zod"; -import ConCostForm from "@/components/ui/form/coMat/conCostForm/conCostForm"; -import type { objSearchType } from "@/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal"; export default function CoMatRvw() { - const { code, coRvw, coMatRvwInit, coMatRvwSearch, coMatRvwInqua } = + const { auth, code, coRvw, coMatRvwInit, coMatRvwSearch, coMatRvwInqua } = useWrStore((state) => state); - const { coMatRvwRes } = coRvw; + const { coMatRvwRes, coMatRvwCount } = coRvw; + const { regAuthrtYn } = auth; + //init, sub, match const initRef = useRef<boolean>(false); const { coRvwHeadList } = coRvw; @@ -68,6 +68,7 @@ export default function CoMatRvw() { const [selectData, setSelectData] = useState<DmdCoReqType>( {} as DmdCoReqType ); + const [selectionVersion, setSelectionVersion] = useState(0); const getSearch = ( startYear: number, @@ -91,39 +92,42 @@ export default function CoMatRvw() { const form = useForm<z.infer<typeof DmdCoReqSchema>>({ resolver: zodResolver(DmdCoReqSchema), + shouldUnregister: false, + // defaultValues: DmdCoReqSchema.parse({}) }); const TableCellClick = (index: number) => { - console.log(coMatRvwRes); if (coMatRvwRes && coMatRvwRes[index]) { - setSelectData(coMatRvwRes[index]); + setSelectionVersion((v) => v + 1); // 행을 클릭할 때마다 버전을 1씩 증가 } }; + const useUsgCdWatch = useWatch({ control: form.control, name: "useUsgCd" }); useEffect(() => { - if (form?.getValues("useUsgCd")) { + if (useUsgCdWatch) { form.setValue( "demandCst.usgPrcsProcs", - usageProcData?.find( - (item) => item.usage === (form?.getValues("useUsgCd") as string) - )?.process ?? "" + usageProcData?.find((item) => item.usage === useUsgCdWatch)?.process ?? + "" ); } - }, [form.watch("useUsgCd")]); + }, [useUsgCdWatch, form, selectionVersion]); useEffect(() => { - if (coMatRvwRes.length > 0) { - const data = coMatRvwRes?.map((item, index) => { + if (coMatRvwRes && coMatRvwRes.length > 0) { + const data = coMatRvwRes?.map((item) => { let flag = ""; - flag = getdmdMatFlag(item?.demandMatching?.splyFcltFlg ??""); + + flag = getdmdMatFlag(item?.demandMatching?.splyFcltFlg || undefined); + return { ...item, ctpvCd: item.demandCo?.ctpvCd, sggCd: item.demandCo?.sggCd, dmdCoNm: item.demandCo?.dmdCoNm, daddr: item.demandCo?.daddr, - telno: item.demandCo?.telno, - regDt: moment(item?.demandCo?.regDt).format("YYYY-MM-DD"), + telno: formatPhoneNumber(item?.demandCo?.telno || ""), + dmndTmng: item?.dmndTmng, splyFcltFlg: flag, }; }); @@ -151,8 +155,15 @@ export default function CoMatRvw() { useEffect(() => { if (selectData) { - console.log(selectData); + try { + DmdCoReqSchema.parse(selectData); + } catch (e) { + if (e instanceof z.ZodError) { + console.log(e.flatten()); + } + } form.reset(selectData); + setSelectionVersion((v) => v + 1); // 강제로 바뀌었다고 알리기 } }, [selectData, form]); return ( @@ -181,7 +192,7 @@ export default function CoMatRvw() { <div className="col-span-7 w-full"> <div className="flex flex-row items-center"> <Label className="text-xl text-gray-600 mx-6"> - 하수 재이용년도 + 수요신청년도 </Label> <SelectBox className="h-full w-32 text-2xl" @@ -221,7 +232,7 @@ export default function CoMatRvw() { <div className="flex flex-row space-x-2"> <Button size={"sm"} - onClick={(e) => { + onClick={() => { if (fromDate && toDate) { const fromNum = Number(fromDate); const toNum = Number(toDate); @@ -234,7 +245,10 @@ export default function CoMatRvw() { if (String(objSearch.city).length > 0) { con = String(objSearch.city); } - if (String(objSearch.sgg).length > 0 && String(objSearch.sgg) !== "0") { + if ( + String(objSearch.sgg).length > 0 && + String(objSearch.sgg) !== "0" + ) { con = String(objSearch.sgg); } getSearch(fromNum, toNum, con, dmdCoNm); @@ -259,410 +273,385 @@ export default function CoMatRvw() { tableData={dmdMatData || []} headlist={coRvwHeadList} pageSize={20} - total={100000} - cellClick={(index: number | undefined, headId: string) => { + total={coMatRvwCount} + cellClick={(index: number | undefined) => { if (index !== undefined && index >= 0) { TableCellClick(index); } }} - onPageChange={() => {}} + onPageChange={(val) => { + setPageNatiion(val); + }} ></UiTable> </ListDiv> <hr /> - <ScrollArea className="h-[49vh]"> - <ListDiv> - <h3 className="mt-4 ml-2 text-2xl font-medium text-label"> - 수요신청 상세현황 - </h3> - <Form {...form}> - <div className="grid grid-cols-12 gap-4 ml-4 mr-4"> - <div className="col-span-3 mt-4"> - <FormField - control={form.control} - name="demandCo.regDt" - render={({ field }) => ( - <FormUi label="요구시기"> - <Input - placeholder="" - type="text" - onChange={(e) => { - field.onChange(String(e.target.value)); - }} - value={ - moment(field.value).format("YYYY-MM-DD") ?? "" - } - readOnly - /> - </FormUi> - )} - /> - </div> - <div className="col-span-3 mt-4"> - <FormField - control={form.control} - name="dmndQty" - render={({ field }) => ( - <FormUi label="요구수량(M3/d)"> - <Input - placeholder="" - type="number" - step="0.1" - onChange={(e) => { - field.onChange(Number(e.target.value)); - }} - value={field.value ?? 0} - readOnly - /> - </FormUi> - )} - /> - </div> - <div className="col-span-3 mt-4"> - <FormField - control={form.control} - name="useUsgCd" - render={({ field }) => ( - <FormUi label="사용용도"> - <Input - placeholder="" - type="text" - step="0.1" - onChange={(e) => { - field.onChange(e.target.value); - }} - value={field.value ?? ""} - readOnly - /> - </FormUi> - )} - /> - </div> - <div className="col-span-3 mt-4"> - <FormField - control={form.control} - name="wntUntprc" - render={({ field }) => ( - <FormUi label="희망단가(원/m3)"> - <Input - placeholder="" - type="number" - step="0.1" - onChange={(e) => { - field.onChange(Number(e.target.value)); - }} - value={field.value ?? 0} - readOnly - /> - </FormUi> - )} - /> - </div> - <div className="col-span-12 w-full"> - <h1 className={formH1Class}>요구수질</h1> - </div> - <div className="col-span-3 mt-4"> - <FormField - control={form.control} - name="grssClfg" - render={({ field }) => ( - <FormUi label="총대장균군수 (개/100ml)"> - <Input - placeholder="" - type="number" - step="0.1" - onChange={(e) => { - field.onChange(Number(e.target.value)); - }} - value={field.value ?? 0} - readOnly - /> - </FormUi> - )} - /> - </div> - <div className="col-span-3 mt-4"> - <FormField - control={form.control} - name="combResdch" - render={({ field }) => ( - <FormUi label="결합잔류염소(mg/L)"> - <Input - placeholder="" - type="number" - step="0.1" - onChange={(e) => { - field.onChange(Number(e.target.value)); - }} - value={field.value ?? 0} - readOnly - /> - </FormUi> - )} - /> - </div> - <div className="col-span-3 mt-4"> - <FormField - control={form.control} - name="trbt" - render={({ field }) => ( - <FormUi label="탁도(NTU)"> - <Input - placeholder="" - type="number" - step="0.1" - onChange={(e) => { - field.onChange(Number(e.target.value)); - }} - value={field.value ?? 0} - readOnly - /> - </FormUi> - )} - /> - </div> - <div className="col-span-3 mt-4"> - <FormField - control={form.control} - name="blgcOxygDmndQnt" - render={({ field }) => ( - <FormUi label="생물학적 산소요구량(BOD)(mg/L)"> - <Input - placeholder="" - type="number" - step="0.1" - onChange={(e) => { - field.onChange(Number(e.target.value)); - }} - value={field.value ?? 0} - readOnly - /> - </FormUi> - )} - /> - </div> - <div className="col-span-3 mt-4"> - <FormField - control={form.control} - name="smll" - render={({ field }) => ( - <FormUi label="냄새"> - <Input - placeholder="" - type="number" - step="0.1" - onChange={(e) => { - field.onChange(Number(e.target.value)); - }} - value={field.value ?? 0} - readOnly - /> - </FormUi> - )} - /> - </div> - <div className="col-span-3 mt-4"> - <FormField - control={form.control} - name="crmty" - render={({ field }) => ( - <FormUi label="색도(도)"> - <Input - placeholder="" - type="number" - step="0.1" - onChange={(e) => { - field.onChange(Number(e.target.value)); - }} - value={field.value ?? 0} - readOnly - /> - </FormUi> - )} - /> - </div> - <div className="col-span-3 mt-4"> - <FormField - control={form.control} - name="tn" - render={({ field }) => ( - <FormUi label="총질소 (T-N)(mg/L)"> - <Input - placeholder="" - type="number" - step="0.1" - onChange={(e) => { - field.onChange(Number(e.target.value)); - }} - value={field.value ?? 0} - readOnly - /> - </FormUi> - )} - /> - </div> - <div className="col-span-3 mt-4"> - <FormField - control={form.control} - name="tph" - render={({ field }) => ( - <FormUi label="총인(T-P)(mg/L)"> - <Input - placeholder="" - type="number" - step="0.1" - onChange={(e) => { - field.onChange(Number(e.target.value)); - }} - value={field.value ?? 0} - readOnly - /> - </FormUi> - )} - /> - </div> - <div className="col-span-3 mt-4"> - <FormField - control={form.control} - name="ph" - render={({ field }) => ( - <FormUi label="수소이온농도(pH)"> - <Input - placeholder="" - type="number" - step="0.1" - onChange={(e) => { - field.onChange(Number(e.target.value)); - }} - value={field.value ?? 0} - readOnly - /> - </FormUi> - )} - /> - </div> - <div className="col-span-3 mt-4"> - <FormField - control={form.control} - name="clIonQnt" - render={({ field }) => ( - <FormUi label="염화물(mgCL/L)"> - <Input - placeholder="" - type="number" - step="0.1" - onChange={(e) => { - field.onChange(Number(e.target.value)); - }} - value={field.value ?? 0} - readOnly - /> - </FormUi> - )} - /> - </div> - <div className="col-span-3 mt-4"> - <FormField - control={form.control} - name="cdtnv" - render={({ field }) => ( - <FormUi label="전기전도도(㎲/cm)"> - <Input - placeholder="" - type="number" - onChange={(e) => { - field.onChange(Number(e.target.value)); - }} - value={field.value ?? 0} - readOnly - /> - </FormUi> - )} - /> - </div> - <div className="col-span-3 mt-4"> - <FormField - control={form.control} - name="rmrk" - render={({ field }) => ( - <FormUi label="비고"> - <Input - placeholder="" - type="text" - onChange={(e) => { - field.onChange(e.target.value); - }} - value={field.value ?? ""} - readOnly - /> - </FormUi> - )} - /> - </div> - <div className="col-span-12 w-full"> - <h1 className={formH1Class}>공급처 후보지</h1> - </div> - <div className="col-span-4 mt-4"> - <FormField - control={form.control} - name={`demandMatching.splyCndtList.${0}`} - render={({ field }) => ( - <FormUi label="후보지1"> - <Input - placeholder="" - type="text" - onChange={(e) => { - field.onChange(String(e.target.value)); - }} - value={field.value ?? ""} - readOnly - /> - </FormUi> - )} - /> - </div> - <div className="col-span-4 mt-4"> - <FormField - control={form.control} - name={`demandMatching.splyCndtList.${1}`} - render={({ field }) => ( - <FormUi label="후보지2"> - <Input - placeholder="" - type="text" - onChange={(e) => { - field.onChange(e.target.value); - }} - value={field.value ?? ""} - readOnly - /> - </FormUi> - )} - /> - </div> - <div className="col-span-4 mt-4"> - <FormField - control={form.control} - name={`demandMatching.splyCndtList.${2}`} - render={({ field }) => ( - <FormUi label="후보지3"> - <Input - placeholder="" - type="text" - onChange={(e) => { - field.onChange(e.target.value); - }} - value={field.value ?? ""} - readOnly - /> - </FormUi> - )} - /> - </div> + {/* <ScrollArea className="h-[49vh]"> */} + <ListDiv> + <h3 className="mt-4 ml-2 text-2xl font-medium text-label"> + 수요신청 상세현황 + </h3> + <Form {...form}> + <div className="grid grid-cols-12 gap-4 ml-4 mr-4"> + <div className="col-span-3 mt-4"> + <FormField + control={form.control} + name="dmndTmng" + render={({ field }) => ( + <FormUi label="요구시기"> + <Input + placeholder="" + type="text" + onChange={(e) => { + field.onChange(String(e.target.value)); + }} + value={ + moment(field.value).format("YYYY-MM-DD") ?? "" + } + readOnly + /> + </FormUi> + )} + /> </div> - </Form> - <ConCostForm form={form} type={"mat"} /> - </ListDiv> - </ScrollArea> + <div className="col-span-3 mt-4"> + <FormField + control={form.control} + name="dmndQty" + render={({ field }) => ( + <FormUi label="요구수량(m³/일)"> + <Input + placeholder="" + type="number" + step="0.1" + onChange={(e) => { + field.onChange(Number(e.target.value)); + }} + value={field.value ?? 0} + readOnly + /> + </FormUi> + )} + /> + </div> + <div className="col-span-3 mt-4"> + <FormField + control={form.control} + name="useUsgCd" + render={({ field }) => ( + <FormUi label="사용용도"> + <Input + placeholder="" + type="text" + step="0.1" + onChange={(e) => { + field.onChange(e.target.value); + }} + value={field.value ?? ""} + readOnly + /> + </FormUi> + )} + /> + </div> + <div className="col-span-3 mt-4"> + <FormField + control={form.control} + name="wntUntprc" + render={({ field }) => ( + <FormUi label="희망단가(원/m3)"> + <Input + placeholder="" + type="number" + step="0.1" + onChange={(e) => { + field.onChange(Number(e.target.value)); + }} + value={field.value ?? 0} + readOnly + /> + </FormUi> + )} + /> + </div> + <div className="col-span-12 w-full"> + <h1 className={formH1Class}>요구수질</h1> + </div> + <div className="col-span-3 mt-4"> + <FormField + control={form.control} + name="grssClfg" + render={({ field }) => ( + <FormUi label="총대장균군수 (개/100ml)"> + <Input + placeholder="" + type="number" + step="0.1" + onChange={(e) => { + field.onChange(Number(e.target.value)); + }} + value={field.value ?? 0} + readOnly + /> + </FormUi> + )} + /> + </div> + <div className="col-span-3 mt-4"> + <FormField + control={form.control} + name="combResdch" + render={({ field }) => ( + <FormUi label="결합잔류염소(mg/L)"> + <Input + placeholder="" + type="number" + step="0.1" + onChange={(e) => { + field.onChange(Number(e.target.value)); + }} + value={field.value ?? 0} + readOnly + /> + </FormUi> + )} + /> + </div> + <div className="col-span-3 mt-4"> + <FormField + control={form.control} + name="trbt" + render={({ field }) => ( + <FormUi label="탁도(NTU)"> + <Input + placeholder="" + type="number" + step="0.1" + onChange={(e) => { + field.onChange(Number(e.target.value)); + }} + value={field.value ?? 0} + readOnly + /> + </FormUi> + )} + /> + </div> + <div className="col-span-3 mt-4"> + <FormField + control={form.control} + name="blgcOxygDmndQnt" + render={({ field }) => ( + <FormUi label="생물학적 산소요구량(BOD)(mg/L)"> + <Input + placeholder="" + type="number" + step="0.1" + onChange={(e) => { + field.onChange(Number(e.target.value)); + }} + value={field.value ?? 0} + readOnly + /> + </FormUi> + )} + /> + </div> + <div className="col-span-3 mt-4"> + <FormField + control={form.control} + name="smll" + render={({ field }) => ( + <FormUi label="냄새"> + <Input + placeholder="" + type="number" + step="0.1" + onChange={(e) => { + field.onChange(Number(e.target.value)); + }} + value={field.value ?? 0} + readOnly + /> + </FormUi> + )} + /> + </div> + <div className="col-span-3 mt-4"> + <FormField + control={form.control} + name="crmty" + render={({ field }) => ( + <FormUi label="색도(도)"> + <Input + placeholder="" + type="number" + step="0.1" + onChange={(e) => { + field.onChange(Number(e.target.value)); + }} + value={field.value ?? 0} + readOnly + /> + </FormUi> + )} + /> + </div> + <div className="col-span-3 mt-4"> + <FormField + control={form.control} + name="tn" + render={({ field }) => ( + <FormUi label="총질소 (T-N)(mg/L)"> + <Input + placeholder="" + type="number" + step="0.1" + onChange={(e) => { + field.onChange(Number(e.target.value)); + }} + value={field.value ?? 0} + readOnly + /> + </FormUi> + )} + /> + </div> + <div className="col-span-3 mt-4"> + <FormField + control={form.control} + name="tph" + render={({ field }) => ( + <FormUi label="총인(T-P)(mg/L)"> + <Input + placeholder="" + type="number" + step="0.1" + onChange={(e) => { + field.onChange(Number(e.target.value)); + }} + value={field.value ?? 0} + readOnly + /> + </FormUi> + )} + /> + </div> + <div className="col-span-3 mt-4"> + <FormField + control={form.control} + name="ph" + render={({ field }) => ( + <FormUi label="수소이온농도(pH)"> + <Input + placeholder="" + type="number" + step="0.1" + onChange={(e) => { + field.onChange(Number(e.target.value)); + }} + value={field.value ?? 0} + readOnly + /> + </FormUi> + )} + /> + </div> + <div className="col-span-3 mt-4"> + <FormField + control={form.control} + name="clIonQnt" + render={({ field }) => ( + <FormUi label="염화물(mgCL/L)"> + <Input + placeholder="" + type="number" + step="0.1" + onChange={(e) => { + field.onChange(Number(e.target.value)); + }} + value={field.value ?? 0} + readOnly + /> + </FormUi> + )} + /> + </div> + <div className="col-span-3 mt-4"> + <FormField + control={form.control} + name="cdtnv" + render={({ field }) => ( + <FormUi label="전기전도도(㎲/cm)"> + <Input + placeholder="" + type="number" + onChange={(e) => { + field.onChange(Number(e.target.value)); + }} + value={field.value ?? 0} + readOnly + /> + </FormUi> + )} + /> + </div> + <div className="col-span-3 mt-4"> + <FormField + control={form.control} + name="rmrk" + render={({ field }) => ( + <FormUi label="비고"> + <Input + placeholder="" + type="text" + onChange={(e) => { + field.onChange(e.target.value); + }} + value={field.value ?? ""} + readOnly + /> + </FormUi> + )} + /> + </div> + <div className="col-span-12 w-full"> + <h1 className={formH1Class}>공급처 후보지</h1> + </div> + {[0, 1, 2].map((idx) => ( + <div key={idx} className="col-span-4 mt-4"> + <FormField + control={form.control} + name={`splyCndtList.${idx}`} + render={({ field }) => ( + <FormUi label={`후보지${idx + 1}`}> + <Input + placeholder="" + type="text" + onChange={(e) => { + const newValue = { + ...(field.value || {}), + ruseUtztnFcltNm: e.target.value, + }; + field.onChange(newValue); + }} + value={field.value?.ruseUtztnFcltNm ?? ""} + readOnly + /> + </FormUi> + )} + /> + </div> + ))} + </div> + </Form> + <ConCostForm + form={form} + type={"mat"} + selectionVersion={selectionVersion} + readonly={regAuthrtYn ==="N"} + /> + </ListDiv> + {/* </ScrollArea> */} </div> </div> </ContentsDiv> diff --git a/apps/wr/app/matMng/coMat/ruseCaseMng/page.tsx b/apps/wr/app/matMng/coMat/ruseCaseMng/page.tsx index e8146b37a..e45a0a0f2 100644 --- a/apps/wr/app/matMng/coMat/ruseCaseMng/page.tsx +++ b/apps/wr/app/matMng/coMat/ruseCaseMng/page.tsx @@ -1,30 +1,171 @@ "use client"; + import { useWrStore } from "@/app/store"; import { ruseUtzType } from "@/app/store/matDmd/ruseCaseMng/headList"; -import { getCity } from "@/components/lib/func/commom"; +import { excelDownload, getCity } from "@/components/lib/func/commom"; +import RuseCaseMngExcel from "@/components/ui/modal/coMat/ruseCaseMng/ruseCaseMngExcelModal"; import type { objSearchType } from "@/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal"; -import { ContentsDiv, SearchDiv, SearchFormLeft, SearchFormRight, SearchInput, SelectBox, Title, UiTable, type SelectDataType } from "@common/business_components/ui"; +import { + ContentsDiv, + filePickedBlobArray, + SearchDiv, + SearchFormLeft, + SearchFormRight, + SearchInput, + SelectBox, + Title, + UiModal, + UiTable, + type SelectDataType, +} from "@common/business_components/ui"; import ListDiv from "@common/business_components/ui/list_div"; import { Button } from "@common/components/ui"; -import { useState } from "react"; +import { useEffect, useState } from "react"; +import { Toaster } from "react-hot-toast"; export default function RuseCaseMng() { - const { code,ruseCase} = useWrStore((state) => state); - const [selectSgg, setSelectSgg] = useState<SelectDataType[] | []>() + const { code, ruseCaseMng, selectRuseCaseMng } = useWrStore((state) => state); + const { ruseCaseMngList, status, count } = ruseCaseMng; + const [selectSgg, setSelectSgg] = useState<SelectDataType[] | []>([]); const [objSearch, setObjSearch] = useState<objSearchType>({ sgg: "", city: "" }); - const [selectUtz, setSelectUtz] = useState<string[] | []>() - const [searchTxt, setSearchTxt] = useState<string>() + + const [sidoCd, setSidoCd] = useState<string>(""); + const [sigunguCd, setSigunguCd] = useState<string>(""); + const [selectUtz, setSelectUtz] = useState<string[] | []>(["조경용수"]); + const [name, setName] = useState<string>(""); + // 물재이용사례 조회 + const searchRuseCaseMng = async (pageNo: number, pageSize: number) => { + await selectRuseCaseMng( + pageNo, + pageSize, + sidoCd ?? "", + sigunguCd ?? "", + selectUtz[0] ?? "", + name ?? "" + ); + } + + // 시도 선택 const getSelCity = (val: string | number) => { const cityValue = String(val); + const cityText = code.cityCd.find((item: { val: string; text: string }) => item.val === cityValue)?.text; + setSidoCd(cityText || ""); const data = getCity(cityValue, code.sggCd); setSelectSgg(data); - setObjSearch({ city: cityValue, sgg: '' }); + setObjSearch({ city: cityValue, sgg: "" }); }; + // 시군구 선택 const getSgg = (val: string | number) => { - setObjSearch({ ...objSearch, sgg: String(val) }) + const sggValue = String(val); + const sggText = selectSgg?.find((item: { val: string; text: string }) => item.val === sggValue)?.text; + setSigunguCd(sggText || ""); + setObjSearch({ ...objSearch, sgg: String(val) }); }; + + // 엑셀 등록 모달 + const [isExcelOpen, setIsExcelOpen] = useState<{ + isModal: boolean; + }>({ isModal: false }); + + // 다운로드 + const download = () => { + searchRuseCaseMng(1, 10000);; + + fetch(`/sample_ruseCaseMng.xlsx`, {}).then(async (response) => { + if (!response.ok) { + throw Error(response.statusText); + } + // 받은 파일 바인너리 처리 + const blobRet = await response.arrayBuffer(); + + const excelData1 = ruseCaseMngList.map((item) => { + return [ + item.ruseBizNm, + item.bizBgngDate, + item.bizEndDate, + item.splyFcltKnd, + item.splyFcltNm, + item.splyFcltCpct, + item.dmdFcltNm, + item.dmdFcltAddr, + item.ruseFcltCpct, + item.ruseFcltPplLen, + item.ruseFcltUsg, + item.ruseFcltSplyCpct, + item.ruseFcltPrcsMsrc, + item.grnteWqUsg, + item.grnteWqGrssClfg, + item.grnteWqCombResdch, + item.grnteWqTrbt, + item.grnteWqBod, + item.grnteWqSmll, + item.grnteWqCrmty, + item.grnteWqTn, + item.grnteWqTp, + item.grnteWqPh, + item.grnteWqClIonQnt, + item.grnteWqCndctStat, + item.grnteWqRmrk, + item.reqBizGrssCst, + item.reqBizReuseFcltCst, + item.reqBizPplCst, + item.reqBizArchCst, + item.reqBizMchnCst, + item.reqBizMtrngCst, + item.reqBizGisCst, + item.reqBizCctvCst, + item.reqBizCmcnCst, + item.reqBizWstCst, + item.reqBizDesignCst, + item.reqBizSpvsCst, + item.reqBizSbsdCst, + item.reqBizEtc, + item.prcmPlanGrssCst, + item.prcmPlanNlep, + item.prcmPlanLcep, + item.prcmPlanBrdnCst, + item.prcmPlanEcqrRt, + item.clclnGrssCst, + item.clclnNlep, + item.clclnLcep, + item.clclnBrdnCst, + item.clclnEcqrRt, + ]; + }); + + const sheetParameter = [ + { + data: excelData1, + headerNum: 2, + headerRange: 'A1:AX1', + sheetName: '물재이용사례', + startCol: 'A' + } + ]; + // 버퍼에 있는 엑셀데이터 전달 후 json으로 변환 + await filePickedBlobArray(blobRet, '물재이용사례현황', sheetParameter); + }); + } + + // 양식받기 + const sampleDownLoad = () => { + excelDownload( + [[]], + "물재이용사례 관리", + "sample_ruseCaseMng.xlsx", + "A1:AX1", + 2 + ); + } + + useEffect(() => { + if(status === 200){ + setIsExcelOpen({ isModal: false }); + } + }, [status]); + return ( <> <ContentsDiv> @@ -54,47 +195,32 @@ export default function RuseCaseMng() { selectClass={""} selectArray={ruseUtzType} onSelectValue={(val) => { - if(val) - setSelectUtz([String(val)])}} + if (val) setSelectUtz([String(val)]); + }} /> - <SearchInput className={"text-2xl"} label="수요처명" - textValue={""} + textValue={name} setText={(e) => { - setSearchTxt(e) + setName(e); }} /> </div> - </SearchFormLeft> - <SearchFormRight - className="w-2/12 mx-5 flex flex-col items-center justify-center space-x-2 "> + <SearchFormRight className="w-2/12 mx-5 flex flex-col items-center justify-center space-x-2"> <div className="space-y"> <div className="flex flex-row space-x-2"> - <Button - size={"sm"} - onClick={(e) => { }} - > + <Button size={"sm"} onClick={() => { searchRuseCaseMng(1, 20) }}> 조회 </Button> - <Button - size={"sm"} - onClick={(e) => { }} - > + <Button size={"sm"} onClick={() => { setIsExcelOpen({ isModal: true }); }}> 엑셀등록 </Button> - <Button - size={"sm"} - onClick={(e) => { }} - > + <Button size={"sm"} onClick={() => { download() }}> 다운로드 </Button> - <Button - size={"sm"} - onClick={(e) => { }} - > + <Button size={"sm"} onClick={() => { sampleDownLoad() }}> 양식받기 </Button> </div> @@ -103,24 +229,36 @@ export default function RuseCaseMng() { </SearchDiv> </div> </div> - <div className="grid grid-cols-6 gap-4 "> + <div className="grid grid-cols-6 gap-4"> <div className="col-span-6"> <ListDiv className="overflow-x-auto"> - <UiTable - headName={"물재이용사례 현황"} - tableData={[]} - children={undefined} - headlist={ruseCase.ruseHeadList} - pageSize={20} - total={100000} - cellClick={(index: number | undefined, headId: string) => { - }} - onPageChange={() => { }} - ></UiTable> + <div className="w-[350%]"> + <UiTable + headName={"물재이용사례 현황"} + tableData={ruseCaseMngList} + headlist={ruseCaseMng.ruseHeadList} + pageSize={20} + total={count} + cellClick={() => {}} + onPageChange={(pageNo: number) => { + searchRuseCaseMng(pageNo, 10); + }} + /> + </div> </ListDiv> </div> </div> </ContentsDiv> + <Toaster position="top-center" /> + {/* 엑셀 등록 모달 */} + <UiModal + open={isExcelOpen.isModal} + onOpenChange={() => { + setIsExcelOpen({ isModal: !isExcelOpen.isModal }) + }} + bodyClass={"w-[40vw] max-w-none"} + body={<RuseCaseMngExcel />} + /> </> - ) + ); } \ No newline at end of file diff --git a/apps/wr/app/matMng/coMat/ruseRvwHstry/page.tsx b/apps/wr/app/matMng/coMat/ruseRvwHstry/page.tsx index 2d339b869..d1fa8f069 100644 --- a/apps/wr/app/matMng/coMat/ruseRvwHstry/page.tsx +++ b/apps/wr/app/matMng/coMat/ruseRvwHstry/page.tsx @@ -1,17 +1,15 @@ "use client"; import { useWrStore } from "@/app/store"; +import { type RvwHistTableType } from "@/app/store/matDmd/coMatRvw/type"; import { - DmdCoReqSchema, - type DmdCoReqType, - type RvwHistTableType, -} from "@/app/store/matDmd/coMatRvw/type"; -import { + confirmToast, + formatPhoneNumber, getCity, getdmdMatFlag, + tableData, yearOptions, } from "@/components/lib/func/commom"; -import ConCostForm from "@/components/ui/form/coMat/conCostForm/conCostForm"; import type { objSearchType } from "@/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal"; import { ContentsDiv, @@ -25,17 +23,14 @@ import { type SelectDataType, } from "@common/business_components/ui"; import ListDiv from "@common/business_components/ui/list_div"; -import { Button, DatePicker, Input, Label } from "@common/components/ui"; -import { zodResolver } from "@hookform/resolvers/zod"; +import { Button, Label } from "@common/components/ui"; import moment from "moment"; import { useCallback, useEffect, useState } from "react"; -import { set, useFieldArray, useForm } from "react-hook-form"; -import { z } from "zod"; -import { tableData } from "@/components/lib/func/commom"; import toast, { Toaster } from "react-hot-toast"; -import { ToastFormUi } from "@/components/ui/toast/toastFromUi"; +import { useTranslation } from "react-i18next"; export default function RuseRvwHstry() { const { + auth, code, ruseHist, ruseRvwHistInit, @@ -43,7 +38,9 @@ export default function RuseRvwHstry() { ruseRvwInqua, ruseRvwComplete, } = useWrStore((state) => state); - const { rvwHistRes, ruseHistHeadList } = ruseHist; + const { rvwHistRes, ruseHistHeadList, rvwHistCount } = ruseHist; + const { regAuthrtYn } = auth; + const [pageNation, setPageNatiion] = useState<number>(1); const [dmdCoNm, setDmdCoNm] = useState<string>(""); @@ -53,6 +50,7 @@ export default function RuseRvwHstry() { sgg: "", city: "", }); + const { t } = useTranslation('base'); const [fromDate, setFromDate] = useState<string>( String(new Date().getFullYear()) @@ -69,6 +67,28 @@ export default function RuseRvwHstry() { setObjSearch({ city: cityValue, sgg: "0" }); }; + useEffect(() => { + getButtonSearch(); + }, [pageNation]); + + const getButtonSearch = () => { + if (fromDate && toDate) { + const fromNum = Number(fromDate); + const toNum = Number(toDate); + if (fromNum > toNum) { + toast.error("시작년도가 종료년도 이후로 설정할 수 없습니다."); + } else { + let con: string | number = ""; + if (String(objSearch.city).length > 0) { + con = String(objSearch.city); + } + if (String(objSearch.sgg).length > 0 && objSearch.sgg !== "0") { + con = String(objSearch.sgg); + } + getSearch(fromNum, toNum, con, dmdCoNm, ruseUtztnFcltNm); + } + } + }; const getSgg = (val: string | number) => { console.log(val); setObjSearch({ ...objSearch, sgg: String(val) }); @@ -92,59 +112,69 @@ export default function RuseRvwHstry() { ruseUtztnFcltNm ); } else { - - ruseRvwInqua(pageNation, 13, String(startYear), String(endYear), sggCd); } }; - const form = useForm<DmdCoReqType>({ - resolver: zodResolver(DmdCoReqSchema), - mode: "onChange", - }); - const TableCellClick = useCallback( - (index: number | undefined) => { - if ( - index !== undefined && - index >= 0 && - rvwHistRes && - rvwHistRes[index] - ) { - form.reset(rvwHistRes[index]); + (index: number | undefined, headId: string) => { + if (index !== undefined && index >= 0) { + const selectedData = rvwHistData; + if (selectedData && selectedData.length > 0) { + if ( + selectedData[index]?.splyFcltFlg === "검토완료" && + headId === "completeYn" + ) { + setRvwHistData([...selectedData]); + } else if ( + selectedData[index]?.splyFcltFlg === "매칭완료" && + headId === "completeYn" + ) { + toast.error(t("canNotChangeMatStateMsg")); + return; + } + } } }, - [rvwHistRes, form] + [rvwHistData] ); const confrirmToast = () => { const completeData = rvwHistData ?.map((item) => { - form.clearErrors; - if (item?.completeYn) { + if (item?.completeYn.chk && item.splyFcltFlg === "검토완료") { return Number(item.dmdInfoSn); } }) .filter((item) => item !== undefined); - if (form.formState.isDirty) { - toast((t) => ( - <span className="flex flex-col items-center"> - 표준공사비 정보가 변경되었습니다. 검토 완료 진행시 입력한 정보는 - 초기화 됩니다. - <br /> - 진행하시겠습니까? - <button - onClick={() => { - toast.dismiss(t.id); - ruseRvwComplete(completeData as Number[]); - }} - > - 확인 - </button> - <button onClick={() => toast.dismiss(t.id)}>취소</button> - </span> - )); + + if (completeData && completeData?.length > 0) { + confirmToast(t('isMatchFinishMsg'), () => ruseRvwComplete(completeData as number[])); + + // const handleConfirm = async () => { + // ruseRvwComplete(completeData as number[]); + // }; + // toast((t) => ( + // <span className="flex flex-col items-center"> + // 매칭완료를 진행하시겠습니까? + // <br /> + // <div className="mt-2 flex flex-row space-x-2"> + // <Button + // size={"sm"} + // onClick={() => { + // toast.dismiss(t.id); + // ruseRvwComplete(completeData as number[]); + // }} + // > + // 확인 + // </Button> + // <Button size={"sm"} onClick={() => toast.dismiss(t.id)}> + // 취소 + // </Button> + // </div> + // </span> + // )); } else { - ruseRvwComplete(completeData as Number[]); + toast.error( t('isNotMatchFinishMsg')); } }; useEffect(() => { @@ -160,9 +190,17 @@ export default function RuseRvwHstry() { useEffect(() => { if (rvwHistRes) { - const data = rvwHistRes?.map((item, index) => { + const data = rvwHistRes?.map((item) => { let flag = ""; - flag = getdmdMatFlag(item.demandMatching?.splyFcltFlg ?? ""); + flag = getdmdMatFlag(item.demandMatching?.splyFcltFlg ?? ""); + const formatDate = (dateStr: string | null | undefined) => { + // null, undefined, 빈 문자열인 경우 '없음'을 반환합니다. + if (!dateStr) return "없음"; + // "YYYYMMDD" 형식으로 날짜를 파싱합니다. + const d = moment(dateStr, "YYYYMMDD"); + // 파싱된 날짜가 유효한 경우에만 포맷하고, 그렇지 않으면 '없음'을 반환합니다. + return d.isValid() ? d.format("YYYY-MM-DD") : "없음"; + }; return { ...item, @@ -170,20 +208,17 @@ export default function RuseRvwHstry() { sggCd: item.demandCo?.sggCd, dmdCoNm: item.demandCo?.dmdCoNm, daddr: item.demandCo?.daddr, - telno: item.demandCo?.telno, - regDt: item?.demandCo?.regDt - ? moment(item?.demandCo?.regDt).format("YYYY-MM-DD") + telno: formatPhoneNumber(item.demandCo?.telno || ""), + dmndTmng: item?.dmndTmng + ? moment(item?.dmndTmng).format("YYYY-MM-DD") : "없음", - rvwCmptnYmd: item?.rvwCmptnYmd - ? moment(item?.rvwCmptnYmd).format("YYYY-MM-DD") - : "없음", - mtchnCmptnYmd: item?.mtchnCmptnYmd - ? moment(item?.mtchnCmptnYmd).format("YYYY-MM-DD") - : "없음", - ruseUtztnFcltNm: item.ruseUtztnFcltNm, + + rvwCmptnYmd: formatDate(item.demandMatching?.rvwCmptnYmd), + mtchnCmptnYmd: formatDate(item.demandMatching?.mtchnCmptnYmd), + ruseUtztnFcltNm: item.ruseUtztnFcltNm, dmndQty: item.dmndQty, splyFcltFlg: flag, - completeYn: false, + completeYn: {chk:flag=="매칭완료", readonly:flag=="매칭완료"}, } as unknown as RvwHistTableType; }); @@ -192,10 +227,6 @@ export default function RuseRvwHstry() { }, [rvwHistRes]); - useEffect(() => { - console.log("변경여부 ", form.formState.isDirty); - }, [form.formState]); - return ( <ContentsDiv> <div className="grid grid-cols-4 gap-4"> @@ -271,37 +302,14 @@ export default function RuseRvwHstry() { <div className="flex flex-row space-x-2"> <Button size={"sm"} - onClick={(e) => { - if (fromDate && toDate) { - const fromNum = Number(fromDate); - const toNum = Number(toDate); - if (fromNum > toNum) { - toast.error( - "시작년도가 종료년도 이후로 설정할 수 없습니다." - ); - } else { - let con: string | number = ""; - if (String(objSearch.city).length > 0) { - con = String(objSearch.city); - } - if (String(objSearch.sgg).length > 0 && objSearch.sgg !== "0") { - con = String(objSearch.sgg); - } - getSearch( - fromNum, - toNum, - con, - dmdCoNm, - ruseUtztnFcltNm - ); - } - } + onClick={() => { + getButtonSearch(); }} > 조회 </Button> - <Button size={"sm"} onClick={confrirmToast}> - 검토완료 + <Button size={"sm"} onClick={confrirmToast} disabled={regAuthrtYn==='N'}> + 저장 </Button> </div> </div> @@ -318,17 +326,15 @@ export default function RuseRvwHstry() { tableData={rvwHistData || []} headlist={ruseHistHeadList} pageSize={20} - total={100000} + total={rvwHistCount} cellClick={(index: number | undefined, headId: string) => { - TableCellClick(index); + console.log("테이블 클릭", index); + TableCellClick(index, headId); + }} + onPageChange={(page) => { + setPageNatiion(page); }} - onPageChange={setPageNatiion} ></UiTable> - <hr /> - <h3 className="mt-4 ml-2 text-2xl font-medium text-label"> - 수요신청 상세현황 - </h3> - <ConCostForm form={form} type={"hist"} /> </ListDiv> </div> </div> diff --git a/apps/wr/app/matMng/coMng/coSttn/page.tsx b/apps/wr/app/matMng/coMng/coSttn/page.tsx index 163195756..8e2705450 100644 --- a/apps/wr/app/matMng/coMng/coSttn/page.tsx +++ b/apps/wr/app/matMng/coMng/coSttn/page.tsx @@ -2,14 +2,15 @@ import { useWrStore } from "@/app/store"; import { btnClass } from "@/app/store/common/code"; +import { excelCoSttnParsing } from "@/app/store/matDmd/coSttn/excel"; import { fetchType, getFetch } from "@/components/lib/api"; import { excelDownload, + formatPhoneNumber, getCity, - tableData + tableData, } from "@/components/lib/func/commom"; import type { objSearchType } from "@/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal"; -import { ToastFormUi } from "@/components/ui/toast/toastFromUi"; import { ContentsDiv, SearchDiv, @@ -26,11 +27,13 @@ import ListDiv from "@common/business_components/ui/list_div"; import SdmInsertForm from "@common/business_components/ui/sdmInsertForm"; import type { DemandCompType } from "@common/business_components/ui/sdmInsertForm/form"; import { Button } from "@common/components/ui"; +import moment from "moment"; import { useCallback, useEffect, useState } from "react"; import toast, { Toaster } from "react-hot-toast"; export default function CoSttn() { const { + auth, code, coSttn, coSttnInit, @@ -40,8 +43,8 @@ export default function CoSttn() { updateDemandCompany, coSttnTxtSearch, demandCompList, - dmdIsSucees } = useWrStore((state) => state); + const { regAuthrtYn, mdfcnAuthrtYn } = auth; const [bdstTable, setBdstTable] = useState<DemandCompType[]>(); const [pageNation, setPageNation] = useState(1); @@ -58,17 +61,33 @@ export default function CoSttn() { const [seartchTxt, setSearchTxt] = useState<string>(); const [upadateData, setUpdateData] = useState<DemandCompType>(); - - const getSelCity = (val: string | number) => { - const cityValue = String(val); // Ensure val is treated as a string - const data = getCity(cityValue, code.sggCd); - setSelectSgg(data); - setObjSearch({ city: cityValue, sgg: "" }); - }; - const getSgg = (val: string | number) => { - setObjSearch({ ...objSearch, sgg: String(val) }); - }; + const getSelCity = useCallback( + (val: string | number) => { + const cityValue = String(val); + const data = getCity(cityValue, code.sggCd); + setSelectSgg(data); + // 시/도를 변경하면 시/군/구는 '전체'("0")로 초기화합니다. + setObjSearch({ city: cityValue, sgg: "0" }); + }, + [code.sggCd] + ); + + const getSgg = useCallback((val: string | number) => { + setObjSearch((prev) => ({ ...prev, sgg: String(val) })); + setPageNation(1); + }, []); + + useEffect(() => { + // code.cityCd 데이터가 로드되었고, 아직 시/도 선택이 안된 초기 상태일 때 실행 + if (code.cityCd && code.cityCd.length > 0 && !objSearch.city) { + const firstCityVal = code.cityCd[0]?.val; + if (firstCityVal) { + // 첫 번째 시/도 값으로 상태를 초기화합니다. + getSelCity(firstCityVal); + } + } + }, [code.cityCd, objSearch.city, getSelCity]); useEffect(() => { if (!coSttn.isInit) { @@ -77,53 +96,67 @@ export default function CoSttn() { }, [coSttn.isInit, coSttnInit]); useEffect(() => { - if (coSttnRes?.length > 0 && code.cityCd.length > 0) { - const data = tableData(coSttnRes, { code }); - const chData = data.map((item: any) => ({ - ...item, + if (coSttnRes) { + if (coSttnRes?.length > 0 && code.cityCd.length > 0) { + const data = tableData(coSttnRes, { code }); + const chData = data.map((item: any) => ({ + ...item, + telno: formatPhoneNumber(item.telno || undefined), + regDt: moment(item.regDt).format("YYYY-MM-DD"), + dmdInfoId: `${item.dmdInfoId.slice(0, 3)}-${item.dmdInfoId.slice(3, 5)}-${item.dmdInfoId.slice(5,10)}-${item.dmdInfoId.slice(10)}` })); - // .filter((item: any) => item !== undefined); - setBdstTable(chData); - } else if (coSttnRes.length == 0) { - setBdstTable([]); + console.log("chData:", chData) + // .filter((item: any) => item !== undefined); + setBdstTable(chData); + } else if (coSttnRes?.length == 0) { + setBdstTable([]); + } } }, [coSttnRes, code]); useEffect(() => { if (coSttn.excelList.length > 0) { if (coSttn.excelList.length === 0) return; - excelDownload( + + const coExcel = excelCoSttnParsing( coSttn.excelList, + code.cityCd, + code.sggCd + ); + excelDownload( + coExcel as [], "수요처 관리", "sample_coSttn.xlsx", - "A1:F1", + "A1:H1", 2 ); } - }, [coSttn.excelList]); - - useEffect(() => { - const con = (objSearch.sgg && objSearch.sgg !== "0") ? objSearch.sgg : objSearch.city ? objSearch.city : "0"; - if(con){ - coSttnSearch(String(pageNation), "13", String(con)); - } - }, [coSttnSearch, objSearch.city, objSearch.sgg, pageNation]); + }, [coSttn.excelList, code]); const excelClick = useCallback(() => { - const con = objSearch.sgg ? objSearch.sgg : objSearch.city; + const con = + objSearch.sgg && objSearch.sgg !== "0" ? objSearch.sgg : objSearch.city; + if (con) coSttnSearch("1", "10000", String(con)); }, [coSttnSearch, objSearch.city, objSearch.sgg]); - + // }, [excelDownload, bdstTable]); const searchClick = () => { - const con = (objSearch.sgg && objSearch.sgg !== "0") ? objSearch.sgg : objSearch.city; + const con = + objSearch.sgg && objSearch.sgg !== "0" ? objSearch.sgg : objSearch.city; if (con) { if (seartchTxt && seartchTxt?.length > 0) { - coSttnTxtSearch("1", "13", String(con), seartchTxt ?? ""); + coSttnTxtSearch( + String(pageNation), + "13", + String(con), + seartchTxt ?? "" + ); } else { - coSttnSearch("1", "13", String(con)); + coSttnSearch(String(pageNation), "13", String(con)); } } }; + return ( <> <ContentsDiv> @@ -138,14 +171,19 @@ export default function CoSttn() { label="시도" selectClass={""} selectArray={code.cityCd || []} - onSelectValue={getSelCity} + onSelectValue={(val) => { + getSelCity(val); + setPageNation(1); + }} + value={objSearch.city} /> <SelectBox className="col-span-2 w-1/6 h-full text-2xl" label="시군구" selectClass={""} - selectArray={selectSgg?selectSgg : [{text:"전체",val:"0"}]} + selectArray={selectSgg || []} onSelectValue={getSgg} + value={objSearch.sgg} /> <SearchInput className={"!text-3xl"} @@ -159,17 +197,17 @@ export default function CoSttn() { </SearchFormLeft> <SearchFormRight className="w-2/12 mx-5 flex flex-col items-center justify-center space-x-2 "> <div className="flex flex-row space-x-2"> - <Button size={"sm"} onClick={searchClick}> + <Button className="mt-2" size={"sm"} onClick={searchClick}> 조회 </Button> <Button size={"sm"} className={btnClass} onClick={() => { - setUpdateData(undefined); setIsDmdModal({ ...isDmdModal, isMoal: true }); }} + disabled={regAuthrtYn === "N"} > 등록 </Button> @@ -179,15 +217,17 @@ export default function CoSttn() { onClick={() => { /* 저장 로직 */ if (!upadateData) { - <ToastFormUi body={"수요처 변경을 원하는 행을 선택해주세요"} title={"수요처관리"}></ToastFormUi>; - }else{ + toast.error("수요처 변경을 원하는 행을 선택해주세요"); + } else { setIsDmdModal({ ...isDmdModal, - isMoal: true}) + isMoal: true, + }); } }} + disabled={mdfcnAuthrtYn === "N"} > - 저장 + 수정 </Button> <Button size={"sm"} @@ -208,21 +248,30 @@ export default function CoSttn() { headName={[]} tableData={bdstTable || []} headlist={coSttn.coSttHeadList} - pageSize={bdstTable?.length ? Math.ceil(bdstTable.length / 13) : 0} - total={ coSttnResCount || 0 } - cellClick={(index: number | undefined, headId: string) => { + pageSize={20} + total={coSttnResCount + 10 || 0} + cellClick={(index: number | undefined) => { setUpdateData(coSttnRes[index as number]); }} - onPageChange={setPageNation} + onPageChange={(page) => { + const con = + objSearch.sgg && objSearch.sgg !== "0" + ? objSearch.sgg + : objSearch.city; + setPageNation(page); + coSttnSearch(String(page), "13", String(con)); + }} ></UiTable> </ListDiv> </div> </div> <UiModal - bodyClass={"w-[40vw] max-w-none h-[90vh]"} + bodyClass={ + "w-[40vw] max-w-none h-[80vh] " // +upadateData?.dmdInfoId?'h-[60vh]':'h-[90vh]' + } open={isDmdModal.isMoal} onOpenChange={() => { - setIsDmdModal({ isMoal: false, data: {} as DemandCompType }); + setIsDmdModal({ isMoal: false, data: {} as DemandCompType }); }} body={ <SdmInsertForm @@ -232,24 +281,38 @@ export default function CoSttn() { rtnValue={(searchText: string, pageCnt: number) => { getDemandCompList(searchText, pageCnt); }} - rtnInputData={ async(data) => { - if(upadateData?.dmdInfoPk){ - updateDemandCompany(data as DemandCompType) - }else{ - const dmd= await getFetch(fetchType.WR,`/reuseFc/request/${data.dmdInfoPk}`) - if(dmd) { - toast.error(`사업자번호 ${dmd.dmdInfoPk}는 이미 등록되었습니다. 다시등록해주세요`) - return - } - regDemandCompany(data as DemandCompType); + rtnInputData={async (data) => { + const con = + objSearch.sgg && objSearch.sgg !== "0" + ? objSearch.sgg + : objSearch.city; + console.log("data ", data); + if (upadateData?.dmdInfoId) { + console.log("data ", data); + await updateDemandCompany(data as DemandCompType); + coSttnInit("1", "13", String(con)); + setIsDmdModal({ + isMoal: false, + data: data as DemandCompType, + }); + } else { + const dmd = await getFetch( + fetchType.WR, + `/reuseFc/request/${data.dmdInfoId}` + ); + if (dmd) { + toast.error( + `사업자번호 ${dmd.dmdInfoId}는 이미 등록되었습니다. 다시등록해주세요` + ); + return; + } + await regDemandCompany(data as DemandCompType); + coSttnInit("1", "13", String(con)); + setIsDmdModal({ + isMoal: false, + data: data as DemandCompType, + }); } - if(dmdIsSucees ==='success'){ - toast.success(`수요처 등록 및 수정 완료되었습니다..`) - setIsDmdModal({ isMoal: false, data: data as DemandCompType }); - }else{ - toast.error(`수요처 등록 및 수정 에러입니다.`) - } - }} bodyClassNm={"w-full mx-auto bg-white rounded-lg shadow-md p-6"} updateList={upadateData} diff --git a/apps/wr/app/matMng/coMng/drgtSttn/page.tsx b/apps/wr/app/matMng/coMng/drgtSttn/page.tsx index 77bbe4a5a..be7210f3f 100644 --- a/apps/wr/app/matMng/coMng/drgtSttn/page.tsx +++ b/apps/wr/app/matMng/coMng/drgtSttn/page.tsx @@ -1,42 +1,126 @@ "use client"; import { useWrStore } from "@/app/store"; import { btnClass } from "@/app/store/common/code"; -import type { customCoSttnBdst } from "@/app/store/matDmd/coSttn"; +import type { customDrgt } from "@/app/store/matDmd/drgtSttn"; import { drgtHeadListExport } from "@/app/store/matDmd/drgtSttn/headList"; -import { excelDownload, getCity } from "@/components/lib/func/commom"; +import { getCity } from "@/components/lib/func/commom"; +import DrgtDetailModal from "@/components/ui/modal/coMat/drgtDetailModal"; import type { objSearchType } from "@/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal"; -import { ContentsDiv, SearchDiv, SearchFormLeft, SearchFormRight, SelectBox, Title, UiTable, type SelectDataType } from "@common/business_components/ui"; +import { ContentsDiv, DateMonthPicker, downloadExcel, SearchDiv, SearchFormLeft, SearchFormRight, SelectBox, Title, UiModal, UiTable, type SelectDataType } from "@common/business_components/ui"; import ListDiv from "@common/business_components/ui/list_div"; -import { Button, DatePicker, Label } from "@common/components/ui"; -import { useCallback, useState } from "react"; +import { Button, Label, ScrollArea } from "@common/components/ui"; +import { useEffect, useState } from "react"; +import { toast, Toaster } from "react-hot-toast"; export default function DrgtSttn() { - const { code, } = useWrStore((state) => state); - const [fromDate, setFromDate] = useState<Date>(new Date()); - const [toDate, setToDate] = useState<Date>(new Date()); - const [selectSgg, setSelectSgg] = useState<SelectDataType[] | []>(); + const { code, drgt, drgtSearch, drgtModalSearch, drgtInit } = useWrStore((state) => state); + const { drgtRes, drgtMoRes } = drgt; + const [fromDate, setFromDate] = useState<string>('202506'); + const [toDate, setToDate] = useState<string>('202506'); + const [selectSgg, setSelectSgg] = useState<SelectDataType[]>([{ val: '0', text: '전체' }]); const [objSearch, setObjSearch] = useState<objSearchType>({ sgg: "", city: "" }); + const [tableData, setTableData] = useState<customDrgt[]>(); + const [selectedSgg, setSelectedSgg] = useState<string>(''); + const [excelData, setExcelData] = useState<any[]>(); + const [drgtModal, setDrgtModal] = useState<{ + isOpen: boolean; + data: customDrgt[] | undefined; + }>({ data: undefined, isOpen: false }); + + const cellClick = (index: number) => { + if (fromDate && toDate && drgtRes[index]) { + drgtModalSearch(fromDate, toDate, drgtRes[index].sggNm ?? ''); + } else { + toast.error("조회기간을 선택해주세요."); + } + } + + const excelClick = () => { + downloadExcel({ + data: excelData ?? [], + fileName: "우리동네가뭄 현황", + sheetNameArray: ["Sheet1"], + headerRange: "A1:G1" + }) + } + + useEffect(() => { + if (!drgt.isInit) { + drgtInit(); + } + }, [drgt.isInit, drgtInit]); + + useEffect(() => { + let resData = Array.isArray(drgtRes) ? drgtRes : [drgtRes]; + resData = resData.map((item) => { + if (item.sggNm === '세종시') { + return { + ...item, + ctpvCd: '세종특별자치시', + sggNm: '' + } + } + + return { + ...item, + ctpvCd: code.cityCd.find(ct => (ct.text == item.sggNm || (code.sggCd.find(sgg => sgg.text == item.sggNm)?.val.substring(0, 2) == ct.val)))?.text, + sggNm: code.sggCd.find(sgg => sgg.text == item.sggNm)?.text + } + }) + const data = Array.isArray(resData) + ? resData.map((item: customDrgt) => { + return { + "시도": item.ctpvCd, + "시군구": item.sggNm, + "정상(횟수)": item.normal, + "관심(횟수)": item.attention, + "주의(횟수)": item.caution, + "관심+주의(횟수)": item.warning, + "가뭄순위": item.ranking, + } + }) : []; + setExcelData(data) + setTableData(resData) + }, [drgtRes]) + + useEffect(() => { + if (drgtMoRes && drgtMoRes.length > 0) { + setDrgtModal({ isOpen: true, data: drgtMoRes }); + } + }, [drgtMoRes]); const getSelCity = (val: string | number) => { - const cityValue = String(val); // Ensure val is treated as a string - const data = getCity(cityValue, code.sggCd); - setSelectSgg(data); - setObjSearch({ city: cityValue, sgg: '' }); + const cityValue = String(val); + + if (cityValue === "0") { + setSelectSgg([{ val: '0', text: '전체' }]); + setSelectedSgg('0'); + setObjSearch({ city: cityValue, sgg: '0' }); + } else { + const data = getCity(cityValue, code.sggCd); + setSelectSgg(data); + + const firstSggVal = data.find(item => item.val === "0")?.val || (data[0]?.val ?? ""); + setSelectedSgg(firstSggVal); + setObjSearch({ city: cityValue, sgg: firstSggVal }); + } }; const getSgg = (val: string | number) => { - console.log(val); - setObjSearch({ ...objSearch, sgg: String(val) }) + setSelectedSgg(String(val)); + setObjSearch({ ...objSearch, sgg: String(val) }); }; - const excelClick = useCallback(() => { - excelDownload( - [[]], - "우리동네가뭄", - "sample_drgtSttn.xlsx", - "A1:H1" - ); - }, []); + useEffect(() => { + if (fromDate === undefined && toDate === undefined) { + const today = new Date(); + const year = today.getFullYear(); + const month = String(today.getMonth() + 1).padStart(2, '0'); + const formatted = `${year}${month}`; + setFromDate(formatted); + setToDate(formatted); + } + }, [fromDate, toDate]) return ( <> <ContentsDiv> @@ -52,25 +136,51 @@ export default function DrgtSttn() { selectClass={""} selectArray={code.cityCd || []} onSelectValue={getSelCity} + value={objSearch?.city ||code.cityCd[0]?.val ||""} /> <SelectBox - className="col-span-2 w-1/6 h-full text-2xl" + className="col-span-2 w-1/6 h-full text-2xl ml-8" label="시군구" selectClass={""} - selectArray={selectSgg || []} + selectArray={selectSgg || [{ val: '0', text: '전체' }]} onSelectValue={getSgg} + value={selectedSgg ||'0'} /> <div className="col-span-5 w-auto"> <div className="flex flex-row items-center"> - <Label className="text-xl text-gray-600">조회기간</Label> + <Label className="text-xl text-gray-600 ml-8">조회기간</Label> <div className="flex flex-row w-1/4"> - <DatePicker mode={'single'} - selected={fromDate} - onSelect={(date: Date | undefined) => { if (date) setFromDate(date) }} /> - <span>~</span> - <DatePicker mode={'single'} - selected={toDate} - onSelect={(date: Date | undefined) => { if (date) setToDate(date) }} /> + <DateMonthPicker + field={{ + value: fromDate ?? "", + onChange: (date: string | undefined) => { + const year = date ? date.slice(0, 4) : ''; + let month = date?.slice(4); + if (month&&month?.length < 2) { + month = month?.padStart(2, "0"); + } + const formatted = `${year}${month}`; + setFromDate(formatted); + } + }} + selectClass="ml-4 text-2xl" + /> + <span className="ml-2 text-center">~</span> + <DateMonthPicker + field={{ + value: toDate ?? "", + onChange: (date: string | undefined) => { + const year = date ? date.slice(0, 4) : ''; + let month = date?.slice(4); + if (month&&month.length < 2) { + month = month?.padStart(2, "0"); + } + const formatted = `${year}${month}`; + setToDate(formatted); + } + }} + selectClass="ml-4" + /> </div> </div> </div> @@ -78,20 +188,42 @@ export default function DrgtSttn() { </SearchFormLeft> <SearchFormRight className="w-2/12 mx-5 flex flex-col items-center justify-center space-x-2 "> - <div className="flex flex-row space-x-2"> + <div className="flex flex-row space-x-2 items-center"> <Button size={"sm"} - onClick={(e) => { }} + onClick={() => { + let sidoText: string = ''; + let sggText: string | string[] = ''; + + if (objSearch?.city.toString().length > 0) { + sidoText = code.cityCd.find((item) => item.val === objSearch?.city)?.text ?? ''; + } + if (objSearch?.sgg.toString().length > 1) { + sggText = code.sggCd.find((item) => item.val === objSearch?.sgg)?.text ?? ''; + } else { + sggText = code.sggCd.filter((item) => item.val.substring(0, 2) === objSearch?.city).map((item) => item.text); + + } + if (sidoText === '전체') { sidoText = '' } + + if (sidoText === '세종특별자치시') { + sidoText = '세종시'; + sggText = sidoText; + } + + const searchArr:string[] = []; + if (sidoText) searchArr.push(sidoText); + if (Array.isArray(sggText)) { + searchArr.push(...sggText); + } else if (sggText) { + searchArr.push(sggText); + } + + drgtSearch(fromDate ?? "", toDate ?? "", searchArr) + }} > 조회 </Button> - <Button - size={"sm"} - className={btnClass} - onClick={() => { }} - > - 상세보기 - </Button> <Button size={"sm"} className={`${btnClass} green`} @@ -114,18 +246,31 @@ export default function DrgtSttn() { <div className="grid grid-cols-6 gap-4 "> <div className="col-span-6"> <ListDiv> - <UiTable - headName={[]} - tableData={[]} - headlist={drgtHeadListExport} - pageSize={20} - total={10000} - cellClick={(index: number | undefined, headId: string) => { }} - onPageChange={() => { }} - ></UiTable> + <ScrollArea className="h-[66vh]"> + <UiTable + headName={undefined} + tableData={tableData || []} + headlist={drgtHeadListExport} + pageSize={14} + total={0} + cellClick={(index: number | undefined) => { + cellClick(index ?? -1); + }} + onPageChange={() => { }} + ></UiTable> + </ScrollArea> </ListDiv> </div> </div> + <UiModal + open={drgtModal.isOpen} + onOpenChange={() => { + setDrgtModal({ ...drgtModal, isOpen: !drgtModal.isOpen }); + }} + bodyClass={"w-[50vw] max-w-none h-[80vh]"} + body={<DrgtDetailModal data={drgtModal.data as customDrgt[]} />} + /> + <Toaster position="top-center" /> </ContentsDiv> </> ) diff --git a/apps/wr/app/matMng/coMng/ldrmntCoSttn/page.tsx b/apps/wr/app/matMng/coMng/ldrmntCoSttn/page.tsx index f5dffbf8b..8c4976e09 100644 --- a/apps/wr/app/matMng/coMng/ldrmntCoSttn/page.tsx +++ b/apps/wr/app/matMng/coMng/ldrmntCoSttn/page.tsx @@ -3,11 +3,11 @@ import { useWrStore } from "@/app/store"; import { btnClass } from "@/app/store/common/code"; import { type customLdrmnt } from "@/app/store/matDmd/ldrmntCoSttn"; -import { ldrmntHeadListExport, ldrmntNodeListExport } from "@/app/store/matDmd/ldrmntCoSttn/headList"; +import { ldrmntHeadListExport } from "@/app/store/matDmd/ldrmntCoSttn/headList"; import { + confirmToast, excelDownload, getCity, - getCityOrSgg, tableData } from "@/components/lib/func/commom"; import CoRegExcel from "@/components/ui/modal/coReg/coRegExcel/coRegExcelModal"; @@ -24,89 +24,67 @@ import { Title, UiModal, UiTable, - type ReturnType, type SelectDataType } from "@common/business_components/ui"; import ListDiv from "@common/business_components/ui/list_div"; import { Button } from "@common/components/ui"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; +import toast, { Toaster } from "react-hot-toast"; +import { useTranslation } from "react-i18next"; export default function LdrmntCoSttn() { - const { code, ldrmnt, ldrmntInit, ldrmntSearch, } = useWrStore( + const { auth, code, ldrmnt, ldrmntInit, ldrmntSearch, ldrmntUpdate } = useWrStore( (state) => state ); - const { ldrmntRes, excelList } = ldrmnt; + const { ldrmntRes, excelList, ldrmntResCount } = ldrmnt; const [detailModal, setDetailModal] = useState<{ isOpen: boolean, data: customLdrmnt | undefined }>({ data: undefined, isOpen: false }) const [pageNation, setPageNation] = useState(1); const [selectSgg, setSelectSgg] = useState<SelectDataType[] | []>() const [objSearch, setObjSearch] = useState<objSearchType>({ sgg: "", city: "" }); const [bdstTable, setBdstTable] = useState<customLdrmnt[]>(); const [searchTxt, setSearchTxt] = useState<string>(); - const ref = useRef<ReturnType>({} as ReturnType); - const [excelData, setExcelData] = useState<any[]>(); - + const [excelData, setExcelData] = useState<any[] | undefined>() + const [resetPage, isResetPage] = useState(false); + const [isDownloadClicked, setIsDownloadClicked] = useState(false); + const { t } = useTranslation('base'); + const { regAuthrtYn, mdfcnAuthrtYn } = auth; const excelClick = useCallback(() => { excelDownload( [[]], - "잠재수요처 관리", + "잠재수요처 등록양식", "sample_ldrmntCoSttn.xlsx", "A1:AL1", 2 ); }, []); - const excelDownloadClick = () => { - downloadExcel({ - data: excelData ?? [], - fileName: "잠재수요처 관리", - sheetNameArray: ["Sheet1"], - headerRange: "A1:F1" - }) - } + const computeCityNm = () => { + let cityTxt = ""; + let sggTxt = ""; - const [isExcelOpen, setIsExcelOpen] = useState<{ - isModal: boolean; - }>({ isModal: false }); - - const cellClick = (index: number | undefined, headId: string) => { - if (index !== undefined) { - setDetailModal({ isOpen: true, data: ldrmntRes[index] }); + if (objSearch.city) { + cityTxt = + code.cityCd.find((c) => c.val === objSearch.city)?.text || ""; + } + if (objSearch.sgg) { + sggTxt = + code.sggCd.find((s) => s.val === objSearch.sgg)?.text || ""; } - } + if (cityTxt === "" || cityTxt === "전체") cityTxt = "0"; - const getSelCity = (val: string | number) => { - const cityValue = String(val); // Ensure val is treated as a string - const data = getCity(cityValue, code.sggCd); - setSelectSgg(data); - setObjSearch({ city: cityValue, sgg: '' }); + return sggTxt && sggTxt !== "0" + ? `${cityTxt} ${sggTxt}` + : cityTxt; }; - const getSgg = (val: string | number) => { - setObjSearch({ ...objSearch, sgg: String(val) }) - }; - - useEffect(() => { - if (!ldrmnt.isInit) { - ldrmntInit("0"); - } - }, [ldrmnt.isInit, ldrmntInit]); - - useEffect(() => { - const transData = Array.isArray(ldrmntRes) ? ldrmntRes : [ldrmntRes]; - const data = tableData(transData, { code }); - const tbData = Array.isArray(data) - ? data.map((item: customLdrmnt) => ({ - ...item, - ctpvCd: item.fctryAddr.split(" ")[0] || "", - sggCd: item.fctryAddr.split(" ")[1] || "" - - })) - : []; - - setBdstTable(tbData); - }, [ldrmntRes, code]); + const excelDownloadClick = useCallback(() => { + const cityNm = computeCityNm(); + setExcelData(undefined); + setIsDownloadClicked(true); + ldrmntSearch(true, 1, String(ldrmntResCount), cityNm, searchTxt || ""); + }, [objSearch, searchTxt, code, ldrmntSearch, computeCityNm, ldrmntResCount]); useEffect(() => { const transData = Array.isArray(excelList) ? excelList : [excelList]; @@ -114,8 +92,8 @@ export default function LdrmntCoSttn() { const data = Array.isArray(exData) ? exData.map((item: customLdrmnt) => { return { - "시도": item.fctryAddr.split(" ")[0] || "", - "시군구": item.fctryAddr.split(" ")[1] || "", + "시도": item.fctryAddr?.split(" ")[0] || "", + "시군구": item.fctryAddr?.split(" ")[1] || "", "회사명": item.dmdCoNm, "공장주소": item.fctryAddr, "공장관리번호": item.fctryMngNo, @@ -125,34 +103,75 @@ export default function LdrmntCoSttn() { setExcelData(data) }, [excelList]) + useEffect(() => { + if (!isDownloadClicked) return; + if (excelData === undefined) return; + if (excelData && excelData.length > 0) { + downloadExcel({ + data: excelData, + fileName: "잠재수요처 관리", + sheetNameArray: ["Sheet1"], + headerRange: "A1:F1" + }); + } else { + toast.error(t('excelDownErrMsg')); + } + setIsDownloadClicked(false); + }, [excelData, isDownloadClicked]); + + const [isExcelOpen, setIsExcelOpen] = useState<{ + isModal: boolean; + }>({ isModal: false }); + + const cellClick = (index: number | undefined) => { + if (index !== undefined) { + setDetailModal({ isOpen: true, data: ldrmntRes[index] }); + } + } + + const getSelCity = (val: string | number) => { + const cityValue = String(val); + + if (cityValue === "0") { + setSelectSgg([]); + setObjSearch({ city: cityValue, sgg: "" }); + } else { + const data = getCity(cityValue, code.sggCd); + setSelectSgg(data); + const firstSggVal = data.find(item => item.val === "0")?.val || (data[0]?.val ?? ""); + setObjSearch({ city: cityValue, sgg: firstSggVal }); + } + }; + + const getSgg = (val: string | number) => { + setObjSearch({ ...objSearch, sgg: String(val) }) + }; useEffect(() => { - let cityTxt = ''; - let sggTxt = ''; - - if (objSearch?.city.toString().length > 0) { - cityTxt = code.cityCd.find((item) => item.val === objSearch?.city)?.text ?? ''; + if (!ldrmnt.isInit) { + ldrmntInit(false, "0"); } + }, [ldrmnt.isInit, ldrmntInit]); - if (objSearch?.sgg.toString().length > 1) { - sggTxt = code.sggCd.find((item) => item.val === objSearch?.sgg)?.text ?? ''; - } + useEffect(() => { + const transData = Array.isArray(ldrmntRes) ? ldrmntRes : [ldrmntRes]; + const data = tableData(transData, { code }); + const tbData = Array.isArray(data) + ? data.map((item: customLdrmnt) => ({ + ...item, + ctpvCd: item.fctryAddr?.split(" ")[0] || "", + sggCd: item.fctryAddr?.split(" ")[1] || "" - let cityNm = ""; + })) + : []; - if (sggTxt !== "0") { - cityNm = cityTxt + " " + sggTxt; - } else { - cityNm = cityTxt; - } + setBdstTable(tbData); + }, [ldrmntRes, code]); - if (cityNm) { - if (cityTxt === '') cityNm = "0"; - ldrmntSearch(pageNation, cityNm, searchTxt || ""); - } else { - ldrmntSearch(pageNation, "", searchTxt || ""); - } - }, [pageNation, objSearch, code, searchTxt, ldrmntSearch]) + useEffect(() => { + ldrmntSearch(false, pageNation, '20', computeCityNm(), searchTxt || ""); + isResetPage(false) + }, [pageNation]); return ( <> @@ -175,6 +194,7 @@ export default function LdrmntCoSttn() { label="시군구" selectClass={""} selectArray={selectSgg || []} + value={objSearch.sgg} // value props 추가 onSelectValue={getSgg} /> <SearchInput @@ -189,34 +209,12 @@ export default function LdrmntCoSttn() { </SearchFormLeft> <SearchFormRight className="w-2/12 mx-5 flex flex-col items-center justify-center space-x-2 "> - <div className="flex flex-row space-x-2"> + <div className="flex flex-row space-x-2 items-center"> <Button size={"sm"} - onClick={(e) => { - - let cityTxt = '' - let sggTxt = '' - - if (objSearch?.city.toString().length > 0) { - cityTxt = code.cityCd.find((item) => item.val === objSearch?.city)?.text ?? ''; - } - - if (objSearch?.sgg.toString().length > 1) { - sggTxt = code.sggCd.find((item) => item.val === objSearch?.sgg)?.text ?? ''; - } - - let cityNm = "" - - if (sggTxt !== "0") { - cityNm = cityTxt + " " + sggTxt - } else { - cityNm = cityTxt - } - - if (cityNm) { - if (cityTxt === '') cityNm = "0"; - ldrmntSearch(pageNation, cityNm, searchTxt || ""); - } + onClick={() => { + ldrmntSearch(false, pageNation, '20', computeCityNm(), searchTxt || ""); + isResetPage(true) }} > 조회 @@ -227,6 +225,7 @@ export default function LdrmntCoSttn() { onClick={() => { setIsExcelOpen({ isModal: true }); }} + disabled={regAuthrtYn === 'N'} > 엑셀등록 </Button> @@ -254,13 +253,14 @@ export default function LdrmntCoSttn() { <div className="overflow-x-auto"> <ListDiv className="min-w-max"> <UiTable - headName={[]} + headName={undefined} tableData={bdstTable || []} headlist={ldrmntHeadListExport} pageSize={20} - total={10000} - cellClick={(index: number | undefined, headId: string) => { cellClick(index ?? undefined, headId) }} + total={ldrmntResCount || 0} + cellClick={(index: number | undefined) => { cellClick(index ?? undefined) }} onPageChange={setPageNation} + resetPageNo={resetPage} /> </ListDiv> </div> @@ -271,7 +271,16 @@ export default function LdrmntCoSttn() { setDetailModal({ ...detailModal, isOpen: !detailModal.isOpen }) }} bodyClass={"w-[40vw] max-w-none max-h-[calc(100%-70px)]"} - body={<CoDetailModal formObj={detailModal.data} />} + body={<CoDetailModal formObj={detailModal.data} + readonly={mdfcnAuthrtYn === 'N'} + onFormSubmit={async (value) => { + const handleConfirm = async () => { + await ldrmntUpdate(value); + setDetailModal({ ...detailModal, isOpen: false }); + ldrmntSearch(false, pageNation, '20', computeCityNm(), searchTxt || ""); + }; + confirmToast(t('isUpdateMsg'), handleConfirm); + }} />} /> <UiModal @@ -280,14 +289,11 @@ export default function LdrmntCoSttn() { setIsExcelOpen({ isModal: !isExcelOpen.isModal }) }} bodyClass={"w-[40vw] max-w-none"} - body={<CoRegExcel />} + body={<CoRegExcel isModal={(isOpen) => setIsExcelOpen({ isModal: isOpen })} />} /> - + <Toaster position="top-center"></Toaster> </div> - </ContentsDiv> </> - ) - } \ No newline at end of file diff --git a/apps/wr/app/page.tsx b/apps/wr/app/page.tsx index 4c6c58faf..8729ac5f0 100644 --- a/apps/wr/app/page.tsx +++ b/apps/wr/app/page.tsx @@ -146,9 +146,16 @@ export default function Home() { 중수도시설통계 </Button> </div> + </div> + </div> + + {/* 시설 현황(지도) */} + <div> + <h2 className="text-xl font-semibold mb-4">시설 현황(지도)</h2> + <div className="grid grid-cols-12 gap-4"> <div className="col-span-2"> - <Button className="w-full" onClick={() => {router.push('/stats/matStat')}}> - 물재이용사례 관리 + <Button className="w-full" onClick={() => {router.push('/map/mainMap')}}> + 시설 현황(지도) </Button> </div> </div> diff --git a/apps/wr/app/ruseFcltMng/fcltReg/htdflReg/page.tsx b/apps/wr/app/ruseFcltMng/fcltReg/htdflReg/page.tsx index 8227e0347..b0b2e7182 100644 --- a/apps/wr/app/ruseFcltMng/fcltReg/htdflReg/page.tsx +++ b/apps/wr/app/ruseFcltMng/fcltReg/htdflReg/page.tsx @@ -1,86 +1,85 @@ "use client"; import { useWrStore } from "@/app/store"; +import type { customHtdflBdst } from "@/app/store/ruseFcltMng/fcltReg/htdflReg"; +import { tableData } from "@/components/lib/func/commom"; +import HtdflRegForm from "@/components/ui/form/fcltReg/htdflReg/htdflRegForm"; +import { BdstReg } from "@/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal"; import { ContentsDiv, ScrollArea, SearchDiv, SearchFormLeft, - SearchFormRight, SearchInput, Title, UiModal, - UiTable, + UiTable } from "@common/business_components"; -import { Button } from "@common/components"; import ListDiv from "@common/business_components/ui/list_div"; +import { Button } from "@common/components"; import { useEffect, useState } from "react"; -import { tableData } from "@/components/lib/func/commom"; -import HtdflRegForm from "@/components/ui/form/fcltReg/htdflReg/htdflRegForm"; -import type { chgBuilding } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; -import BdstChgModal from "@/components/ui/modal/fcltReg/bdstChgModal/bdstChgModal"; -import type { customHtdflBdst } from "@/app/store/ruseFcltMng/fcltReg/htdflReg"; -import { BdstReg } from "@/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal"; +import { Toaster } from "react-hot-toast"; export default function Home() { - const { code, htdfl, initHtdfl,getHtdflBdstData } = useWrStore((state) => state); + const { auth,code, htdfl, initHtdflReg, getHtdflBdstData, htdflNmSearch } = + useWrStore((state) => state); const [searchTxt, setSearchTxt] = useState(""); const [pageNation, setPageNation] = useState(1); const [htdflForm, setHtdflForm] = useState<customHtdflBdst>(); const [bdstTable, setbdstTable] = useState<customHtdflBdst[]>(); - const [issNewReg,setIsNewReg] = useState<boolean>( false); + const [issNewReg, setIsNewReg] = useState<boolean>(false); - const [bdstChgProps, setBdstChgProps] = useState<{ - isOpen: boolean; - chgBuilding: chgBuilding[]; - }>({ isOpen: false, chgBuilding: [] }); - - const { htdflRes } = htdfl; + + const { htdflRes ,htdflResCount} = htdfl; + const { regAuthrtYn, mdfcnAuthrtYn } = auth; useEffect(() => { if (!htdfl.isInit) { - initHtdfl('0'); + initHtdflReg("0"); } - }, [htdfl.isInit, initHtdfl]); - + }, [htdfl.isInit, initHtdflReg]); useEffect(() => { - if (code.sggCd.length > 0) { + if (htdflRes && htdflRes.length > 0) { + const htdflData = tableData(htdflRes, { code }).map( + (item: customHtdflBdst) => { + const archArea = + item?.archArea.split(".")[0] !== "-1" + ? item?.archArea.split(".")[0] + : "확인불가"; - const htdflData = tableData(htdflRes, { code }).map( - (item: customHtdflBdst) => { - return { - ...item, - bdstChgYn: item?.chgBuilding && item.chgBuilding.length > 0 && item.chgBuilding[0]?.bdstChgNm !== null ? 'Y' : 'N', - bdstUsgCd: item.reuseBuilding[0]?.bdstUsgCd - }; - } - ); - setbdstTable(htdflData); + return { + ...item, + archArea: archArea, + }; + } + ); + setbdstTable(htdflData); + } else { + setbdstTable([]); } }, [code, htdflRes]); - const cellClick: (val: number|undefined ,headId: string) => void = ( + const cellClick: (val: number | undefined, headId: string) => void = ( val - ,headId) => { + ) => { if ((bdstTable ?? []).length > 0 && val !== undefined) { { setHtdflForm(htdflRes[val]); } } - - if (headId === "bdstChgYn" && val !== undefined) { - if ( - htdflRes[val]?.bdstInfoPk != null && - htdflRes[val]?.chgBuilding?.length > 0 - ) { - bdstChgModal(htdflRes[val]?.chgBuilding as chgBuilding[]); - } - } - }; - const bdstChgModal = (chgBuilding: chgBuilding[]) => { - setBdstChgProps({ isOpen: true, chgBuilding: chgBuilding }); + // if (headId === "bdstChgYn" && val !== undefined) { + // if ( + // htdflRes[val]?.bdstInfoSn != null && + // htdflRes[val]?.chgBuilding?.length > 0 + // ) { + // bdstChgModal(htdflRes[val]?.chgBuilding as chgBuilding[]); + // } + // } }; + // const bdstChgModal = (chgBuilding: chgBuilding[]) => { + // setBdstChgProps({ isOpen: true, chgBuilding: chgBuilding }); + // }; useEffect(() => { getHtdflBdstData(pageNation, "0"); }, [pageNation]); @@ -94,25 +93,36 @@ export default function Home() { <SearchDiv> <SearchFormLeft> <SearchInput - className="col-span-3 my-7 !text-3xl" - label="건물명" + className="col-span-3 !text-3xl" + label="시설명" textValue={searchTxt || ""} setText={(e) => { setSearchTxt(e); }} /> - </SearchFormLeft> - <SearchFormRight> - <div className="flex flex-row space-x-2 items-center"> - <Button size={"sm"} onClick={()=>{initHtdfl(searchTxt||"")}}>조회</Button> - <Button + <div className="flex flex-row space-x-2 items-center"> + <Button size={"sm"} + onClick={() => { + if (searchTxt && searchTxt.length > 0) { + htdflNmSearch(String(0), searchTxt); + } else { + initHtdflReg(String(0)); + } + }} + > + 조회 + </Button> + <Button size={"sm"} onClick={() => setIsNewReg(!issNewReg)} - > - 신규등록 - </Button> + disabled={regAuthrtYn === "N"} + > + 신규등록 + </Button> </div> - </SearchFormRight> + </SearchFormLeft> + {/* <SearchFormRight> + </SearchFormRight> */} </SearchDiv> </div> </div> @@ -123,44 +133,44 @@ export default function Home() { <UiTable tableData={bdstTable || []} headlist={htdfl.upHeadList} - pageSize={100} - total={10} - headName={"건축물 목록"} - cellClick={(val, headId) => { cellClick(val || undefined, headId); } } + pageSize={20} + total={htdflResCount} + headName={"시설물 목록"} + cellClick={(val, headId) => { + cellClick(val || undefined, headId); + }} onPageChange={setPageNation} - > - </UiTable> + ></UiTable> </ListDiv> </div> <div className="col-span-2 max-h-full"> - <ListDiv> - <ScrollArea className="h-[60vh]"> - <HtdflRegForm - htdflForm={htdflForm ?? ({} as customHtdflBdst)} - isModal={false} - /> - </ScrollArea> + <ListDiv> + <ScrollArea className="h-[65vh]"> + <HtdflRegForm + htdflForm={htdflForm ?? ({} as customHtdflBdst)} + isModal={false} + readonly={mdfcnAuthrtYn === "N"} + /> + </ScrollArea> </ListDiv> </div> </div> - <UiModal - body={<BdstReg moType="HTB"></BdstReg>} - bodyClass="w-[68vw] max-w-none h-[90vh]" - open={issNewReg} - onOpenChange={(open) => { - setIsNewReg(open); - }} - /> - - <UiModal - bodyClass={""} - open={bdstChgProps.isOpen} - body={<BdstChgModal chgBuilding={bdstChgProps.chgBuilding}></BdstChgModal>} + <UiModal + body={ + <BdstReg + moType="HTB" + isModal={false} + setIsModal={setIsNewReg} + ></BdstReg> + } + bodyClass="w-[68vw] max-w-none h-[90vh]" + open={issNewReg} onOpenChange={(open) => { - setBdstChgProps({ ...bdstChgProps, isOpen: open }); - } - } + setIsNewReg(open); + }} /> + + <Toaster position="top-center" /> </ContentsDiv> </> ); diff --git a/apps/wr/app/ruseFcltMng/fcltReg/pbnstReg/page.tsx b/apps/wr/app/ruseFcltMng/fcltReg/pbnstReg/page.tsx index 441eabf25..934946b74 100644 --- a/apps/wr/app/ruseFcltMng/fcltReg/pbnstReg/page.tsx +++ b/apps/wr/app/ruseFcltMng/fcltReg/pbnstReg/page.tsx @@ -14,7 +14,7 @@ import { useWrStore } from "../../../store"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@common/components"; import { useCallback, useEffect, useState } from "react"; import ListDiv from "@common/business_components/ui/list_div"; -import { tableData } from "@/components/lib/func/commom"; +import { confirmToast, tableData } from "@/components/lib/func/commom"; import type { customPbnst, fcBizType, @@ -24,43 +24,46 @@ import type { import { pbnstRegZod, pbnstRegArrayZod, - fcBizSchema, } from "@/app/store/ruseFcltMng/fcltReg/pbnstReg/type"; import { PbnstMngForm } from "@/components/ui/form/fcltReg/pbnstReg/mngReg/mngRegForm"; import { PbnstBuseForm } from "@/components/ui/form/fcltReg/pbnstReg/buseReg/bugRegForm"; import { PbnstRegModal } from "@/components/ui/modal/fcltReg/pbnstRegModal/pbnstRegModal"; -import toast, { Toaster } from "react-hot-toast"; +import toast, { Toaster } from "react-hot-toast"; // toast 이미 import 되어 있음 import { writeRgtrAndMdfr } from "@/app/store/ruseFcltMng/fcltReg/pbnstReg"; -import { useFieldArray, useForm } from "react-hook-form"; +import { useFieldArray, useForm, type FieldErrors } from "react-hook-form"; // FieldErrors 타입 추가 import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; +import { createDefaultPbnstItem } from "./utilsZod"; +import { useTranslation } from "react-i18next"; export default function PublicReuseFaciltyInsert() { const { + auth, code, pbnst, - searchFclts, pubHasuInit, + pbnstInsert, pbnstRegClick, pbnstRuseFcltUpdate, pubHasuSearch, pbnstRuseBizUpdate, } = useWrStore((state) => state); - const { pubFacHeadList, pbnstHasuRes, pbnstRes } = pbnst; - const [currentTab, setCurrentTab] = useState<string>("mng"); + + const { t } = useTranslation("base"); + const { pubFacHeadList, pbnstHasuResCount, pbnstHasuRes, pbnstRes } = pbnst; + const { regAuthrtYn, mdfcnAuthrtYn } = auth; + const [, setCurrentTab] = useState<string>("mng"); const [isRuseReg, setIsRuseReg] = useState<{ isModal: boolean; - data: pubHasuType; - }>({ isModal: false, data: {} as pubHasuType }); - + }>({ isModal: false }); + const [resetPage, isResetPage] = useState(false); const [indexMng, setIndexMng] = useState<number>(0); const [indexBiz, setIndexBiz] = useState<number>(0); const [searchTxt, setSearchTxt] = useState<string>(); const [bdstTable, setbdstTable] = useState<pubHasuType[]>(); const [selectRow, setSelectRow] = useState<pubHasuType>(); + const [pageNation, setPageNation] = useState<number>(1); - - const form = useForm<{ items: z.infer<typeof pbnstRegZod>[]; }>({ @@ -68,6 +71,8 @@ export default function PublicReuseFaciltyInsert() { // defaultValues: { items: pbnstRegArrayZod.parse([]) }, mode: "onChange", }); + const { isDirty } = form.formState; + const { fields: items, append, @@ -76,6 +81,8 @@ export default function PublicReuseFaciltyInsert() { control: form.control, name: `items`, }); + + const formWatch = form.watch("items"); useEffect(() => { if (!pbnst.isInit) { pubHasuInit("1", "41"); @@ -83,37 +90,129 @@ export default function PublicReuseFaciltyInsert() { }, [pbnst.isInit, pubHasuInit]); const onSubmit = async (valuse: { items: PbnstRegArrrayType }) => { + if (!isDirty) { + toast.error("변경사항이 없습니다."); // 변경사항이 없으면 메시지 표시 + return; + } const data = writeRgtrAndMdfr(valuse.items); let bizs = [] as fcBizType[]; data.forEach((fclt) => { bizs = [...bizs, ...fclt.fcBizs]; }); - const pbnst =data.map((fclt) => { - return({ - pbnstRuseFcltPk: fclt.pbnstRuseFcltPk, - fcltCpct: fclt.fcltCpct, - ruseCpct: fclt.ruseCpct, - splyExtraCpct: fclt.splyExtraCpct, - rgtr: fclt.rgtr, - mdfr: fclt.mdfr, - fcFiles: fclt.fcFiles, - fcOpers: fclt.fcOpers, - }) - }) - await pbnstRuseFcltUpdate(pbnst as customPbnst[]); - await pbnstRuseBizUpdate(bizs); + const pbnst = data.map((fclt) => { + return { + pbnstRuseFcltId: fclt.pbnstRuseFcltId, + fcltCpct: fclt.fcltCpct, + ruseCpct: fclt.ruseCpct, + splyExtraCpct: fclt.splyExtraCpct, + rgtr: fclt.rgtr, + mdfr: fclt.mdfr, + fcFiles: fclt.fcFiles, + fcOpers: fclt.fcOpers, + }; + }); + + const handleConfirm = async () => { + Promise.allSettled([ + pbnstRuseFcltUpdate(pbnst as customPbnst[]), + pbnstRuseBizUpdate(bizs), + ]).then(([resFclt, resBiz]) => { + const isFcltSuccess = resFclt.status === "fulfilled"; + const isBizSuccess = resBiz.status === "fulfilled"; + if (isFcltSuccess && isBizSuccess) { + toast.success("하수 재이용 시설 및 사업이 수정되었습니다. "); + // 메시지 표시, 페이지 이동 등 + } else { + let message = ""; + // ✅ 각각 어떤 항목이 실패했는지도 로그/알림 처리 + if (!isFcltSuccess) { + message = "하수재이용 시설 "; + } + if (!isBizSuccess) { + message += "하수재이용 시설 사업 "; + } + toast.error(message + "오류 입니다 "); + pubHasuInit("1", "41"); + } + }); + }; + confirmToast(t("isUpdatetMsg"), handleConfirm); }; - useEffect(() => { - if (code.sggCd.length) { - if (searchFclts) { - setbdstTable(tableData(searchFclts, { code })); - } else if (code.sggCd.length > 0 && pbnstHasuRes.length > 0) { - setbdstTable(tableData(pbnstHasuRes, { code })); - } + // 폼 유효성 검사 실패 시 호출될 함수 + const onInvalid = ( + errors: FieldErrors<{ items: z.infer<typeof pbnstRegZod>[] }> + ) => { + const findAllErrors = ( + errorObject: any, + path: (string | number)[] = [] + ): { message: string; path: (string | number)[] }[] => { + if (!errorObject) return []; + + return Object.keys(errorObject).flatMap((key) => { + const value = errorObject[key]; + const currentKey = !isNaN(parseInt(key)) ? parseInt(key) : key; + const newPath = [...path, currentKey]; + + if (value && typeof value === "object") { + if (value.message) { + return [{ message: value.message, path: newPath }]; + } + + return findAllErrors(value, newPath); + } + return []; + }); + }; + + const errorList = findAllErrors(errors); + + if (errorList.length > 0) { + errorList.forEach((errorInfo) => { + const { message, path } = errorInfo; + let prefix = ""; + + const itemsIndex = path.indexOf("items"); + + if (itemsIndex > -1 && path.length > itemsIndex + 1) { + const itemIdx = path[itemsIndex + 1] as number; + const facilityName = form.getValues( + `items.${itemIdx}.ruseUtztnFcltNm` + ); + if (facilityName) { + prefix += `[${facilityName}] `; + } + + const fcBizsIndex = path.indexOf("fcBizs"); + if (fcBizsIndex > -1 && path.length > fcBizsIndex + 1) { + const bizIdx = path[fcBizsIndex + 1] as number; + const cycle = form.getValues( + `items.${itemIdx}.fcBizs.${bizIdx}.cycl` + ); + if (cycle) { + prefix += `[${cycle}차수] `; + } + } + } + // 최종 오류 메시지 출력 + toast.error(`${prefix}${message}`); + }); } - }, [code, pbnstHasuRes, searchFclts]); + }; + useEffect(() => { + console.log("form errors:", form.formState.errors); + }, [form.formState.errors]); + + useEffect(() => { + console.log("formState", items); + }, [items]); + + useEffect(() => { + if (code.sggCd.length && pbnstHasuRes.length > 0) { + setbdstTable(tableData(pbnstHasuRes, { code })); + } + }, [code, pbnstHasuRes]); const cellClick = (val: number | undefined) => { if (val === undefined) return; @@ -121,6 +220,8 @@ export default function PublicReuseFaciltyInsert() { setSelectRow(pbnstHasuRes?.[val]); if (selectRow) { pbnstRegClick(selectRow); + } else { + console.error("선택된 행이 없습니다."); } }; @@ -128,16 +229,6 @@ export default function PublicReuseFaciltyInsert() { setCurrentTab(value); }; - useEffect(() => { - if (pbnst.pbnstRes&& pbnst.pbnstRes?.length>0) { - console.log("pbnst.pbnstRes", pbnstRes); - form.reset({ items: pbnstRegArrayZod.parse(pbnst.pbnstRes) }); - }else{ - - console.log("pbnst.pbnstRes is null", pbnstRes); - form.reset({ items: [] }); - } - }, [pbnst.pbnstRes]); useEffect(() => { if (indexMng) { setIndexBiz(0); @@ -145,57 +236,35 @@ export default function PublicReuseFaciltyInsert() { }, [indexMng]); useEffect(() => { - console.log("form errors:", form.formState.errors); - }, [form.formState.errors]); + if (pbnst.pbnstRes && pbnst.pbnstRes?.length > 0) { + form.reset({ items: pbnst.pbnstRes }); + } else { + form.reset({ items: [] }); + } + }, [pbnst.pbnstRes]); const appendPbnstRuseFclt = useCallback(() => { - const selecTableRow = selectRow; - if (pbnstRes && pbnstRes.length > 0) { - const lastPbnst = pbnstRes[pbnstRes.length - 1]; - if (lastPbnst && selecTableRow) { - - append( - pbnstRegZod.parse({ - pbnstRuseFcltPk: - String(selecTableRow.bscFctCd) + String(pbnstRes.length + 1), - ruseUtztnFcltNm: "", - fcBizs: [ - fcBizSchema.parse({ - pbnstRuseFcltFk: - String(lastPbnst.pbnstRuseFcltPk) + - String(pbnstRes.length + 1), - cycl: 1, - }), - ] as fcBizType[], - }) - ); - setIsRuseReg({ ...isRuseReg, isModal: true }); + if (selectRow) { + // pbnstRes.length를 사용하여 새로운 항목의 인덱스를 결정 + append(createDefaultPbnstItem(selectRow, pbnstRes.length)); + setIsRuseReg({ isModal: true }); } } else { - if (selecTableRow) { - append( - pbnstRegZod.parse({ - pbnstRuseFcltPk: String(selecTableRow.bscFctCd)+'1', - ruseUtztnFcltNm: "", - regDt: null, - mdfcnDt: null, - fcBizs: [fcBizSchema.parse({pbnstRuseFcltFk:String(selecTableRow.bscFctCd)+'1',cycl:1})] as fcBizType[], - }) - ) - setIsRuseReg({ ...isRuseReg, isModal: true }); + if (selectRow) { + // pbnstRes가 비어있으면 첫 항목이므로 인덱스 0 + + append(createDefaultPbnstItem(selectRow, 0)); + setIsRuseReg({ isModal: true }); } } - }, [pbnstRegZod, pbnstRes, append, selectRow, form]); + }, [pbnstRes, append, selectRow]); useEffect(() => { - // console.log(" page getgetvaslue ", form.getValues()); - }, [form.getValues()]); + pubHasuInit(String(pageNation), "41"); + isResetPage(false); + }, [pageNation, pubHasuInit]); -useEffect(() =>{ - pubHasuInit(String(pageNation), "41"); -},[pageNation,pubHasuInit]) - return ( <> <ContentsDiv> @@ -205,8 +274,8 @@ useEffect(() =>{ <SearchDiv> <SearchFormLeft> <SearchInput - className={"col-span-3 my-7 !text-3xl"} - label="건물명" + className={"col-span-3 !text-3xl"} + label="시설명" textValue={searchTxt || ""} setText={(e) => { setSearchTxt(e); @@ -216,11 +285,15 @@ useEffect(() =>{ <Button className="" size={"sm"} - onClick={(e) => { - searchTxt && searchTxt?.length > 0 - ? pubHasuSearch(String(pageNation), "41", searchTxt) - : pubHasuInit(String(pageNation), "41"); + onClick={() => { + if (searchTxt && searchTxt?.length > 0) { + pubHasuSearch(String(pageNation), "41", searchTxt); + } else { + pubHasuInit(String(pageNation), "41"); + } + isResetPage(true); }} + > 조회 </Button> @@ -228,7 +301,104 @@ useEffect(() =>{ size={"sm"} className="" onClick={() => { - // pbnstMngData([] as customPbnst[]); + // pbnstMngData([] as customPbnst[]); + if (!selectRow) { + toast.error( + "하수리시시설을 테이블을 선택하고 등록버튼을 눌러주세요" + ); + return; + } else { + appendPbnstRuseFclt(); + } + }} + disabled={regAuthrtYn === "N"} + > + 신규등록 + </Button> + </div> + </SearchFormLeft> + </SearchDiv> + </div> + </div> + + <div className="grid grid-cols-8 gap-4"> + <div className="col-span-5 h-full min-h-0"> + <ListDiv className="min-h-0"> + {/* <ScrollArea className="h-[60vh]"> */} {/* 높이 조정 */} + <UiTable + tableData={bdstTable || []} + headlist={pubFacHeadList} + pageSize={10} + total={pbnstHasuResCount || 20} + headName={"공공 하수처리시설 목록"} + cellClick={cellClick} + onPageChange={setPageNation} + resetPageNo={resetPage} + /> + {/* </ScrollArea> */} + </ListDiv> + </div> + <ListDiv className="col-span-3 pr-6"> + {formWatch && formWatch[0]?.pbnstRuseFcltId ? ( + <Tabs defaultValue="mng" onValueChange={handleTabChange}> + <div className="flex justify-between items-center"> + <TabsList className=""> + <TabsTrigger className="!text-xl" value="mng"> + 시설정보 + </TabsTrigger> + <TabsTrigger className="!text-xl" value="buse"> + 차수별정보 + </TabsTrigger> + </TabsList> + <Button + size={"sm"} + onClick={form.handleSubmit(onSubmit, onInvalid)} + className="mr-5 mt-2" + disabled={mdfcnAuthrtYn === "N"} + > + 저장 + </Button> + </div> + <ScrollArea className="h-[60vh]"> + <TabsContent className="max-h-full" value="mng"> + <PbnstMngForm + formType="update" + form={form} + setIndexMng={(index: number) => { + setIndexMng(index); + }} + indexMng={indexMng} + readonly={mdfcnAuthrtYn === "N"} + /> + </TabsContent> + <TabsContent className="max-h-full" value="buse"> + <PbnstBuseForm + formType="update" + form={form} + setIndexBiz={(index: number) => { + setIndexBiz(index); + }} + indexMng={indexMng} + indexBiz={indexBiz} + readonly={mdfcnAuthrtYn === "N"} + /> + </TabsContent> + </ScrollArea> + </Tabs> + ) : ( + <> + <div className="card-header text-2xl"> + 등록된 재이용시설 정보가 없습니다. + </div> + <div className="!mx-[1.6rem] !p-0r flex flex-col items-center justify-center h-64 border rounded-md"> + <p className="text-3xl mb-4"> + 신규등록 버튼을 클릭하여 재이용시설을 등록해주세요. + </p> + <Button + size={"sm"} + className="" + onClick={() => { + // pbnstMngData([] as customPbnst[]); if (!selectRow) { toast.error( "하수리시시설을 테이블을 선택하고 등록버튼을 눌러주세요" @@ -242,83 +412,27 @@ useEffect(() =>{ 신규등록 </Button> </div> - </SearchFormLeft> - </SearchDiv> - </div> - </div> - - <div className="grid grid-cols-6 gap-4 h-full"> - <div className="col-span-4 h-full min-h-0"> - <ListDiv className="min-h-0"> - <ScrollArea className="h-[60vh]"> - <UiTable - tableData={bdstTable || []} - headlist={pubFacHeadList} - pageSize={10} - total={100000} - headName={"공공 하수처리시설 등록"} - cellClick={cellClick} - onPageChange={setPageNation} - children={undefined} - /> - </ScrollArea> - </ListDiv> - </div> - <ListDiv className="col-span-2 pr-6 "> - <ScrollArea className="h-[60vh]"> - <Tabs defaultValue="mng" onValueChange={handleTabChange}> - <TabsList className=""> - <TabsTrigger className="!text-xl" value="mng"> - 시설정보 - </TabsTrigger> - <TabsTrigger className="!text-xl" value="buse"> - 차수별정보 - </TabsTrigger> - <Button - size={"sm"} - onClick={form.handleSubmit(onSubmit)} - className="!justify-end" - > - 저장 - </Button> - </TabsList> - <TabsContent className="max-h-full" value="mng"> - <PbnstMngForm - formType="update" - form={form} - setIndexMng={(index: number) => { - console.log("setIndexMng", index); - setIndexMng(index); - }} - indexMng={indexMng} - /> - </TabsContent> - <TabsContent className="max-h-full" value="buse"> - <PbnstBuseForm - formType="update" - form={form} - setIndexBiz={(index: number) => { - setIndexBiz(index); - }} - indexMng={indexMng} - indexBiz={indexBiz} - /> - </TabsContent> - </Tabs> - </ScrollArea> + </> + )} </ListDiv> <UiModal body={ <PbnstRegModal - form={form} - data={isRuseReg.data || ({} as customPbnst)} + form={form} // form prop 전달 + setPbnstInsert={(data) => { + if (data) { + pbnstInsert(data); // pbnstInsert 함수 호출 + setIsRuseReg({ isModal: false }); + } + }} + onInvalid={onInvalid} ></PbnstRegModal> } - bodyClass="w-[68vw] max-w-none h-[90vh]" + bodyClass="w-[68vw] max-w-none h-[90vh]" open={isRuseReg.isModal} onOpenChange={(open) => { remove(form.getValues("items").length - 1); - setIsRuseReg({ data: {} as pubHasuType, isModal: open }); + setIsRuseReg({ isModal: open }); }} /> </div> diff --git a/apps/wr/app/ruseFcltMng/fcltReg/pbnstReg/utilsZod.tsx b/apps/wr/app/ruseFcltMng/fcltReg/pbnstReg/utilsZod.tsx new file mode 100644 index 000000000..f61697e7e --- /dev/null +++ b/apps/wr/app/ruseFcltMng/fcltReg/pbnstReg/utilsZod.tsx @@ -0,0 +1,47 @@ +import type { fcBizType, operType, pubHasuType } from "@/app/store/ruseFcltMng/fcltReg/pbnstReg/type"; + +export function createDefaultPbnstItem(row: pubHasuType, index: number) { + const pk = row.bscFctCd + String(index + 1); + + return JSON.parse(JSON.stringify({ + pbnstRuseFcltId: pk, + swtrmtFcltId: "", + ruseUtztnFcltNm: "", + fcltCpct: 0, + ruseCpct: 0, + splyExtraCpct: 0, + regDt: null, + rgtr: "", + mdfcnDt: null, + mdfr: "", + fcFiles: [], + fcBizs: [createDefaultBiz(pk, 1)], + fcOpers: [], // 빈 배열 강제 + })); +} + + + export function createDefaultOper(pbnstRuseFcltId: string): operType { + return { + pbnstRuseFcltId, + operMthd: "", + rprtvBzenty: "", + bgngDt: new Date(), + endDt: new Date(), + choose: false, + rgtr: "", + mdfr: "", + } as operType; + } + + + export function createDefaultBiz( + pbnstRuseFcltId: string, + cycl: number + ): fcBizType { + return { + pbnstRuseFcltId, + cycl, + // 필요한 값 있으면 여기에 추가 + } as fcBizType; + } \ No newline at end of file diff --git a/apps/wr/app/ruseFcltMng/fcltReg/rainReg/page.tsx b/apps/wr/app/ruseFcltMng/fcltReg/rainReg/page.tsx index 31743aaa9..e3daab9f9 100644 --- a/apps/wr/app/ruseFcltMng/fcltReg/rainReg/page.tsx +++ b/apps/wr/app/ruseFcltMng/fcltReg/rainReg/page.tsx @@ -1,12 +1,8 @@ "use client"; import { useWrStore } from "@/app/store"; -import { - type chgBuilding, - type customRainBdst, -} from "@/app/store/ruseFcltMng/fcltReg/rainReg"; -import { tableData } from "@/components/lib/func/commom"; +import { type customRainBdst } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; +import { formatCommaNumber, tableData } from "@/components/lib/func/commom"; import RainRegForm from "@/components/ui/form/fcltReg/rainReg/rainRegForm"; -import BdstChgModal from "@/components/ui/modal/fcltReg/bdstChgModal/bdstChgModal"; import { BdstReg } from "@/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal"; import { ContentsDiv, @@ -21,66 +17,63 @@ import { import ListDiv from "@common/business_components/ui/list_div"; import { Button } from "@common/components"; import { useEffect, useState } from "react"; +import { Toaster } from "react-hot-toast"; export default function Home() { - const { rain, code, initRain, rainSearch, getRainData } = useWrStore((state) => { - return state; - }); - const { rainRes } = rain; + const { auth, rain, code, initRain, rainSearch, getRainData } = useWrStore( + (state) => { + return state; + } + ); + + const { regAuthrtYn, mdfcnAuthrtYn } = auth; + + + + const { rainRes, rainResCount } = rain; const [rainForm, setRainForm] = useState<customRainBdst>(); const [issNewReg, setIsNewReg] = useState<boolean>(false); const [pageNation, setPageNation] = useState(1); const [bdstTable, setbdstTable] = useState<customRainBdst[]>(); const [searchTxt, setSearchTxt] = useState<string>(); - const [bdstChgProps, setBdstChgProps] = useState<{ - isOpen: boolean; - chgBuilding: chgBuilding[]; - }>({ isOpen: false, chgBuilding: [] }); useEffect(() => { + setIsNewReg(false); if (!rain.isInit) { - initRain('41'); + initRain("41"); } }, [rain.isInit, initRain]); useEffect(() => { if (code.sggCd.length > 0) { - const rainData = tableData(rainRes, { code }).map( - (item: customRainBdst) => { - return { - ...item, - bdstChgYn: item.chgBuilding.length > 0 ? "Y" : "N", - }; - } - ); + if (rainRes && rainRes.length > 0) { + const rainData = tableData(rainRes, { code }).map( + (item: customRainBdst) => { + const archArea = + item?.archArea.split(".")[0] !== "-1" + ? item?.archArea.split(".")[0] + : "확인불가"; + return { + ...item, + archArea: formatCommaNumber(archArea), + bdstChgYn: item.chgBuilding[0]?.bdstChgNm != null ? "Y" : "N", + }; + } + ); - setbdstTable(rainData); + setbdstTable(rainData); + } else { + setbdstTable([]); + } } }, [code, rainRes]); - const cellClick: (val: number | undefined, headId: string) => void = ( - val, - headId - ) => { + const cellClick: (val: number | undefined) => void = (val) => { if (val !== undefined) { setRainForm(rainRes[val]); } - - if (headId === "bdstChgYn" && val !== undefined) { - if ( - rainRes[val]?.bdstInfoPk != null && - rainRes[val]?.chgBuilding.length > 0 - ) { - bdstChgModal(rainRes[val].chgBuilding); - } - } - }; - - const bdstChgModal = (chgBuilding: chgBuilding[]) => { - setBdstChgProps({ isOpen: true, chgBuilding: chgBuilding }); }; useEffect(() => { - console.log("getPageNation", pageNation); getRainData(pageNation, "41"); }, [pageNation]); @@ -93,7 +86,7 @@ export default function Home() { <SearchDiv> <SearchFormLeft> <SearchInput - className={"col-span-3 my-7 !text-3xl"} + className={"col-span-3 !text-3xl"} label="건물명" textValue={searchTxt || ""} setText={(e) => { @@ -104,8 +97,9 @@ export default function Home() { <Button className="" size={"sm"} - onClick={(e) => { - rainSearch(searchTxt || "", pageNation, "41"); + onClick={() => { + rainSearch(searchTxt || "", 1, "41"); + //rainSearch(searchTxt || "", pageNation, "41"); }} > 조회 @@ -113,7 +107,10 @@ export default function Home() { <Button size={"sm"} className="" - onClick={() => setIsNewReg(!issNewReg)} + onClick={() => { + setIsNewReg(!issNewReg); + }} + disabled={regAuthrtYn === "N"} > 신규등록 </Button> @@ -129,35 +126,40 @@ export default function Home() { <div className="col-span-4 h-full min-h-0"> <ListDiv className="min-h-0"> {/* <ScrollArea className="h-[60vh]"> */} - <UiTable - tableData={bdstTable || []} - headlist={rain.upHeadList} - pageSize={20} - total={10000} - headName={"건축물 목록"} - cellClick={(val, headId) => { - cellClick(val, headId); - }} - onPageChange={setPageNation} - > - </UiTable> + <UiTable + tableData={bdstTable || []} + headlist={rain.upHeadList} + pageSize={20} + total={rainResCount} + headName={"건축물 목록"} + cellClick={(val) => { + cellClick(val); + }} + onPageChange={setPageNation} + ></UiTable> {/* </ScrollArea> */} </ListDiv> </div> - <div className="col-span-2 max-h-full"> - + <div className="col-span-2 max-h-full"> <ListDiv className=""> - <ScrollArea className="h-[60vh]"> - <RainRegForm - rainForm={rainForm ?? ({} as customRainBdst)} - isModal={false} - ></RainRegForm> + <ScrollArea className="h-[65vh]"> + <RainRegForm + rainForm={rainForm ?? ({} as customRainBdst)} + isModal={false} + readonly={mdfcnAuthrtYn !== "Y"} + ></RainRegForm> </ScrollArea> </ListDiv> </div> </div> <UiModal - body={<BdstReg moType="RNB"></BdstReg>} + body={ + <BdstReg + moType="RNB" + isModal={issNewReg} + setIsModal={setIsNewReg} + ></BdstReg> + } bodyClass="w-[68vw] max-w-none h-[90vh]" open={issNewReg} onOpenChange={(open) => { @@ -165,16 +167,7 @@ export default function Home() { }} /> - <UiModal - bodyClass={""} - open={bdstChgProps.isOpen} - body={ - <BdstChgModal chgBuilding={bdstChgProps.chgBuilding}></BdstChgModal> - } - onOpenChange={(open) => { - setBdstChgProps({ ...bdstChgProps, isOpen: open }); - }} - /> + <Toaster position="top-center" /> </ContentsDiv> </> ); diff --git a/apps/wr/app/ruseFcltMng/fcltReg/rswtReg/page.tsx b/apps/wr/app/ruseFcltMng/fcltReg/rswtReg/page.tsx index 2931690b9..598ddfeca 100644 --- a/apps/wr/app/ruseFcltMng/fcltReg/rswtReg/page.tsx +++ b/apps/wr/app/ruseFcltMng/fcltReg/rswtReg/page.tsx @@ -1,41 +1,37 @@ "use client"; import { useWrStore } from "@/app/store"; -import type { chgBuilding } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; import type { customRswtBdst } from "@/app/store/ruseFcltMng/fcltReg/rswtReg"; import { tableData } from "@/components/lib/func/commom"; import RswtrRegForm from "@/components/ui/form/fcltReg/rswtrReg/rswtrRegForm"; -import BdstChgModal from "@/components/ui/modal/fcltReg/bdstChgModal/bdstChgModal"; import { BdstReg } from "@/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal"; import { ContentsDiv, ScrollArea, SearchDiv, SearchFormLeft, - SearchFormRight, SearchInput, Title, UiModal, - UiTable, + UiTable } from "@common/business_components"; import ListDiv from "@common/business_components/ui/list_div"; import { Button } from "@common/components"; import { useEffect, useState } from "react"; +import { Toaster } from "react-hot-toast"; export default function Home() { - const { code, rswt, initRswt, rswtBdstSearch, getRswtBdstData } = useWrStore( + const { auth,code, rswt, initRswt, rswtBdstSearch, getRswtBdstData } = useWrStore( (state) => state ); + const { regAuthrtYn, mdfcnAuthrtYn } = auth; + const [rswtForm, setRswtForm] = useState<customRswtBdst>(); const [searchTxt, setSearchTxt] = useState<string>(); const [issNewReg, setIsNewReg] = useState<boolean>(false); const [pageNation, setPageNation] = useState(1); const [selectVal, setSelect] = useState<number>(); const [bdstTable, setbdstTable] = useState<customRswtBdst[]>(); - const [bdstChgProps, setBdstChgProps] = useState<{ - isOpen: boolean; - chgBuilding: chgBuilding[]; - }>({ isOpen: false, chgBuilding: [] }); - const { rswtRes } = rswt; + const { rswtRes, rswtResCount } = rswt; useEffect(() => { if (!rswt.isInit) { initRswt("41"); @@ -43,42 +39,49 @@ export default function Home() { }, [rswt.isInit, initRswt]); useEffect(() => { if (code.sggCd.length > 0) { - const rswtData = tableData(rswtRes, { code }).map( - (item: customRswtBdst) => { - return { - ...item, - bdstChgYn: item.chgBuilding[0]?.bdstChgNm != null ? "Y" : "N", - bdstUsgCd: item.reuseBuilding[0]?.bdstUsgCd, - }; - } - ); - setbdstTable(rswtData); + + if (rswtRes?.length > 0) { + const rswtData = tableData(rswtRes, { code })?.map( + (item: customRswtBdst) => { + const archArea = + item?.archArea.split(".")[0] !== "-1" + ? item?.archArea.split(".")[0] + : "확인불가"; + return { + ...item, + archArea: archArea, + bdstChgYn: item.chgBuilding[0]?.bdstChgNm != null ? "Y" : "N", + bdstUsgCd: item.reuseBuilding[0]?.bdstUsgCd, + }; + } + ); + setbdstTable(rswtData); + setRswtForm(rswtRes[selectVal ?? 0]); + } else { + setbdstTable([]); + setRswtForm({} as customRswtBdst); + } + } + // }, [code, rswtRes, selectVal]); + + }, [code, rswtRes]); + useEffect(() => { + if (rswtRes && rswtRes.length > 0) { setRswtForm(rswtRes[selectVal ?? 0]); } - }, [code, rswtRes, selectVal]); + }, [rswtRes, selectVal]); + const cellClick: (val: number | undefined, headId: string) => void = ( - val, - headId + val ) => { if ((bdstTable ?? []).length > 0 && val !== undefined) { { setSelect(val); - setRswtForm(rswtRes[val]); } } - if (headId === "bdstChgYn" && val !== undefined) { - if ( - rswtRes[val]?.bdstInfoPk != null && - rswtRes[val]?.chgBuilding?.length > 0 - ) { - bdstChgModal(rswtRes[val].chgBuilding); - } - } - }; - const bdstChgModal = (chgBuilding: chgBuilding[]) => { - setBdstChgProps({ isOpen: true, chgBuilding: chgBuilding }); }; + useEffect(() => { getRswtBdstData(pageNation, "41"); }, [pageNation]); @@ -92,20 +95,18 @@ export default function Home() { <SearchDiv> <SearchFormLeft> <SearchInput - className="col-span-3 my-7" + className="col-span-3" label="건물명" textValue={searchTxt || ""} setText={(e) => { setSearchTxt(e); }} /> - </SearchFormLeft> - <SearchFormRight> - <div className="flex flex-row space-x-2"> + <div className="flex flex-row space-x-2 items-center"> <Button size={"sm"} - className="mr-4 " - onClick={(e) => { + className="" + onClick={() => { rswtBdstSearch(searchTxt || "", pageNation, "41"); }} > @@ -115,11 +116,14 @@ export default function Home() { size={"sm"} className="mr-4" onClick={() => setIsNewReg(!issNewReg)} + disabled={regAuthrtYn === "N"} > 신규등록 </Button> </div> - </SearchFormRight> + </SearchFormLeft> + {/* <SearchFormRight> + </SearchFormRight> */} </SearchDiv> </div> </div> @@ -131,19 +135,21 @@ export default function Home() { tableData={bdstTable || []} headlist={rswt.upHeadList} pageSize={20} - total={10000} + total={rswtResCount} headName={"건축물 목록"} cellClick={cellClick} onPageChange={setPageNation} + index={0} ></UiTable> </ListDiv> </div> <div className="col-span-2 max-h-full"> - <ListDiv> - <ScrollArea className="h-[60vh]"> + <ListDiv className=""> + <ScrollArea className="h-[65vh]"> <RswtrRegForm rswtForm={rswtForm ?? ({} as customRswtBdst)} isModal={false} + readonly={mdfcnAuthrtYn === "N"} /> </ScrollArea> </ListDiv> @@ -151,23 +157,21 @@ export default function Home() { </div> <UiModal - body={<BdstReg moType="RST"></BdstReg>} + body={ + <BdstReg + moType="RST" + setIsModal={setIsNewReg} + isModal={issNewReg} + ></BdstReg> + } bodyClass="w-[68vw] max-w-none h-[90vh]" open={issNewReg} onOpenChange={(open) => { setIsNewReg(open); }} /> - <UiModal - bodyClass={""} - open={bdstChgProps.isOpen} - body={ - <BdstChgModal chgBuilding={bdstChgProps.chgBuilding}></BdstChgModal> - } - onOpenChange={(open) => { - setBdstChgProps({ ...bdstChgProps, isOpen: open }); - }} - /> + + <Toaster position="top-center" /> </ContentsDiv> </> ); diff --git a/apps/wr/app/ruseFcltMng/fcltSttn/htdflSttn/page.tsx b/apps/wr/app/ruseFcltMng/fcltSttn/htdflSttn/page.tsx index e43d5b04f..46528bcf7 100644 --- a/apps/wr/app/ruseFcltMng/fcltSttn/htdflSttn/page.tsx +++ b/apps/wr/app/ruseFcltMng/fcltSttn/htdflSttn/page.tsx @@ -3,60 +3,93 @@ import { useWrStore } from "@/app/store"; import { btnClass } from "@/app/store/common/code"; import type { - chgBuilding, - customRainBdst, + customRainBdst } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; import { excelDownload, - getCityOrSgg, - tableData, + getCity, + tableData } from "@/components/lib/func/commom"; +import type { objSearchType } from "@/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal"; import HeadChModal from "@/components/ui/modal/sttn/headChModal/headChModal"; import SttnDetailModal from "@/components/ui/modal/sttn/sttnDetailModal/sttnDetailModal"; import { ContentsDiv, - SearchBaseForm, + SearchDiv, + SearchFormLeft, + SearchFormRight, + SearchInput, + SelectBox, Title, UiModal, UiTable, - type ReturnType, + type SelectDataType } from "@common/business_components/ui"; import ListDiv from "@common/business_components/ui/list_div"; -import { Button } from "@common/components/ui"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { excelHtdflSttnParsing } from "@/app/store/ruseFcltMng/fcltSttn/htdflSttn/excel"; import { HeadMakeColSpan } from "@common/business_components/ui/table/HeadMakeColSpan"; +import { Button, Label } from "@common/components/ui"; +import { useCallback, useEffect, useRef, useState } from "react"; +import toast, { Toaster } from "react-hot-toast"; +import { useTranslation } from "react-i18next"; export default function HtdflSttn() { const { code, htdflSttn, htdflSttnSearch, initHtdfl } = useWrStore( (state) => state ); - const { htdflSttnRes } = htdflSttn; - - const ref = useRef<ReturnType>({} as ReturnType); + const { htdflSttnRes, htdflSttnResCount } = htdflSttn; + const isDownloadTriggered = useRef(false); + const { t } = useTranslation('base'); const [pageNation, setPageNation] = useState(1); const [isChHead, setIsChHead] = useState(false); + const [resetPage, isResetPage] = useState(false); const [bdstTable, setBdstTable] = useState<customRainBdst[]>(); + const [selectSgg, setSelectSgg] = useState<SelectDataType[] | []>() + const [objSearch, setObjSearch] = useState<objSearchType>({ sgg: "", city: "" }); const [detailModal, setDetailModal] = useState<{ isOpen: boolean; data: customRainBdst | undefined; }>({ data: undefined, isOpen: false }); - const [bdstChgProps, setBdstChgProps] = useState<{ - isOpen: boolean; - chBuilding: chgBuilding[]; - }>({ isOpen: false, chBuilding: [] }); - - const codeData = getCityOrSgg(ref); - - const excelClick = useCallback(() => { - if (codeData) htdflSttnSearch(1, "10000", codeData); - }, [htdflSttnSearch, codeData]); + // const [bdstChgProps, setBdstChgProps] = useState<{ + // isOpen: boolean; + // chBuilding: chgBuilding[]; + // }>({ isOpen: false, chBuilding: [] }); + const [fcltNm, setFcltNm] = useState<string>(""); useEffect(() => { - if (codeData != null) htdflSttnSearch(pageNation, "20", codeData); + htdflSttnSearch(false, pageNation, "20", String(getConValue()), fcltNm, code.cityCd, code.sggCd); + isResetPage(false) }, [pageNation]); + const getSelCity = (val: string | number) => { + const cityValue = String(val); + + if (cityValue === "0") { + setSelectSgg([]); + setObjSearch({ city: cityValue, sgg: "" }); + } else { + const data = getCity(cityValue, code.sggCd); + setSelectSgg(data); + + const firstSggVal = data.find(item => item.val === "0")?.val || (data[0]?.val ?? ""); + setObjSearch({ city: cityValue, sgg: firstSggVal }); + } + }; + + const getSgg = (val: string | number) => { + setObjSearch({ ...objSearch, sgg: String(val) }) + }; + + const getConValue = () => { + if (!objSearch.city || objSearch.city === "0") { + return "0"; + } + if (objSearch.sgg && objSearch.sgg !== "0") { + return objSearch.sgg; + } + return objSearch.city; + }; + const cellClick = (index: number | undefined, headId: string) => { if ( headId !== "chBuildingYn" && @@ -66,43 +99,46 @@ export default function HtdflSttn() { setDetailModal({ isOpen: true, data: bdstTable[index] }); } }; + + const excelClick = useCallback(() => { + isDownloadTriggered.current = true; + htdflSttnSearch(true, 1, String(htdflSttnResCount), String(getConValue()), fcltNm, code.cityCd, code.sggCd); + }, [htdflSttnSearch, objSearch, code.cityCd, code.sggCd, fcltNm, htdflSttnResCount]); + useEffect(() => { - const excelDown = async () => { - if (htdflSttn.excelList.length > 0) { - if (htdflSttn.excelList.length === 0) return; - const excelData = await tableData(htdflSttn.excelList, { code }); - const htdflExcel = excelHtdflSttnParsing(excelData, code.cityCd, code.sggCd); - excelDownload( - htdflExcel as [[]], - "온배수시설 현황", - "sample_htdflSttn.xlsx", - "A1:S3" - ); - } - }; - excelDown(); + if (!isDownloadTriggered.current) { + return; + } + if (htdflSttn.excelList.length > 0) { + excelDownload( + htdflSttn.excelList, + "온배수시설 현황", + "sample_htdflSttn.xlsx", + "A1:R3" + ); + } else { + toast.error(t('excelDownErrMsg')); + } + isDownloadTriggered.current = false; }, [htdflSttn.excelList, code]); useEffect(() => { if (!htdflSttn.isInit) { - initHtdfl(codeData ?? "0"); + initHtdfl(false, 1, "20", "0", fcltNm, code.cityCd, code.sggCd); } }, [htdflSttn.isInit, initHtdfl]); useEffect(() => { - if (htdflSttnRes.length > 0 && code.cityCd.length > 0) { - const data = tableData(htdflSttnRes, { code }); - const chData = data - .map((item: any) => ({ - ...item, - ...item.reuseBuilding[0], - ...item.reuseBuilding[0].htdflObj, - chBuildingYn: item.chgBuilding[0]?.bdstChgNm !== null ? "Y" : "N", - })) - .filter((item: any) => item.htdflObj !== undefined); - - setBdstTable(chData); - } + const data = tableData(htdflSttnRes, { code }); + const chData = data + .map((item: any) => ({ + ...item, + ...item.reuseBuilding[0], + ...item.reuseBuilding[0].htdflObj, + chBuildingYn: item.chgBuilding[0]?.bdstChgNm !== null ? "Y" : "N", + })) + .filter((item: any) => item.htdflObj !== undefined); + setBdstTable(chData); }, [code, htdflSttnRes, htdflSttn.htdflSttHeadList]); return ( @@ -111,55 +147,88 @@ export default function HtdflSttn() { <div className="grid grid-cols-4 gap-4 "> <div className="col-span-4"> <Title title={"온배수 시설 현황"} /> - <SearchBaseForm - selectSidoData={code.cityCd} - selectSigunData={code.sggCd} - selectRegionData={code.rvbsnCd} - selectUnitWaterShedData={code.lgbsnCd} - selectMiddleRegionData={code.mdmbsnCd} - selectEnvPartData={code.lclgvrnmntCd} - rightChildren={<Button - className={btnClass} - onClick={() => { - setIsChHead(true); - } } - > - 조회항목설정 - </Button>} - leftChildren={undefined} - queryClick={function (returnType: ReturnType): void { - // '{partValue: 'A', sidoValue: '0', sigunValue: '0', regionValue: '0', unitWaterShedValue: '0', …}' - let con; - if (returnType.sigunValue != "0") { - con = returnType.sigunValue; - } else { - con = returnType.sidoValue; - } - htdflSttnSearch(pageNation, "20", con); - } } - excelClick={excelClick} - initClick={function (): void { } } - btnClass={btnClass} - ref={ref} isAdmin={false} /> + + <SearchDiv> + <SearchFormLeft> + <div className="col-span-10 flex flex-row"> + <SelectBox + className="col-span-2 w-1/6 h-full text-2xl" + label="시도" + selectClass={""} + selectArray={code.cityCd} + onSelectValue={getSelCity} + /> + <SelectBox + className="col-span-2 w-1/6 h-full text-2xl ml-4" + label="시군구" + selectClass={""} + selectArray={selectSgg || []} + value={objSearch.sgg} // value props 추가 + onSelectValue={getSgg} + /> + <div className="col-span-2 flex flex-row space-x-2 ml-3 mt-2"> + <Label className="w-28 mt-2 text-xl"> + {"시설명"} + </Label> + <SearchInput + className={"w-full h-full text-2xl"} + label={""} + textValue={fcltNm || ""} + setText={setFcltNm} + /> + </div> + </div> + </SearchFormLeft> + <SearchFormRight + className="w-2/12 mx-5 flex flex-col items-center justify-center space-x-2 "> + <div className="flex flex-row space-x-2 items-center"> + <Button + size={"sm"} + onClick={() => { + htdflSttnSearch(false, 1, "20", String(getConValue()), fcltNm, code.cityCd, code.sggCd); + isResetPage(true) + }} + > + 조회 + </Button> + <Button + size={"sm"} + className={`${btnClass} green`} + onClick={excelClick} + > + 다운로드 + </Button> + <Button + size={"sm"} + className={btnClass} + onClick={() => { + setIsChHead(true); + }} + > + 조회항목설정 + </Button> + </div> + </SearchFormRight> + </SearchDiv> </div> </div> <div className="grid grid-cols-6 gap-4 "> <div className="col-span-6"> <ListDiv> <UiTable - headName={[]} + headName={undefined} tableData={bdstTable || []} - children={ - <HeadMakeColSpan upHeadList={htdflSttn.htdflSttNodeList} /> - } headlist={htdflSttn.htdflSttHeadList} pageSize={20} - total={100000} + total={htdflSttnResCount || 0} cellClick={(index: number | undefined, headId: string) => { cellClick(index ?? undefined, headId); }} onPageChange={setPageNation} - ></UiTable> + resetPageNo={resetPage} + > + <HeadMakeColSpan upHeadList={htdflSttn.htdflSttNodeList} /> + </UiTable> </ListDiv> </div> </div> @@ -188,6 +257,7 @@ export default function HtdflSttn() { body={<SttnDetailModal type={"HTB"} formObj={detailModal.data} />} /> </ContentsDiv> + <Toaster position="top-center" /> </> ); } diff --git a/apps/wr/app/ruseFcltMng/fcltSttn/pbnstSttn/page.tsx b/apps/wr/app/ruseFcltMng/fcltSttn/pbnstSttn/page.tsx index 7a7a125d2..eb7a398b1 100644 --- a/apps/wr/app/ruseFcltMng/fcltSttn/pbnstSttn/page.tsx +++ b/apps/wr/app/ruseFcltMng/fcltSttn/pbnstSttn/page.tsx @@ -1,6 +1,11 @@ "use client"; import { useWrStore } from "@/app/store"; import { btnClass } from "@/app/store/common/code"; +import type { customPbnst } from "@/app/store/ruseFcltMng/fcltReg/pbnstReg/type"; +import type { WaterReuseFacility } from "@/app/store/ruseFcltMng/fcltSttn/pbnstSttn/type"; +import { excelDownload, yearOptions } from "@/components/lib/func/commom"; +import HeadChModal from "@/components/ui/modal/sttn/headChModal/headChModal"; +import { PbnstSttnModal } from "@/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal"; import { ContentsDiv, SearchBaseForm, @@ -12,15 +17,9 @@ import { type ReturnType, } from "@common/business_components/ui"; import ListDiv from "@common/business_components/ui/list_div"; -import { Button, Label } from "@common/components/ui"; -import HeadChModal from "@/components/ui/modal/sttn/headChModal/headChModal"; -import { useCallback, useEffect, useRef, useState } from "react"; import { HeadMakeColSpan } from "@common/business_components/ui/table/HeadMakeColSpan"; -import type { customPbnst } from "@/app/store/ruseFcltMng/fcltReg/pbnstReg/type"; -import { excelDownload, yearOptions } from "@/components/lib/func/commom"; -import SearchInputText from "@/components/ui/searchInput/searchInput"; -import type { WaterReuseFacility } from "@/app/store/ruseFcltMng/fcltSttn/pbnstSttn/type"; -import { PbnstSttnModal } from "@/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal"; +import { Button, Label } from "@common/components/ui"; +import { useCallback, useEffect, useRef, useState } from "react"; export default function PbnstSttn() { const { @@ -29,9 +28,11 @@ export default function PbnstSttn() { pbnstSttnSearch, pbnSttnInit, getPbnStnDetail, - pbnNmSearch, + getFlcts, + resetPbnstDetail } = useWrStore((state) => state); + const { pbnstSttnRes, fclts, pbnstSttnResCount } = pbnstSttn; const ref = useRef<ReturnType>({} as ReturnType); const [searchYear, setSearchYear] = useState<string>( String(new Date().getFullYear()) @@ -46,21 +47,25 @@ export default function PbnstSttn() { const [pbnRuseNm, setPbnRuseNm] = useState<string>(""); const excelClick = useCallback(() => { - pbnstSttnSearch("1", "10000", ref.current, "", searchYear); - }, [pbnstSttnSearch, searchYear, ref.current]); + pbnstSttnSearch( + true, + String(1), + "100000", + ref.current, + pbnRuseNm, + searchYear, + fclts + ); + }, [pbnstSttnSearch, fclts, pbnRuseNm]); const cellCLick = useCallback( (index: number | undefined) => { - if ( - index !== undefined && - pbnstSttn.pbnstSttnRes && - pbnstSttn.pbnstSttnRes[index] - ) { - const pbnstRuseFcltPk = pbnstSttn.pbnstSttnRes[index].pbnstRuseFcltPk; - getPbnStnDetail(pbnstRuseFcltPk); + if (index !== undefined && pbnstSttnRes && pbnstSttnRes[index]) { + const pbnstRuseFcltId = pbnstSttnRes[index].pbnstRuseFcltId; + getPbnStnDetail(pbnstRuseFcltId); } }, - [getPbnStnDetail, pbnstSttn.pbnstSttnRes] + [getPbnStnDetail, pbnstSttnRes] ); useEffect(() => { @@ -69,39 +74,37 @@ export default function PbnstSttn() { pbnstSttn.excelList, "하수재이용시설 현황", "sample_pbnstSttn.xlsx", - "A1:U2" + "A1:U2", + 3 ); } }, [pbnstSttn.excelList]); useEffect(() => { + if (!pbnstSttn.isInit) { - pbnSttnInit("1", "13", ref.current, searchYear); + getFlcts(); } }, [pbnstSttn.isInit, pbnSttnInit, ref.current]); useEffect(() => { - setBdstTable(pbnstSttn.pbnstSttnRes); - }, [pbnstSttn.pbnstSttnRes]); - - // useEffect(() => { - // pbnstSttnSearch("1", "20", ref.current, pbnRuseNm, searchYear); - // }, [pageNation, ref.current, pbnRuseNm, searchYear, pbnstSttnSearch]); - - // useEffect(() => { - // if (pbnRuseNm.length > 1) - // pbnNmSearch(String(pageNation), "23", ref.current, pbnRuseNm, searchYear); - // }, [pbnRuseNm,pageNation]); + if (pbnstSttnRes && pbnstSttnRes.length > 0) { + setBdstTable(pbnstSttnRes); + } else { + setBdstTable([]); + } + }, [pbnstSttnRes]); useEffect(() => { - if (pbnstSttn.pbnstDetail.pbnstRuseFcltPk) + pbnSttnInit("1", "15", ref.current, searchYear, fclts); + }, [fclts]); + + useEffect(() => { + if (pbnstSttn.pbnstDetail.pbnstRuseFcltId) setPbnDetaileData({ isModal: true, data: pbnstSttn.pbnstDetail, }); }, [pbnstSttn.pbnstDetail]); - useEffect(() => { - console.log("pbnDetaileData.isModal ", pbnDetaileData.isModal); - }, [pbnDetaileData.isModal]); return ( <> <ContentsDiv> @@ -115,6 +118,10 @@ export default function PbnstSttn() { selectUnitWaterShedData={code.lgbsnCd} selectMiddleRegionData={code.mdmbsnCd} selectEnvPartData={code.lclgvrnmntCd} + buttonOrder={["query", "init", "excel"]} + rightClassName="ml-auto mr-6 flex items-center space-x-1" + leftClassName="flex gap-5 w-9/12 m-4" + leftSelectClassName="w-1/4" rightChildren={ <Button className={btnClass} @@ -127,34 +134,44 @@ export default function PbnstSttn() { } leftChildren={ <div className="col-span-3 flex items-center"> - <SelectBox - className="w-48 h-full text-2xl" - label="연도" - selectClass={"w-full "} - selectArray={yearOptions} - value={searchYear} - onSelectValue={(val: string | number) => - setSearchYear(String(val)) - } - /> - <Label className="w-64 mt-2 mx-4 text-xl text-gray-600"> - {"하수처리시설명 "} - </Label> - <SearchInput - className={"text-2xl w-full ㅍ"} - label="" - textValue={pbnRuseNm || ""} - setText={(e) => { - setPbnRuseNm(e || ""); - }} - /> - </div> + <SelectBox + className="w-48 h-full text-2xl" + label="연도" + selectClass={"w-full "} + selectArray={yearOptions} + value={searchYear} + onSelectValue={(val: string | number) => + setSearchYear(String(val)) + } + /> + <Label className="w-full mt-2 ml-4 text-xl text-gray-600"> + {"하수처리시설명"} + </Label> + <SearchInput + className={"text-2xl w-full"} + label="" + textValue={pbnRuseNm || ""} + setText={(e) => { + setPbnRuseNm(e || ""); + }} + /> + </div> } queryClick={function (returnType: ReturnType): void { - pbnstSttnSearch("1", "20", returnType, pbnRuseNm, searchYear); + pbnstSttnSearch( + false, + "1", + "15", + returnType, + pbnRuseNm, + searchYear, + fclts + ); }} excelClick={excelClick} - initClick={function (): void {}} + initClick={function (): void { + setPbnRuseNm(""); + }} btnClass={btnClass} ref={ref} isAdmin={false} @@ -165,27 +182,28 @@ export default function PbnstSttn() { <div className="col-span-6"> <ListDiv> <UiTable - headName={[]} + headName={undefined} tableData={bdstTable || []} - children={ - <HeadMakeColSpan upHeadList={pbnstSttn.pbnstSttNodeList} /> - } headlist={pbnstSttn.pbnstSttHeadList} - pageSize={20} - total={100000} - cellClick={(index: number | undefined, headId: string) => { + pageSize={15} + total={pbnstSttnResCount} + cellClick={(index: number | undefined) => { cellCLick(index); }} onPageChange={(page: number) => { pbnstSttnSearch( + false, String(page), - String(13), + String(15), ref.current, pbnRuseNm, - searchYear + searchYear, + fclts ); }} - ></UiTable> + > + <HeadMakeColSpan upHeadList={pbnstSttn.pbnstSttNodeList} /> + </UiTable> </ListDiv> </div> </div> @@ -214,7 +232,10 @@ export default function PbnstSttn() { bodyClass="w-[68vw] max-w-none h-[90vh]" open={pbnDetaileData.isModal} onOpenChange={(open) => { - setPbnDetaileData({ data: {} as customPbnst, isModal: false }); + //TODO:OPEN 상태 확인필요 + if (!open) + setPbnDetaileData({ data: {} as customPbnst, isModal: open }); + resetPbnstDetail(); }} /> </ContentsDiv> diff --git a/apps/wr/app/ruseFcltMng/fcltSttn/rainSttn/page.tsx b/apps/wr/app/ruseFcltMng/fcltSttn/rainSttn/page.tsx index 631e24350..4f83ae576 100644 --- a/apps/wr/app/ruseFcltMng/fcltSttn/rainSttn/page.tsx +++ b/apps/wr/app/ruseFcltMng/fcltSttn/rainSttn/page.tsx @@ -1,118 +1,191 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ "use client"; import { useWrStore } from "@/app/store"; import { btnClass } from "@/app/store/common/code"; +import type { + customRainBdst, +} from "@/app/store/ruseFcltMng/fcltReg/rainReg"; import { excelDownload, - getCityOrSgg, + getCity, tableData, } from "@/components/lib/func/commom"; +import type { objSearchType } from "@/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal"; import HeadChModal from "@/components/ui/modal/sttn/headChModal/headChModal"; +import SttnDetailModal from "@/components/ui/modal/sttn/sttnDetailModal/sttnDetailModal"; import { ContentsDiv, - SearchBaseForm, + SearchDiv, + SearchFormLeft, + SearchFormRight, + SearchInput, + SelectBox, Title, UiModal, UiTable, - type ReturnType, + type SelectDataType, } from "@common/business_components/ui"; import ListDiv from "@common/business_components/ui/list_div"; -import { Button } from "@common/components/ui"; -import { useCallback, useEffect, useRef, useState } from "react"; -import type { - chgBuilding, - customRainBdst, -} from "@/app/store/ruseFcltMng/fcltReg/rainReg"; import { HeadMakeColSpan } from "@common/business_components/ui/table/HeadMakeColSpan"; -import SttnDetailModal from "@/components/ui/modal/sttn/sttnDetailModal/sttnDetailModal"; -import BdstChgModal from "@/components/ui/modal/fcltReg/bdstChgModal/bdstChgModal"; +import { Button, Label } from "@common/components/ui"; +import { useCallback, useEffect, useRef, useState } from "react"; +import toast, { Toaster } from "react-hot-toast"; +import { useTranslation } from "react-i18next"; export default function RainSttn() { const { code, rainSttn, rainSttnSearch, rainSttnInit } = useWrStore( (state) => state ); - const { rainSttnRes } = rainSttn; - const ref = useRef<ReturnType>({} as ReturnType); + const { rainSttnRes, rainSttnResCount } = rainSttn; const [pageNation, setPageNation] = useState(1); const [isChHead, setIsChHead] = useState(false); const [bdstTable, setBdstTable] = useState<customRainBdst[]>(); + const [selectSgg, setSelectSgg] = useState<SelectDataType[] | []>(); + const [resetPage, isResetPage] = useState(false); + const isDownloadTriggered = useRef(false); + const [objSearch, setObjSearch] = useState<objSearchType>({ + sgg: "", + city: "", + }); const [detailModal, setDetailModal] = useState<{ isOpen: boolean; data: customRainBdst | undefined; }>({ data: undefined, isOpen: false }); - const [bdstChgProps, setBdstChgProps] = useState<{ - isOpen: boolean; - chBuilding: chgBuilding[]; - }>({ isOpen: false, chBuilding: [] }); - const codeData = getCityOrSgg(ref); + const [fcltNm, setFcltNm] = useState<string>(""); + const { t } = useTranslation("base"); - const cellClick = (index: number | undefined, headId: string) => { - if ( - headId !== "chBuildingYn" && - index !== undefined && - bdstTable !== undefined - ) { + const cellClick = (index: number | undefined) => { + if (index !== undefined && bdstTable !== undefined) { setDetailModal({ isOpen: true, data: bdstTable[index] }); - } else if ( - headId === "chBuildingYn" && - index !== undefined && - bdstTable !== undefined - ) { - if ( - bdstTable[index] && - bdstTable[index]?.chgBuilding[0]?.bdstChgNm !== null - ) { - setBdstChgProps({ - isOpen: true, - chBuilding: bdstTable[index].chgBuilding, - }); - } } }; + const getSelCity = (val: string | number) => { + const cityValue = String(val); + if (cityValue === "0") { + setSelectSgg([]); + setObjSearch({ city: cityValue, sgg: "" }); + } else { + const data = getCity(cityValue, code.sggCd); + setSelectSgg(data); + const firstSggVal = + data.find((item) => item.val === "0")?.val || (data[0]?.val ?? ""); + setObjSearch({ city: cityValue, sgg: firstSggVal }); + } + }; + + const getSgg = (val: string | number) => { + setObjSearch({ ...objSearch, sgg: String(val) }); + }; + + const getConValue = () => { + if (!objSearch.city || objSearch.city === "0") { + return "0"; + } + if (objSearch.sgg && objSearch.sgg !== "0") { + return objSearch.sgg; + } + return objSearch.city; + }; + useEffect(() => { if (!rainSttn.isInit) { - rainSttnInit(codeData ?? "0"); + rainSttnInit(false, 1, "20", "0", fcltNm, code.cityCd, code.sggCd); } }, [rainSttn.isInit, rainSttnInit]); useEffect(() => { - if (rainSttnRes.length > 0 && code.cityCd.length > 0) { - const data = tableData(rainSttnRes, { code }); - const chData = data - .map((item: any) => ({ + const data = tableData(rainSttnRes, { code }); + const chData = data + .map((item: any) => { + return { ...item, ...item.reuseBuilding[0], ...item.reuseBuilding[0].rainyObj, + dtfcltCpct: + item.reuseBuilding[0].rainyObj.dtfcltCpct === -1 + ? "확인불가" + : item.reuseBuilding[0].rainyObj.dtfcltCpct, + instlCst: + item.reuseBuilding[0].instlCst === -1 + ? "확인불가" + : item.reuseBuilding[0].instlCst, + instlCstSprtAmt: + item.reuseBuilding[0].instlCstSprtAmt === -1 + ? "확인불가" + : item.reuseBuilding[0].instlCstSprtAmt, chBuildingYn: item.chgBuilding[0]?.bdstChgNm !== null ? "Y" : "N", - })) - .filter((item: any) => item.rainyObj !== undefined); - - setBdstTable(chData); - } - }, [code, rainSttnRes, rainSttn.rainSttHeadList]); + pntfColArea: + item.reuseBuilding[0].rainyObj.pntfColArea === -1 + ? "확인불가" + : item.reuseBuilding[0].rainyObj.pntfColArea, + filtrTrtmfctCpct: + item.reuseBuilding[0].rainyObj.filtrTrtmfctCpct === -1 + ? "확인불가" + : item.reuseBuilding[0].rainyObj.filtrTrtmfctCpct, + // archArea: + // Math.floor(item.archArea) === -1 ? "확인불가" : Math.trunc(item.archArea) ? item.archArea, + archArea: isNaN(Number(item.archArea)) + ? item.archArea + : Math.floor(Number(item.archArea)) === -1 + ? '확인불가' + : Math.trunc(Number(item.archArea)) + }; + }) + .filter((item: any) => item.rainyObj !== undefined); + setBdstTable(chData); + }, [code, rainSttnRes]); useEffect(() => { - if (codeData != null) rainSttnSearch(pageNation, "20", codeData, code.cityCd, code.sggCd); + rainSttnSearch( + false, + pageNation, + "20", + String(getConValue()), + fcltNm, + code.cityCd, + code.sggCd + ); + isResetPage(false); }, [pageNation]); useEffect(() => { + if (!isDownloadTriggered.current) { + return; + } if (rainSttn.excelList.length > 0) { - if (rainSttn.excelList.length === 0) return; excelDownload( rainSttn.excelList, "빗물재이용시설 현황", "sample_rainSttn.xlsx", "A1:W3" ); + } else { + toast.error(t("excelDownErrMsg")); } + isDownloadTriggered.current = false; }, [rainSttn.excelList]); const excelClick = useCallback(() => { - if (codeData) rainSttnSearch(1, "10000", codeData, code.cityCd, code.sggCd); - }, [rainSttnSearch, codeData, code.cityCd, code.sggCd]); + isDownloadTriggered.current = true; + rainSttnSearch( + true, + 1, + String(rainSttnResCount), + String(getConValue()), + fcltNm, + code.cityCd, + code.sggCd + ); + }, [ + rainSttnSearch, + objSearch, + code.cityCd, + code.sggCd, + fcltNm, + rainSttnResCount, + ]); return ( <> @@ -120,59 +193,94 @@ export default function RainSttn() { <div className="grid grid-cols-4 gap-4 "> <div className="col-span-4"> <Title title={"빗물재이용 시설 현황"} /> - <SearchBaseForm - selectSidoData={code.cityCd} - selectSigunData={code.sggCd} - selectRegionData={code.rvbsnCd} - selectUnitWaterShedData={code.lgbsnCd} - selectMiddleRegionData={code.mdmbsnCd} - selectEnvPartData={code.lclgvrnmntCd} - rightChildren={ - <Button - className={btnClass} - onClick={() => { - setIsChHead(true); - }} - > - 조회항목설정 - </Button> - } - leftChildren={undefined} - queryClick={function (returnType: ReturnType): void { - // '{partValue: 'A', sidoValue: '0', sigunValue: '0', regionValue: '0', unitWaterShedValue: '0', …}' - let con; - if (returnType.sigunValue != "0") { - con = returnType.sigunValue; - } else { - con = returnType.sidoValue; - } - rainSttnSearch(1, "20", con, code.cityCd, code.sggCd); - }} - excelClick={excelClick} - initClick={function (): void { }} - btnClass={btnClass} - ref={ref} - isAdmin={false} - /> + <SearchDiv> + <SearchFormLeft> + <div className="col-span-10 flex flex-row"> + <SelectBox + className="col-span-2 w-1/6 h-full text-2xl" + label="시도" + selectClass={""} + selectArray={code.cityCd} + onSelectValue={getSelCity} + /> + <SelectBox + className="col-span-2 w-1/6 h-full text-2xl ml-4" + label="시군구" + selectClass={""} + selectArray={selectSgg || []} + value={objSearch.sgg} // value props 추가 + onSelectValue={getSgg} + /> + <div className="col-span-2 flex flex-row space-x-2 ml-3 mt-2"> + <Label className="w-28 mt-2 text-xl"> + {"시설명"} + </Label> + <SearchInput + className={"w-full text-2xl"} + label={""} + textValue={fcltNm || ""} + setText={setFcltNm} + /> + </div> + </div> + </SearchFormLeft> + <SearchFormRight className="w-2/12 mx-5 flex flex-col items-center justify-center space-x-2 "> + <div className="flex flex-row space-x-2 items-center"> + <Button + size={"sm"} + onClick={() => { + rainSttnSearch( + false, + 1, + "20", + String(getConValue()), + fcltNm, + code.cityCd, + code.sggCd + ); + isResetPage(true); + }} + > + 조회 + </Button> + <Button + size={"sm"} + className={`${btnClass} green`} + onClick={excelClick} + > + 다운로드 + </Button> + <Button + size={"sm"} + className={btnClass} + onClick={() => { + setIsChHead(true); + }} + > + 조회항목설정 + </Button> + </div> + </SearchFormRight> + </SearchDiv> </div> </div> <div className="grid grid-cols-6 gap-4 "> <div className="col-span-6"> <ListDiv> <UiTable - headName={[]} + headName={undefined} tableData={bdstTable || []} - children={ - <HeadMakeColSpan upHeadList={rainSttn.rainSttNodeList} /> - } headlist={rainSttn.rainSttHeadList} pageSize={20} - total={100000} - cellClick={(index: number | undefined, headId: string) => { - cellClick(index ?? undefined, headId); + total={rainSttnResCount || 0} + cellClick={(index: number | undefined) => { + cellClick(index ?? undefined); }} onPageChange={setPageNation} - ></UiTable> + resetPageNo={resetPage} + > + <HeadMakeColSpan upHeadList={rainSttn.rainSttNodeList} /> + </UiTable> </ListDiv> </div> </div> @@ -200,17 +308,8 @@ export default function RainSttn() { bodyClass={"w-[40vw] max-w-none max-h-[calc(100%-70px)]"} body={<SttnDetailModal type={"RNB"} formObj={detailModal.data} />} /> - <UiModal - bodyClass={""} - open={bdstChgProps.isOpen} - body={ - <BdstChgModal chgBuilding={bdstChgProps.chBuilding}></BdstChgModal> - } - onOpenChange={(open) => { - setBdstChgProps({ ...bdstChgProps, isOpen: open }); - }} - /> </ContentsDiv> + <Toaster position="top-center" /> </> ); } diff --git a/apps/wr/app/ruseFcltMng/fcltSttn/rswtSttn/page.tsx b/apps/wr/app/ruseFcltMng/fcltSttn/rswtSttn/page.tsx index 7a3444a9b..699d14d33 100644 --- a/apps/wr/app/ruseFcltMng/fcltSttn/rswtSttn/page.tsx +++ b/apps/wr/app/ruseFcltMng/fcltSttn/rswtSttn/page.tsx @@ -1,28 +1,33 @@ "use client"; import { useWrStore } from "@/app/store"; import { btnClass } from "@/app/store/common/code"; -import type { chgBuilding } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; import type { customRswtBdst } from "@/app/store/ruseFcltMng/fcltReg/rswtReg"; import { excelDownload, - getCityOrSgg, - tableData, + getCity, + tableData } from "@/components/lib/func/commom"; -import BdstChgModal from "@/components/ui/modal/fcltReg/bdstChgModal/bdstChgModal"; +import type { objSearchType } from "@/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal"; import HeadChModal from "@/components/ui/modal/sttn/headChModal/headChModal"; import SttnDetailModal from "@/components/ui/modal/sttn/sttnDetailModal/sttnDetailModal"; import { ContentsDiv, - SearchBaseForm, + SearchDiv, + SearchFormLeft, + SearchFormRight, + SearchInput, + SelectBox, Title, UiModal, UiTable, - type ReturnType, + type SelectDataType } from "@common/business_components/ui"; import ListDiv from "@common/business_components/ui/list_div"; import { HeadMakeColSpan } from "@common/business_components/ui/table/HeadMakeColSpan"; -import { Button } from "@common/components/ui"; +import { Button, Label } from "@common/components/ui"; import { useCallback, useEffect, useRef, useState } from "react"; +import toast, { Toaster } from "react-hot-toast"; +import { useTranslation } from "react-i18next"; export default function RswtSttn() { const { code, rswtSttn, rswtSttnSearch, rswtSttnInit } = useWrStore( @@ -30,81 +35,105 @@ export default function RswtSttn() { ); const [pageNation, setPageNation] = useState(1); - const { rswtSttnRes } = rswtSttn; + const { rswtSttnRes, rswtSttnResCount } = rswtSttn; const [isChHead, setIsChHead] = useState(false); - const ref = useRef<ReturnType>({} as ReturnType); + const [resetPage, isResetPage] = useState(false); const [bdstTable, setBdstTable] = useState<customRswtBdst[]>(); + const isDownloadTriggered = useRef(false); + const [selectSgg, setSelectSgg] = useState<SelectDataType[] | []>() + const [objSearch, setObjSearch] = useState<objSearchType>({ sgg: "", city: "" }); const [detailModal, setDetailModal] = useState<{ isOpen: boolean, data: customRswtBdst | undefined }>({ data: undefined, isOpen: false }) - const [bdstChgProps, setBdstChgProps] = useState<{ - isOpen: boolean; - chBuilding: chgBuilding[]; - }>({ isOpen: false, chBuilding: [] }); + const [fcltNm, setFcltNm] = useState<string>(""); + const { t } = useTranslation('base'); + const cellClick = (index: number | undefined) => { - const codeCityOrSgg = getCityOrSgg(ref); - - const cellClick = (index: number | undefined, headId: string) => { - - if (headId !== 'chBuildingYn' && index !== undefined && bdstTable !== undefined) { + if ( index !== undefined && bdstTable !== undefined) { setDetailModal({ isOpen: true, data: bdstTable[index] }) - } else if (headId === 'chBuildingYn' && index !== undefined && bdstTable !== undefined) { - if ( - bdstTable[index] && - bdstTable[index]?.chBuildingYn === 'Y' - - ) { - - setBdstChgProps({ isOpen: true, chBuilding: bdstTable[index].chgBuilding }); - } - } + } } + const getSelCity = (val: string | number) => { + const cityValue = String(val); + + if (cityValue === "0") { + setSelectSgg([]); + setObjSearch({ city: cityValue, sgg: "" }); + } else { + const data = getCity(cityValue, code.sggCd); + setSelectSgg(data); + + const firstSggVal = data.find(item => item.val === "0")?.val || (data[0]?.val ?? ""); + setObjSearch({ city: cityValue, sgg: firstSggVal }); + } + }; + + const getSgg = (val: string | number) => { + setObjSearch({ ...objSearch, sgg: String(val) }) + }; + + const getConValue = () => { + if (!objSearch.city || objSearch.city === "0") { + return "0"; + } + if (objSearch.sgg && objSearch.sgg !== "0") { + return objSearch.sgg; + } + return objSearch.city; + }; + useEffect(() => { if (!rswtSttn.isInit) { - rswtSttnInit("0"); + rswtSttnInit(false, 1, "20", "0", fcltNm, code.cityCd, code.sggCd); } }, [rswtSttn.isInit, rswtSttnInit]); useEffect(() => { - if (rswtSttnRes.length > 0 && code.cityCd.length > 0) { - const data = tableData(rswtSttnRes, { code }); - const chData = data - .map((item: customRswtBdst) => ({ + const data = tableData(rswtSttnRes, { code }); + const chData = data + .map((item: customRswtBdst) => { + return { ...item, ...item.reuseBuilding[0], ...item.reuseBuilding[0]?.rswtrObj, - chBuildingYn: item?.chgBuilding[0]?.bdstChgNm !== null && item?.chgBuilding.length > 0 ? 'Y' : 'N' - })) - .filter( - (item: customRswtBdst) => - item.reuseBuilding[0]?.rswtrObj !== undefined - ); + chBuildingYn: item?.chgBuilding[0]?.bdstChgNm !== null && item?.chgBuilding.length > 0 ? 'Y' : 'N', + instlCst: item.reuseBuilding[0]?.instlCst === -1 ? "확인불가" : item.reuseBuilding[0]?.instlCst, + instlCstSprtAmt: item.reuseBuilding[0]?.instlCstSprtAmt === -1 ? "확인불가" : item.reuseBuilding[0]?.instlCstSprtAmt, + archArea: item?.archArea.split('.')[0] !== '-1' ? item?.archArea.split('.')[0] : '확인불가' + } + }) + .filter( + (item: customRswtBdst) => + item.reuseBuilding[0]?.rswtrObj !== undefined + ); - setBdstTable(chData); - } + setBdstTable(chData); }, [code, rswtSttnRes]); useEffect(() => { + if (!isDownloadTriggered.current) { + return; + } if (rswtSttn.excelList.length > 0) { - if (rswtSttn.excelList.length === 0) return; excelDownload( rswtSttn.excelList, "중수도시설 현황", "sample_rswtSttn.xlsx", "A1:W3", ); + } else { + toast.error(t('excelDownErrMsg')); } + isDownloadTriggered.current = false; }, [rswtSttn.excelList]); - const excelClick = useCallback( - (returnType: ReturnType) => { - if (codeCityOrSgg) - rswtSttnSearch(1, "10000", codeCityOrSgg, code.cityCd, code.sggCd); - }, - [rswtSttnSearch, code.cityCd, code.sggCd, codeCityOrSgg] - ); + const excelClick = useCallback(() => { + isDownloadTriggered.current = true; + rswtSttnSearch(true, 1, String(rswtSttnResCount), String(getConValue()), fcltNm, code.cityCd, code.sggCd); + }, [rswtSttnSearch, objSearch, code.cityCd, code.sggCd, fcltNm, rswtSttnResCount]); useEffect(() => { - if (codeCityOrSgg != null) rswtSttnSearch(pageNation, "20", codeCityOrSgg, code.cityCd, code.sggCd); + rswtSttnSearch(false, pageNation, "20", String(getConValue()), fcltNm, code.cityCd, code.sggCd); + isResetPage(false) }, [pageNation]); return ( @@ -113,48 +142,89 @@ export default function RswtSttn() { <div className="grid grid-cols-4 gap-4 "> <div className="col-span-4"> <Title title={"중수도시설 현황"} /> - <SearchBaseForm - selectSidoData={code.cityCd} - selectSigunData={code.sggCd} - selectRegionData={code.rvbsnCd} - selectUnitWaterShedData={code.lgbsnCd} - selectMiddleRegionData={code.mdmbsnCd} - selectEnvPartData={code.lclgvrnmntCd} - rightChildren={<Button - className={btnClass} - onClick={() => { - setIsChHead(true); - } } - > - 조회항목설정 - </Button>} - leftChildren={undefined} - queryClick={function (returnType: ReturnType): void { - // '{partValue: 'A', sidoValue: '0', sigunValue: '0', regionValue: '0', unitWaterShedValue: '0', …}' - if (codeCityOrSgg) - rswtSttnSearch(1, "20", codeCityOrSgg, code.cityCd, code.sggCd); - } } - excelClick={excelClick} - initClick={function (): void { } } - btnClass={btnClass} - ref={ref} isAdmin={false} /> + <SearchDiv> + <SearchFormLeft> + <div className="col-span-10 flex flex-row"> + <SelectBox + className="col-span-2 w-1/6 h-full text-2xl" + label="시도" + selectClass={""} + selectArray={code.cityCd} + onSelectValue={ + getSelCity + } + /> + <SelectBox + className="col-span-2 w-1/6 h-full text-2xl ml-4" + label="시군구" + selectClass={""} + selectArray={selectSgg || []} + value={objSearch.sgg} // value props 추가 + onSelectValue={ + getSgg + } + /> + <div className="col-span-2 flex flex-row space-x-2 ml-3 mt-2"> + <Label className="w-28 mt-2 text-xl"> + {"시설명"} + </Label> + <SearchInput + className={"w-full h-full text-2xl"} + label={""} + textValue={fcltNm || ""} + setText={setFcltNm} + /> + </div> + </div> + </SearchFormLeft> + <SearchFormRight + className="w-2/12 mx-5 flex flex-col items-center justify-center space-x-2 "> + <div className="flex flex-row space-x-2 items-center"> + <Button + size={"sm"} + onClick={() => { + rswtSttnSearch(false, 1, "20", String(getConValue()), fcltNm, code.cityCd, code.sggCd); + isResetPage(true) + }} + > + 조회 + </Button> + <Button + size={"sm"} + className={`${btnClass} green`} + onClick={excelClick} + > + 다운로드 + </Button> + <Button + size={"sm"} + className={btnClass} + onClick={() => { + setIsChHead(true); + }} + > + 조회항목설정 + </Button> + </div> + </SearchFormRight> + </SearchDiv> </div> </div> <div className="grid grid-cols-6 gap-4 "> <div className="col-span-6"> <ListDiv> <UiTable - headName={[]} + headName={undefined} tableData={bdstTable || []} - children={ - <HeadMakeColSpan upHeadList={rswtSttn.rswtSttNodeList} /> - } headlist={rswtSttn.rswtSttHeadList} pageSize={20} - total={10000} - cellClick={(index: number | undefined, headId: string) => { cellClick(index ?? undefined, headId) }} + total={rswtSttnResCount || 0} + cellClick={(index: number | undefined) => { cellClick(index ?? undefined) }} onPageChange={setPageNation} - ></UiTable> + resetPageNo={resetPage} + > + <HeadMakeColSpan upHeadList={rswtSttn.rswtSttNodeList} /> + </UiTable> </ListDiv> </div> </div> @@ -183,17 +253,8 @@ export default function RswtSttn() { body={<SttnDetailModal type={"RST"} formObj={detailModal.data} />} /> - <UiModal - bodyClass={""} - open={bdstChgProps.isOpen} - body={ - <BdstChgModal chgBuilding={bdstChgProps.chBuilding}></BdstChgModal> - } - onOpenChange={(open) => { - setBdstChgProps({ ...bdstChgProps, isOpen: open }); - }} - /> </ContentsDiv> + <Toaster position="top-center" /> </> ); } diff --git a/apps/wr/app/ruseFcltMng/prfmncReg/chkgdnReg/TableHead.tsx b/apps/wr/app/ruseFcltMng/prfmncReg/chkgdnReg/TableHead.tsx index 10a0bf85f..28ba6c512 100644 --- a/apps/wr/app/ruseFcltMng/prfmncReg/chkgdnReg/TableHead.tsx +++ b/apps/wr/app/ruseFcltMng/prfmncReg/chkgdnReg/TableHead.tsx @@ -26,10 +26,10 @@ export const headMakeprfmnc = ( key={head.id} rowSpan={1} colSpan={ - makeHeadNm.find((ch) => ch.headNm === head.upName)?.length + makeHeadNm?.find((ch) => ch.headNm === head.upName)?.length } > - {makeHeadNm.find((ch) => ch.headNm === head.upName)?.text} + {makeHeadNm?.find((ch) => ch.headNm === head.upName)?.text} </TableHead> ) : null )} @@ -39,7 +39,7 @@ export const headMakeprfmnc = ( {upHeadList .filter((data) => data.visible != false) .map((head: TableUpperProps) => - makeHeadNm.find((ch) => ch.headNm === head.upName) ? ( + makeHeadNm?.find((ch) => ch.headNm === head.upName) ? ( <TableHead className={`border border-gray-600 px-2 py-1 `} key={head.id} diff --git a/apps/wr/app/ruseFcltMng/prfmncReg/chkgdnReg/page.tsx b/apps/wr/app/ruseFcltMng/prfmncReg/chkgdnReg/page.tsx index 18dbc7943..bf18d4d98 100644 --- a/apps/wr/app/ruseFcltMng/prfmncReg/chkgdnReg/page.tsx +++ b/apps/wr/app/ruseFcltMng/prfmncReg/chkgdnReg/page.tsx @@ -1,12 +1,17 @@ "use client"; import { useWrStore } from "@/app/store"; -import { type rainChkZodType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn"; -import type { - rswtChkZodType, - rswtrChkDetailsZodType, +import { + commonCd, + rainPageSize, + type rainChkZodType, +} from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn"; +import { + rswtPageSize, + type rswtrChkDetailsZodType, } from "@/app/store/prfmncReg/chkgdnReg/rswrtChkgdn"; import { + confirmToast, excelArrayDownload, excelDownload, yearOptions, @@ -15,25 +20,31 @@ import ChkgdExcel from "@/components/ui/modal/chkgd/chkgdExcel/chkgdExcel"; import ChkgdModal from "@/components/ui/modal/chkgd/chkgdModal/chkgdModal"; import RainChkgdTable from "@/components/ui/table/chkgdTable/rainChkgd/rainChkgdTable"; import RswtChkgdTable from "@/components/ui/table/chkgdTable/rswtChkgd/rswtChkgdTable"; +import { SearchInput } from "@common/business_components"; import { ContentsDiv, SearchDiv, SearchFormLeft, SearchFormRight, - SearchInput, SelectBox, Title, UiModal, } from "@common/business_components/ui"; import ListDiv from "@common/business_components/ui/list_div"; import { Button, Label } from "@common/components/ui"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@common/components/ui/tooltip"; import { useCallback, useEffect, useState } from "react"; -import { Toaster } from "react-hot-toast"; -import { shallow } from "zustand/shallow"; +import toast, { Toaster } from "react-hot-toast"; +import { useTranslation } from "react-i18next"; -//----------------------- export default function Page() { const { + auth, code, rainChkgdn, rswtChkgdn, @@ -42,9 +53,10 @@ export default function Page() { rainChkDelete, rainChkExcelDownload, rainChkHeadChange, + rainChkNmSearch, isRainSttn, - rswtChkgdnInit, + rswtChkNmSearch, rswtChkPolInit, rswtChkDetilsUpdate, rswtChkDelete, @@ -53,6 +65,7 @@ export default function Page() { } = useWrStore((state) => { return state; }); + const { regAuthrtYn, mdfcnAuthrtYn, delAuthrtYn } = auth; const [isRegExcel, setIsRegExcel] = useState(false); const [updateRainDetail, setUpdateRainDetail] = useState<rainChkZodType[]>( [] @@ -61,38 +74,89 @@ export default function Page() { rswtrChkDetailsZodType[] >([]); const [deleteIdx, setDeleteIdx] = useState<number[]>([]); - + const [rainPageNation, setRainPageNation] = useState(1); + const [rswtPageNation, setRswtPageNation] = useState(1); + const [fcltNm, setFcltNm] = useState<string>(""); const [selFcltDiv, setSelFcltDiv] = useState("RNB"); const [isModal, setIsModal] = useState({ open: false, data: {}, }); - const cellClick = () => { - if (searchYear) rainChkInit(searchYear, "0"); - }; - const [searchYear, setSearchYear] = useState<string>( String(new Date().getFullYear()) ); + const cellClick = () => { + if (searchYear) + if (selFcltDiv === "RNB") { + if (fcltNm) { + rainChkNmSearch( + rainPageNation, + rainPageSize, + searchYear, + commonCd, + fcltNm + ); + setRainPageNation(1); + } else { + rainChkInit(rainPageNation, rainPageSize, searchYear, commonCd); + } + } else { + if (fcltNm) { + rswtChkNmSearch( + rainPageNation, + rainPageSize, + searchYear, + commonCd, + fcltNm + ); + setRswtPageNation(1); + } else { + rswtChkgdnInit(rswtPageNation, rswtPageSize, searchYear, commonCd); + } + } + }; + const rainPageChange = (pageNo: number) => { + if (pageNo > 0) { + setRainPageNation(pageNo); + rainChkInit(pageNo, rainPageSize, searchYear, commonCd); + } + }; + const rswtPageChange = (pageNo: number) => { + if (pageNo > 0) { + setRswtPageNation(pageNo); + rswtChkgdnInit(pageNo, rswtPageSize, searchYear, commonCd); + } + }; useEffect(() => { if (!rainChkgdn.isInit) { - rainChkInit(new Date().getFullYear().toString(), "0"); + rainChkInit( + 1, + rainPageSize, + new Date().getFullYear().toString(), + commonCd + ); isRainSttn(false); setIsModal({ data: {}, open: false }); setIsRegExcel(false); } + }, [rainChkgdn.isInit, rainChkInit, rainPageSize]); + useEffect(() => { if (!rswtChkgdn.isInit) { - rswtChkgdnInit(new Date().getFullYear().toString(), "0"); + rswtChkgdnInit( + 1, + rswtPageSize, + new Date().getFullYear().toString(), + commonCd + ); setIsModal({ data: {}, open: false }); setIsRegExcel(false); } - }, [rainChkgdn.isInit, rainChkInit]); + }, [rswtChkgdn.isInit, rswtChkgdnInit, rswtPageSize]); useEffect(() => { - if (rswtChkgdn.excelList[0]&&rswtChkgdn.excelList[0]?.data.length > 1) { - + if (rswtChkgdn.excelList[0] && rswtChkgdn.excelList[0]?.data.length > 1) { excelArrayDownload( "중수도시설 지도점검", "sample_rs_chk.xlsx", @@ -141,20 +205,35 @@ export default function Page() { } }, [rainChkgdn.isSttn, rswtChkgdn.isSttn]); + const { t } = useTranslation('base'); const clickDelete = (data: number[], selFcltDiv: string) => { if (data.length > 0) { if (selFcltDiv === "RNB") { - rainChkDelete(data); + const handleConfirm = async () => { + rainChkDelete(data, searchYear); + }; + confirmToast(t('isDeleteMsg'), handleConfirm); } else { - rswtChkDelete(data); + const handleConfirm = async () => { + rswtChkDelete(data, searchYear); + }; + confirmToast(t('isDeleteMsg'), handleConfirm); } } }; const chkUpdateTable = (selFcltDiv: string) => { if (selFcltDiv === "RNB") { - rainChkDetilUpdate(updateRainDetail); + if (updateRainDetail && updateRainDetail.length > 0) { + rainChkDetilUpdate(updateRainDetail, searchYear); + } else { + toast.error("수정을 원하는 행을 선택하고 저장버튼을 눌러주세요 "); + } } else { - rswtChkDetilsUpdate(updateRswtDetail); + if (updateRswtDetail && updateRswtDetail.length > 0) { + rswtChkDetilsUpdate(updateRswtDetail, searchYear); + } else { + toast.error("수정을 원하는 행을 선택하고 저장버튼을 눌러주세요 "); + } } }; @@ -175,9 +254,9 @@ export default function Page() { selectClass={""} selectArray={yearOptions} value={searchYear} - onSelectValue={(val: string | number) => - setSearchYear(String(val)) - } + onSelectValue={(val: string | number) => { + setSearchYear(String(val)); + }} /> </div> <div className="col-span-2 flex flex-row space-x-2"> @@ -189,21 +268,31 @@ export default function Page() { label={""} selectClass={""} selectArray={rainChkgdn.fcltDiv} - onSelectValue={(val: string | number) =>{ - if(val === "RST") rswtChkPolInit(); - setSelFcltDiv(String(val)) - } - } + onSelectValue={(val: string | number) => { + if (val === "RST") rswtChkPolInit(); + setSelFcltDiv(String(val)); + }} value={selFcltDiv ?? rainChkgdn.fcltDiv[0]?.val} /> </div> + <div className="col-span-2 flex flex-row space-x-2"> + <Label className="w-28 mt-2 text-xl text-gray-600"> + {"시설명"} + </Label> + <SearchInput + className={"w-full h-full text-2xl"} + label={""} + textValue={fcltNm || ""} + setText={setFcltNm} + /> + </div> </SearchFormLeft> <SearchFormRight className="w-2/12 mx-5 flex flex-col items-center justify-center space-x-2 "> <div className="space-y"> <div className="flex flex-row space-x-2"> <Button size={"sm"} - onClick={(e) => { + onClick={() => { cellClick(); }} > @@ -214,22 +303,38 @@ export default function Page() { onClick={() => { clickDelete(deleteIdx, selFcltDiv); }} + disabled={delAuthrtYn === 'N'} > 삭제 </Button> - <Button - size={"sm"} - onClick={() => { - chkUpdateTable(selFcltDiv); - }} - > - 저장 - </Button> + + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + size={"sm"} + onClick={() => { + chkUpdateTable(selFcltDiv); + }} + disabled={mdfcnAuthrtYn === 'N'} + > + 저장 + </Button> + </TooltipTrigger> + <TooltipContent side="bottom" sideOffset={5}> + <h2 className="text-xl"> + 아래 테이블에 선택을 체크하면 입력셀이 활성화 됩니다. + </h2> + </TooltipContent> + </Tooltip> + </TooltipProvider> + <Button size={"sm"} onClick={() => { setIsRegExcel(!isRegExcel); }} + disabled={regAuthrtYn === 'N'} > 결과등록 </Button> @@ -251,6 +356,10 @@ export default function Page() { detailUp={setUpdateRainDetail} handleClick={handleClick} rainChkData={rainChkgdn.rainChk} + pageNation={rainPageNation} + onPageChange={rainPageChange} + pageCount={rainChkgdn.rainChkCount} + readonly={mdfcnAuthrtYn === 'N'} /> ) : ( <RswtChkgdTable @@ -258,6 +367,10 @@ export default function Page() { deleteRswtData={setDeleteIdx} handleRswtClick={handleClick} rswtChkData={rswtChkgdn.rswtChkRes} + pageNation={rswtPageNation} + onPageChange={rswtPageChange} + pageCount={rswtChkgdn.rswtChkCount} + readonly={mdfcnAuthrtYn === 'N'} /> )} </ListDiv> @@ -265,17 +378,32 @@ export default function Page() { </div> <UiModal open={isModal.open} - onOpenChange={() => { - setIsModal({ open: !open, data: {} }); + onOpenChange={(open: boolean) => { + setIsModal((prev) => ({ ...prev, open })); }} bodyClass={"w-[80vw] max-w-none"} - body={<ChkgdModal type={selFcltDiv} obj={isModal.data} />} + body={ + <ChkgdModal + type={selFcltDiv} + obj={isModal.data} + setIsModal={(open) => setIsModal((prev) => ({ ...prev, open }))} + readonly={mdfcnAuthrtYn === 'N' ? false : true} + /> + } ></UiModal> <UiModal open={isRegExcel} onOpenChange={setIsRegExcel} bodyClass={"w-[30vw] max-w-none"} - body={<ChkgdExcel type={selFcltDiv}></ChkgdExcel>} + body={ + <ChkgdExcel + type={selFcltDiv} + setIsRegExcel={setIsRegExcel} + setYearChange={(year) => { + setSearchYear(year); + }} + ></ChkgdExcel> + } /> <Toaster position="top-center" /> </ContentsDiv> diff --git a/apps/wr/app/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx b/apps/wr/app/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx index 6be50a8c5..fbab00284 100644 --- a/apps/wr/app/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx +++ b/apps/wr/app/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx @@ -1,19 +1,28 @@ "use client"; import { useWrStore } from "@/app/store"; -import type { pubHasuType } from "@/app/store/ruseFcltMng/fcltReg/pbnstReg/type"; -import { excelDownload, yearOptions } from "@/components/lib/func/commom"; +import { excelToPbnPrfData } from "@/app/store/prfmncReg/pbnstPrfmncReg/excel"; +import { + initValInUsage, + initValOutUsage, + subTotalObj, + totalObj, + type FcPrfType, + type PrfType, +} from "@/app/store/prfmncReg/pbnstPrfmncReg/type"; +import { confirmToast, excelDownload, yearOptions } from "@/components/lib/func/commom"; +import { InputTableBody } from "@/components/lib/func/TableData"; import PbnPrfExcelModal from "@/components/ui/modal/chkgd/pbnPrfExcelModal/pbnPrfExcelModal"; import { ContentsDiv, - Title, + ScrollArea, SearchDiv, SearchFormLeft, - SelectBox, - SearchInput, SearchFormRight, - ScrollArea, - UiTable, + SearchInput, + SelectBox, + Title, UiModal, + UiTable, } from "@common/business_components/ui"; import ListDiv from "@common/business_components/ui/list_div"; import { @@ -23,26 +32,17 @@ import { TableBody, TableHeader, } from "@common/components/ui"; -import { useState, useEffect, useCallback } from "react"; -import { set } from "react-hook-form"; -import { Toaster } from "react-hot-toast"; +import { useCallback, useEffect, useState } from "react"; +import toast, { Toaster } from "react-hot-toast"; +import { useTranslation } from "react-i18next"; import { headMakeprfmnc } from "../chkgdnReg/TableHead"; -import { InputTableBody } from "@/components/lib/func/TableData"; -import { - initValInUsage, - initValOutUsage, - subTotalObj, - totalObj, - type FcPrfType, - type PrfType, -} from "@/app/store/prfmncReg/pbnstPrfmncReg/type"; -import { excelToPbnPrfData } from "@/app/store/prfmncReg/pbnstPrfmncReg/excel"; -export default function prfmncPrfmncReg() { - const { code, prfmnc, prfmncHasuInit, prfmnRegData } = useWrStore( +export default function PrfmncPrfmncReg() { + const { auth, prfmnc, prfmncHasuInit, prfmnRegData, prfmncHasuNmSearch } = useWrStore( (state) => state ); - const { pbnstPrfmncHeadList, monthHeadList, prfmncRes } = prfmnc; + const { pbnstPrfmncHeadList, monthHeadList, prfmncRes, prfmncResCount } = prfmnc; + const { regAuthrtYn, mdfcnAuthrtYn } = auth; const [bdstTable, setbdstTable] = useState<FcPrfType[]>(); const [searchTxt, setSearchTxt] = useState<string>(); const [isExcelOpen, setIsExcelOPen] = useState<{ @@ -50,12 +50,13 @@ export default function prfmncPrfmncReg() { data: FcPrfType; }>({ isModal: false, data: {} as FcPrfType }); const [inputData, setInputData] = useState<any[]>(); - const [pbnstRuseFcltPk, setPbnstRuseFcltPk] = useState<string>(); - const [pageNation , setPageNation] = useState<number>(1); - + const [pbnstRuseFcltId, setpbnstRuseFcltId] = useState<string>(); + const [pageNation, setPageNation] = useState<number>(1); + const [cellNum, setCellNum] = useState<number>(1); const [searchYear, setSearchYear] = useState<string>( String(new Date().getFullYear()) ); + const excelClick = useCallback(() => { excelDownload( [[]], @@ -64,36 +65,69 @@ export default function prfmncPrfmncReg() { "A1:O1" ); }, [prfmnc.excelList]); + /* + const onSave = useCallback( + async() => { + if (pbnstRuseFcltId) { + await prfmnRegData(pageNation,'41', + excelToPbnPrfData( + inputData as unknown as JSON[], + searchYear, + pbnstRuseFcltId + ) as unknown as PrfType + ); + // cellClick(cellNum) + } + }, [inputData, searchYear, pbnstRuseFcltId, pageNation,cellNum]);*/ + const { t } = useTranslation('base'); const onSave = useCallback(() => { - if (pbnstRuseFcltPk) { + const handleConfirm = async () => { + if (!pbnstRuseFcltId) return; + prfmnRegData( + pageNation, + '41', excelToPbnPrfData( inputData as unknown as JSON[], searchYear, - pbnstRuseFcltPk + pbnstRuseFcltId ) as unknown as PrfType + , searchYear ); - prfmncHasuInit(String(pageNation), "41"); - } - }, [inputData, searchYear, pbnstRuseFcltPk,prfmncHasuInit,pageNation]); - - + }; + confirmToast(t('isUpdateMsg'), handleConfirm); + }, [inputData, searchYear, pbnstRuseFcltId, pageNation, cellNum]); const cellClick = useCallback( - (index: number | undefined, headId: string) => { + (index: number | undefined) => { if (index !== undefined) { setIsExcelOPen({ isModal: false, data: prfmncRes[index] || ({} as FcPrfType), }); - const inUsageData = structuredClone(initValInUsage); - const outUsageData = structuredClone(initValOutUsage); - const inSubtotal = structuredClone(subTotalObj); - const outSubtotal = structuredClone(subTotalObj); + const inUsageData = structuredClone(initValInUsage).map((row) => ({ + ...row, + 구분: "장내용수", + })); + const outUsageData = structuredClone(initValOutUsage).map((row) => ({ + ...row, + 구분: "장외용수", + })); + const inSubtotal = { + ...structuredClone(subTotalObj), + 구분: "장내용수", + }; + const outSubtotal = { + ...structuredClone(subTotalObj), + 구분: "장외용수", + }; if (prfmncRes[index]?.prfList) { prfmncRes[index].prfList?.forEach((prfItem) => { + const yr = Number(prfItem.ym?.slice(0, 4)); // "YYYY" + + if(yr === Number(searchYear)) { const month = Number(prfItem?.ym?.slice(4)); // "MM" const monthKey = `${month}월`; @@ -124,10 +158,18 @@ export default function prfmncPrfmncReg() { outSubtotal.total += value; } }); + } }); + + if(inSubtotal.total!==0 && outSubtotal.total!==0) { + inUsageData.push(inSubtotal); outUsageData.push(outSubtotal); - const usageTotalObj: { [key: string]: any } = structuredClone(totalObj); + const usageTotalObj: { [key: string]: any } = { + ...structuredClone(totalObj), + 구분: "총합계", + 용수명: "", + }; for (const key in usageTotalObj) { if (["usage", "gubun", "edit"].includes(key)) continue; const val = @@ -137,32 +179,45 @@ export default function prfmncPrfmncReg() { (usageTotalObj as any)[key] = val; } const tableData = [...inUsageData, ...outUsageData, usageTotalObj]; - setPbnstRuseFcltPk(prfmncRes[index].pbnstRuseFcltPk); - + setpbnstRuseFcltId(prfmncRes[index].pbnstRuseFcltId); setInputData(tableData); + }else{ + setInputData([]); + } } else { setInputData([]); } } }, - [prfmncRes] + [prfmncRes,searchYear] ); useEffect(() => { if (!prfmnc.isInit) { - prfmncHasuInit('1', "41"); + const year = new Date().getFullYear() + prfmncHasuInit(1, '41', String(year)); } }, [prfmnc.isInit, prfmncHasuInit]); useEffect(() => { - setbdstTable(prfmncRes); - }, [prfmncRes]); + + + if (prfmncRes && prfmncRes.length > 0) { + + setbdstTable(prfmncRes); + } + + // cellNum이 0일 경우(첫 번째 행)도 유효한 인덱스이므로 undefined 여부만 체크합니다. + if (cellNum !== undefined) { + cellClick(cellNum); + } + }, [prfmncRes, cellNum, cellClick]); return ( <> <ContentsDiv> <div className="grid grid-cols-4 gap-4"> <div className="col-span-4"> - <Title title={"하수재이용 시설 등록"} /> + <Title title={"하수재이용 실적 등록"} /> <SearchDiv> <SearchFormLeft> <div className="col-span-2 flex flex-row space-x-2"> @@ -197,7 +252,17 @@ export default function prfmncPrfmncReg() { <SearchFormRight className="w-2/12 mx-5 flex flex-col items-center justify-center space-x-2 "> <div className="space-y"> <div className="flex flex-row space-x-2"> - <Button size={"sm"} onClick={(e) => {}}> + <Button size={"sm"} onClick={() => { + + if (searchTxt && searchTxt?.length > 0) { + prfmncHasuNmSearch(1, '41', searchYear, searchTxt) + setPageNation(1) + } else { + prfmncHasuInit(1, '41', searchYear) + setPageNation(1) + } + + }}> 조회 </Button> <Button @@ -205,14 +270,21 @@ export default function prfmncPrfmncReg() { onClick={() => { onSave(); }} + disabled={mdfcnAuthrtYn === 'N'} > 저장 </Button> <Button size={"sm"} onClick={() => { - setIsExcelOPen({ ...isExcelOpen, isModal: true }); + if (cellNum === undefined) { + toast.error("하수처리재이용시설을 선택해주세요."); + return; + } else { + setIsExcelOPen({ ...isExcelOpen, isModal: true }); + } }} + disabled={regAuthrtYn === 'N'} > 파일등록 </Button> @@ -231,16 +303,21 @@ export default function prfmncPrfmncReg() { <ListDiv className="min-h-0"> <ScrollArea className="h-[60vh]"> <UiTable + key={'leftTable'} tableData={bdstTable || []} headlist={pbnstPrfmncHeadList} pageSize={10} - total={10000} + total={prfmncResCount} headName={"하수재이용시설 목록"} - cellClick={(index: number | undefined, headId: string) => { - cellClick(index, headId); + cellClick={(index: number | undefined) => { + cellClick(index); + setCellNum(index ?? 0); }} - onPageChange={setPageNation} - children={undefined} + onPageChange={ + (page) => { + prfmncHasuInit(page, '41', searchYear); + setPageNation(page) + }} /> </ScrollArea> </ListDiv> @@ -249,16 +326,15 @@ export default function prfmncPrfmncReg() { <ScrollArea className="h-[60vh]"> <div className="card-header"> <h3 className="text-2xl font-medium text-label"> - {"하수처리수재이용 실적(m3) 등록결과"} + {"하수처리수재이용 실적(m³) 등록결과"} </h3> </div> - <Table variant="secondary"> <TableHeader className="font-bold"> {headMakeprfmnc(monthHeadList, [])} </TableHeader> - <TableBody> - {InputTableBody(inputData ?? [], monthHeadList, setInputData)} + <TableBody key={prfmnc.pageKey + 'pageKey'}> + {InputTableBody(inputData ?? [], monthHeadList, setInputData, mdfcnAuthrtYn === 'N' ? false : true)} </TableBody> </Table> </ScrollArea> @@ -266,11 +342,26 @@ export default function prfmncPrfmncReg() { </div> </ContentsDiv> <UiModal - bodyClass={""} - body={<PbnPrfExcelModal data={isExcelOpen.data}></PbnPrfExcelModal>} + bodyClass={"w-[30vw] max-w-none"} + body={ + <PbnPrfExcelModal + data={isExcelOpen.data} + setIsExcelOPen={(open) => { + setIsExcelOPen({ ...isExcelOpen, isModal: open }) + }} + setModalExcelData={async (excelData) => { + await prfmnRegData(pageNation, '41', + excelData, + searchYear + ); + // 데이터 저장 후 UI 업데이트는 useEffect가 감지하여 처리하므로 여기서는 호출하지 않습니다. + } + } + ></PbnPrfExcelModal> + } open={isExcelOpen.isModal} onOpenChange={() => { - setIsExcelOPen({ isModal: false, data: {} as FcPrfType }); + setIsExcelOPen({ ...isExcelOpen, isModal: false }); }} ></UiModal> <Toaster position="top-center" /> diff --git a/apps/wr/app/ruseFcltMng/prfmncSttn/chkgdnSttn/page.tsx b/apps/wr/app/ruseFcltMng/prfmncSttn/chkgdnSttn/page.tsx index f9a799c24..ba1821f20 100644 --- a/apps/wr/app/ruseFcltMng/prfmncSttn/chkgdnSttn/page.tsx +++ b/apps/wr/app/ruseFcltMng/prfmncSttn/chkgdnSttn/page.tsx @@ -1,14 +1,12 @@ "use client"; import { useWrStore } from "@/app/store"; -import { type rainChkZodType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn"; -import type { rswtChkZodType } from "@/app/store/prfmncReg/chkgdnReg/rswrtChkgdn"; +import { commonCd, rainPageSize } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn"; +import { rswtPageSize } from "@/app/store/prfmncReg/chkgdnReg/rswrtChkgdn"; import { - excelArrayDownload, excelDownload, - yearOptions, + yearOptions } from "@/components/lib/func/commom"; -import ChkgdExcel from "@/components/ui/modal/chkgd/chkgdExcel/chkgdExcel"; import ChkgdModal from "@/components/ui/modal/chkgd/chkgdModal/chkgdModal"; import RainChkgdTable from "@/components/ui/table/chkgdTable/rainChkgd/rainChkgdTable"; import RswtChkgdTable from "@/components/ui/table/chkgdTable/rswtChkgd/rswtChkgdTable"; @@ -20,56 +18,100 @@ import { SearchInput, SelectBox, Title, - UiModal, + UiModal } from "@common/business_components/ui"; import ListDiv from "@common/business_components/ui/list_div"; import { Button, Label } from "@common/components/ui"; import { useCallback, useEffect, useState } from "react"; -import { Toaster } from "react-hot-toast"; +import toast, { Toaster } from "react-hot-toast"; +import { useTranslation } from "react-i18next"; -//----------------------- export default function Page() { const { - code, rainChkSttn, rswtChkSttn, - - rainChkSttnInit, - rainChkSttnSearch, + rainChkSttnNmSearch, rswtChkSttnInit, + rswtChkNmSttnSearch, isRainSttn, - isRswtSttn + isRswtSttn, + rswtChkPolInit, } = useWrStore((state) => { return state; }); const [selFcltDiv, setSelFcltDiv] = useState("RNB"); + const [rainPageNation, setRainPageNation] = useState(1); + const [rswtPageNation, setRswtPageNation] = useState(1); + const [fcltNm, setFcltNm] = useState<string>(""); const [isModal, setIsModal] = useState({ open: false, data: {}, }); const [searchYear, setSearchYear] = useState<string>(String(new Date().getFullYear())); const cellClick = () => { - if (searchYear) rainChkSttnSearch(searchYear, "0"); + if (searchYear) { + if (selFcltDiv === "RNB") { + if (fcltNm) { + rainChkSttnNmSearch( + rainPageNation, + rainPageSize, + searchYear, + commonCd, + fcltNm + ); + } else { + rainChkSttnInit(rainPageNation, rainPageSize, searchYear, commonCd); + } + } else { + if (fcltNm) { + rswtChkNmSttnSearch( + rswtPageNation, + rswtPageSize, + searchYear, + commonCd, + fcltNm + ); + } else { + rswtChkSttnInit(rswtPageNation, rswtPageSize, searchYear, commonCd); + } + } + } + }; + + const rainPageChange = (pageNo: number) => { + if (pageNo > 0) { + setRainPageNation(pageNo); + rainChkSttnInit(pageNo, rainPageSize, searchYear, commonCd); + } + }; + + const rswtPageChange = (pageNo: number) => { + if (pageNo > 0) { + setRswtPageNation(pageNo); + rswtChkSttnInit(pageNo, rswtPageSize, searchYear, commonCd); + } }; useEffect(() => { if (!rainChkSttn.isInit) { isRainSttn(true) - rainChkSttnInit( "0"); + rainChkSttnInit(1, rainPageSize, new Date().getFullYear().toString(), commonCd); setIsModal({ data: {}, open: false }); + } - }, [rainChkSttn.isInit, rainChkSttnInit]); - useEffect(() => { + }, [rainChkSttn.isInit, rainChkSttnInit, rainPageSize]); + useEffect(() => { if (!rswtChkSttn.isInit) { setIsModal({ data: {}, open: false }); - rswtChkSttnInit( "0"); + rswtChkSttnInit(1, rswtPageSize, new Date().getFullYear().toString(), commonCd); isRswtSttn(true) } - },[rswtChkSttn.isInit, rswtChkSttnInit]) + }, [rswtChkSttn.isInit, rswtChkSttnInit, rswtPageSize]) + const handleClick = useCallback( @@ -78,25 +120,34 @@ export default function Page() { }, [isModal] ); -const excelDown=() =>{ - if(rainChkSttn.sttnExcelList && selFcltDiv === "RNB"){ - excelDownload( - rainChkSttn.sttnExcelList, - "빗물이용시설 지도점검 현황", - "sample_rainChkSttn.xlsx", - "A1:Q2", - 3 - ); - }else if(rswtChkSttn.sttnExcelList && selFcltDiv === "RST"){ - excelDownload( - rswtChkSttn.sttnExcelList, - "중수도시설 지도점검 현황", - "sample_rswtChkSttn.xlsx", - "A1:X2", - 3 - ); + const { t } = useTranslation('base'); + const excelDown = () => { + if (rainChkSttn.sttnExcelList && selFcltDiv === "RNB") { + if (!rainChkSttn.sttnExcelList || rainChkSttn.sttnExcelList.length === 0) { + toast.error(t('excelDownErrMsg')); + return; + } + excelDownload( + rainChkSttn.sttnExcelList, + "빗물이용시설 지도점검 현황", + "sample_rainChkSttn.xlsx", + "A1:Q2", + 3 + ); + } else if (rswtChkSttn.sttnExcelList && selFcltDiv === "RST") { + if (!rswtChkSttn.sttnExcelList || rswtChkSttn.sttnExcelList.length === 0) { + toast.error(t('excelDownErrMsg')); + return; + } + excelDownload( + rswtChkSttn.sttnExcelList, + "중수도시설 지도점검 현황", + "sample_rswtChkSttn.xlsx", + "A1:X2", + 3 + ); + } } -} return ( <ContentsDiv> @@ -125,10 +176,23 @@ const excelDown=() =>{ label={""} selectClass={""} selectArray={rainChkSttn.fcltDiv} - onSelectValue={(val: string | number) => - setSelFcltDiv(String(val)) - } - value={selFcltDiv ?? rainChkSttn.fcltDiv[0]?.val} + onSelectValue={(val: string | number) => { + if (val === "RST") rswtChkPolInit(); + setSelFcltDiv(String(val)) + } + } + value={selFcltDiv ?? rainChkSttn.fcltDiv[0]?.val} + /> + </div> + <div className="col-span-2 flex flex-row space-x-2"> + <Label className="w-28 mt-2 text-xl text-gray-600"> + {"시설명"} + </Label> + <SearchInput + className={"w-full text-2xl"} + label={""} + textValue={fcltNm || ""} + setText={setFcltNm} /> </div> </SearchFormLeft> @@ -137,13 +201,13 @@ const excelDown=() =>{ <div className="flex flex-row space-x-2"> <Button size={"sm"} - onClick={(e) => { + onClick={() => { cellClick(); }} > 조회 </Button> - <Button color="green" size={"sm"} onClick={(e) =>{excelDown()}}> + <Button color="green" size={"sm"} onClick={() => { excelDown() }}> 다운로드 </Button> </div> @@ -157,17 +221,25 @@ const excelDown=() =>{ <ListDiv> {selFcltDiv === "RNB" ? ( <RainChkgdTable - deleteData={() => {}} - detailUp={() => {}} + deleteData={() => { }} + detailUp={() => { }} handleClick={handleClick} rainChkData={rainChkSttn.rainChkRes} + pageNation={rainPageNation} + onPageChange={rainPageChange} + pageCount={rainChkSttn.rainChkCount} + readonly={true} /> ) : ( <RswtChkgdTable - detaiRswtlUpdate={() => {}} - deleteRswtData={() => {}} + detaiRswtlUpdate={() => { }} + deleteRswtData={() => { }} handleRswtClick={handleClick} rswtChkData={rswtChkSttn.rswtChkRes} + pageNation={rswtPageNation} + onPageChange={rswtPageChange} + pageCount={rswtChkSttn.rswtChkCount} + readonly={true} /> )} </ListDiv> @@ -179,7 +251,7 @@ const excelDown=() =>{ setIsModal({ open: !open, data: {} }); }} bodyClass={"w-[80vw] max-w-none"} - body={<ChkgdModal type={selFcltDiv} obj={isModal.data} />} + body={<ChkgdModal readonly={true} type={selFcltDiv} obj={isModal.data} setIsModal={() => { }} />} ></UiModal> <Toaster position="top-center" /> </ContentsDiv> diff --git a/apps/wr/app/ruseFcltMng/prfmncSttn/pbnstPrfmncSttn/page.tsx b/apps/wr/app/ruseFcltMng/prfmncSttn/pbnstPrfmncSttn/page.tsx index a50c08071..8035d2875 100644 --- a/apps/wr/app/ruseFcltMng/prfmncSttn/pbnstPrfmncSttn/page.tsx +++ b/apps/wr/app/ruseFcltMng/prfmncSttn/pbnstPrfmncSttn/page.tsx @@ -5,40 +5,47 @@ import { btnClass } from "@/app/store/common/code"; import { initValInUsage, initValOutUsage, subTotalObj, totalObj, type FcPrfType, type PrfType } from "@/app/store/prfmncReg/pbnstPrfmncReg/type"; import { excelPrfmncSttnParsing } from "@/app/store/prfmncSttn/pbnstPrfmncSttn/excel"; import { excelDownload, yearOptions } from "@/components/lib/func/commom"; -import SearchInputText from "@/components/ui/searchInput/searchInput"; +import { InputTableBody } from "@/components/lib/func/TableData"; import { CHART_FORM, ContentsDiv, MainChart, ScrollArea, SearchBaseForm, SearchInput, SelectBox, Title, UiTable, type ChartConfig, type ReturnType } from "@common/business_components"; import ListDiv from "@common/business_components/ui/list_div"; -import { Button, Label } from "@common/components/ui"; +import { Label, Table, TableBody, TableHeader } from "@common/components/ui"; import { useCallback, useEffect, useRef, useState } from "react"; import toast, { Toaster } from "react-hot-toast"; +import { useTranslation } from "react-i18next"; +import { headMakeprfmnc } from "../../prfmncReg/chkgdnReg/TableHead"; export default function PbnstPrfmncSttn() { const { code, tableHeadMaker, pbnstPrfmncSttn, - pbnstPrfmncInit, + // pbnstPrfmncInit, pbnstPrfmncSearch, + getFlctsSttn, prfmnc, - pbnstSttn, pbnNmSearch } = useWrStore((state) => state); const { monthHeadList } = prfmnc - const { pbnstPrfHeadList, prfmncRes, rawData, selectYearList } = pbnstPrfmncSttn; - const [isChHead, setIsChHead] = useState(false); + const { pbnstPrfHeadList, prfmncResCount, prfmncRes, rawData, selectYearList, fclts } = pbnstPrfmncSttn; + // const [isChHead, setIsChHead] = useState(false); const [fromDate, setFromDate] = useState<string>(String(new Date().getFullYear())); const [toDate, setToDate] = useState<string>(String(new Date().getFullYear())); const [year, setYear] = useState<string>(String(new Date().getFullYear())); const [bdstTable, setbdstTable] = useState<FcPrfType[]>(); const ref = useRef<ReturnType>({} as ReturnType); - const [swtrmtFcltNm, setSwtrmtFcltNm] = useState<string>(); + // const [swtrmtFcltNm, setSwtrmtFcltNm] = useState<string>(); const [pbnRuseNm, setPbnRuseNm] = useState<string>(""); - - const [searchYear, setSearchYear] = useState<string>( - String(new Date().getFullYear()) - ); + const { t } = useTranslation('base'); + const searchYear = String(new Date().getFullYear()); const [excelData, setExcelData] = useState<any[]>([]); + const [pageNation, setPageNation] = useState(1); + const [resetPage, isResetPage] = useState(false); + const excelClick = useCallback(() => { + if (!excelData || excelData.length === 0) { + toast.error(t('excelDownErrMsg')); + return; + } excelDownload( excelData, "하수재이용실적 현황", @@ -66,8 +73,6 @@ export default function PbnstPrfmncSttn() { chartData5: lineChartDataProps[]; chartData6: lineChartDataProps[]; chartData7: lineChartDataProps[]; - chartData8: lineChartDataProps[]; - chartData9: lineChartDataProps[]; }>(); const [outLineData, setOutLineData] = useState<{ chartData1: lineChartDataProps[]; @@ -76,23 +81,31 @@ export default function PbnstPrfmncSttn() { chartData4: lineChartDataProps[]; chartData5: lineChartDataProps[]; chartData6: lineChartDataProps[]; + chartData7: lineChartDataProps[]; + chartData8: lineChartDataProps[]; }>(); - type dnChartForm = { label: string, val: number, fill?: string }; + type dnChartForm = { label: string, val: number, color?: string }; type barChartForm = { label: string, "장내용수": number, "장외용수": number }; type lineChartDataProps = { label: string, val: number, color: string }; const prfTableData = (prfList: PrfType[], year: string) => { - const inUsageData = structuredClone(initValInUsage); - const outUsageData = structuredClone(initValOutUsage); - - const inSubtotal = structuredClone(subTotalObj); - const outSubtotal = structuredClone(subTotalObj); - - const prfFilter = prfList?.filter((e) => e.ym.slice(0, 4) === year) + const inUsageData = structuredClone(initValInUsage).map(row => ({ + ...row + // 구분: "장내용수", + })); + const outUsageData = structuredClone(initValOutUsage).map(row => ({ + ...row + // 구분: "장외용수", + })); + const inSubtotal = { ...structuredClone(subTotalObj), 구분: "장내용수" }; + const outSubtotal = { ...structuredClone(subTotalObj), 구분: "장외용수" }; + const prfFilter = prfList?.filter((e) => + e.ym.slice(0, 4) === year + ) if (prfList && prfFilter.length > 0) { - - prfList?.forEach((prfItem) => { + + prfFilter.forEach((prfItem) => { const month = Number(prfItem?.ym?.slice(4)); // "MM" const monthKey = `${month}월`; @@ -138,11 +151,12 @@ export default function PbnstPrfmncSttn() { const tableData = [...inUsageData, ...outUsageData, usageTotalObj]; setPrfData(tableData); setExcelData(excelPrfmncSttnParsing(prfData ?? [])) + // }); } else { setPrfData([]); } } - + const [chartId, setChartId] = useState<string>(); useEffect(() => { // 누적 변수 초기화 @@ -166,7 +180,7 @@ export default function PbnstPrfmncSttn() { let inUsageSum = 0, outUsageSum = 0; - let inUsageArr: number[] = [], + const inUsageArr: number[] = [], outUsageArr: number[] = []; // 라인차트용 데이터 @@ -198,11 +212,12 @@ export default function PbnstPrfmncSttn() { prfTableData([], year) rawData.forEach((item) => { // 실적 테이블 데이터 - if (chartId){ - if(item.prfList?.[0]?.ym.slice(0, 4) === year && item.prfList?.[0]?.pbnstRuseFcltFk === chartId){ + if (chartId) { + if (item.prfList.find((e) => e.ym.slice(0, 4) === year) && item.prfList?.[0]?.pbnstRuseFcltId === chartId) { prfTableData(item.prfList, year); } - item.prfList.filter((e) => e.ym.slice(0, 4) === year && e.pbnstRuseFcltFk === chartId).forEach((element) => { + + item.prfList.filter((e) => e.ym.slice(0, 4) === year && e.pbnstRuseFcltId === chartId).forEach((element) => { // 누적 wshwtr += element.wshwtr; coolUswtr += element.coolUswtr; @@ -221,16 +236,18 @@ export default function PbnstPrfmncSttn() { indstUswtr += element.indstUswtr; // 월별 합계 - inUsageSum = element.wshwtr + inUsageSum = + element.wshwtr + element.coolUswtr + element.cleanUswtr + element.drwtUswtr + element.dltnUswtr + element.ithlRswtr + element.ithlEtcUswtr - + element.cleanToilUswtr - + element.resn; - outUsageSum = element.lndwtr + outUsageSum = + element.cleanToilUswtr + + element.resn + + element.lndwtr + element.hydUswtr + element.ugwtChr + element.farmUswtr @@ -251,9 +268,10 @@ export default function PbnstPrfmncSttn() { dltnUswtrData.push({ label: `${month}월`, val: element.dltnUswtr, color: "purple" }); ithlRswtrData.push({ label: `${month}월`, val: element.ithlRswtr, color: "green" }); ithlEtcUswtrData.push({ label: `${month}월`, val: element.ithlEtcUswtr, color: "blue" }); + + cleanToilUswtrData.push({ label: `${month}월`, val: element.cleanToilUswtr, color: "magenta" }); resnData.push({ label: `${month}월`, val: element.resn, color: "skyblue" }); - lndwtrData.push({ label: `${month}월`, val: element.lndwtr, color: "red" }); hydUswtrData.push({ label: `${month}월`, val: element.hydUswtr, color: "orange" }); ugwtChrData.push({ label: `${month}월`, val: element.ugwtChr, color: "yellow" }); @@ -264,41 +282,55 @@ export default function PbnstPrfmncSttn() { } }); + let dnTotalIn = 0; + let dnTotalOut = 0; + inUsageArr.forEach((item) => { + dnTotalIn += item + }) + outUsageArr.forEach((item) => { + dnTotalOut += item + }) + // 도넛차트 데이터 - dnData.push({ label: "세척수", val: wshwtr, fill: "red" }); - dnData.push({ label: "냉각수", val: coolUswtr, fill: "orange" }); - dnData.push({ label: "청소수", val: cleanUswtr, fill: "yellow" }); - dnData.push({ label: "식수대", val: drwtUswtr, fill: "green" }); - dnData.push({ label: "희석용수", val: dltnUswtr, fill: "skyblue" }); - dnData.push({ label: "중수도", val: ithlRswtr, fill: "blue" }); - dnData.push({ label: "기타", val: ithlEtcUswtr, fill: "purple" }); - dnData.push({ label: "청소화장실용수", val: cleanToilUswtr, fill: "red" }); - dnData.push({ label: "세척살수용수", val: resn, fill: "orange" }); - dnData.push({ label: "조경용수", val: lndwtr, fill: "yellow" }); - dnData.push({ label: "친수용수", val: hydUswtr, fill: "green" }); - dnData.push({ label: "지하수충전", val: ugwtChr, fill: "skyblue" }); - dnData.push({ label: "농업용수", val: farmUswtr, fill: "blue" }); - dnData.push({ label: "하천등유지용수", val: rvrKeepUswtr, fill: "purple" }); - dnData.push({ label: "공업용수", val: indstUswtr, fill: "black" }); + dnData.push({ label: "장내용수", val: dnTotalIn, color: "#8DD8FF" }); + dnData.push({ label: "장외용수", val: dnTotalOut, color: "#FFF085" }); // 장내용수 도넛 - dnInData.push({ label: "세척수", val: wshwtr, fill: "yellow" }); - dnInData.push({ label: "냉각수", val: coolUswtr, fill: "green" }); - dnInData.push({ label: "청소수", val: cleanUswtr, fill: "yellow" }); - dnInData.push({ label: "식수대", val: drwtUswtr, fill: "green" }); - dnInData.push({ label: "희석용수", val: dltnUswtr, fill: "yellow" }); - dnInData.push({ label: "중수도", val: ithlRswtr, fill: "green" }); - dnInData.push({ label: "기타", val: ithlEtcUswtr, fill: "yellow" }); - dnInData.push({ label: "청소화장실용수", val: cleanToilUswtr, fill: "green" }); - dnInData.push({ label: "세척살수용수", val: resn, fill: "yellow" }); + dnInData.push({ label: "세척수", val: wshwtr, color: "#F7CFD8" }); + dnInData.push({ label: "냉각수", val: coolUswtr, color: "#F4F8D3" }); + dnInData.push({ label: "청소수", val: cleanUswtr, color: "#A6D6D6" }); + dnInData.push({ label: "식수대", val: drwtUswtr, color: "#8E7DBE" }); + dnInData.push({ label: "희석용수", val: dltnUswtr, color: "#FF8383" }); + dnInData.push({ label: "중수도", val: ithlRswtr, color: "#FFF574" }); + dnInData.push({ label: "기타", val: ithlEtcUswtr, color: "#A1D6CB" }); // 장외용수 도넛 - dnOutData.push({ label: "조경용수", val: lndwtr, fill: "yellow" }); - dnOutData.push({ label: "친수용수", val: hydUswtr, fill: "black" }); - dnOutData.push({ label: "지하수충전", val: ugwtChr, fill: "yellow" }); - dnOutData.push({ label: "농업용수", val: farmUswtr, fill: "black" }); - dnOutData.push({ label: "하천등유지용수", val: rvrKeepUswtr, fill: "yellow" }); - dnOutData.push({ label: "공업용수", val: indstUswtr, fill: "black" }); + dnOutData.push({ label: "청소화장실용수", val: cleanToilUswtr, color: "#A19AD3" }); + dnOutData.push({ label: "세척살수용수", val: resn, color: "#B1F0F7" }); + dnOutData.push({ label: "조경용수", val: lndwtr, color: "#81BFDA" }); + dnOutData.push({ label: "친수용수", val: hydUswtr, color: "#F5F0CD" }); + dnOutData.push({ label: "지하수충전", val: ugwtChr, color: "#FADA7A" }); + dnOutData.push({ label: "농업용수", val: farmUswtr, color: "#A6AEBF" }); + dnOutData.push({ label: "하천등유지용수", val: rvrKeepUswtr, color: "#C5D3E8" }); + dnOutData.push({ label: "공업용수", val: indstUswtr, color: "#D0E8C5" }); + + // 라인차트 데이터 월별 오름차순 정렬 + const sortFn = (a: { label: string }, b: { label: string }) => parseInt(a.label) - parseInt(b.label); + wshwtrData.sort(sortFn); + coolUswtrData.sort(sortFn); + cleanUswtrData.sort(sortFn); + drwtUswtrData.sort(sortFn); + dltnUswtrData.sort(sortFn); + ithlRswtrData.sort(sortFn); + ithlEtcUswtrData.sort(sortFn); + cleanToilUswtrData.sort(sortFn); + resnData.sort(sortFn); + lndwtrData.sort(sortFn); + hydUswtrData.sort(sortFn); + ugwtChrData.sort(sortFn); + farmUswtrData.sort(sortFn); + rvrKeepUswtrData.sort(sortFn); + indstUswtrData.sort(sortFn); // 라인차트 데이터 setInLineData({ @@ -308,18 +340,19 @@ export default function PbnstPrfmncSttn() { chartData4: drwtUswtrData, chartData5: dltnUswtrData, chartData6: ithlRswtrData, - chartData7: ithlEtcUswtrData, - chartData8: cleanToilUswtrData, - chartData9: resnData + chartData7: ithlEtcUswtrData }); setOutLineData({ - chartData1: lndwtrData, - chartData2: hydUswtrData, - chartData3: ugwtChrData, - chartData4: farmUswtrData, - chartData5: rvrKeepUswtrData, - chartData6: indstUswtrData, + chartData1: cleanToilUswtrData, + chartData2: resnData, + chartData3: lndwtrData, + chartData4: hydUswtrData, + chartData5: ugwtChrData, + chartData6: farmUswtrData, + chartData7: rvrKeepUswtrData, + chartData8: indstUswtrData, }); + barData.sort((a, b) => parseInt(a.label) - parseInt(b.label)); setBarData(barData); setChartData(dnData); setDnInUsage(dnInData); @@ -331,10 +364,10 @@ export default function PbnstPrfmncSttn() { const newChartConfig = chartData.reduce((config, element) => { config[element.label] = { label: element.label, - fill: element.fill + color: element.color }; return config; - }, {} as Record<string, { label: string; fill?: string }>) satisfies ChartConfig; + }, {} as Record<string, { label: string; color?: string }>) satisfies ChartConfig; setChartConfig(newChartConfig); }, [chartData]); @@ -342,7 +375,7 @@ export default function PbnstPrfmncSttn() { const newChartConfig = dnInUsage.reduce((config, element) => { config[element.label] = { label: element.label, - fill: element.fill + color: element.color ?? "gray" }; return config; }, {} as Record<string, { label: string; color: string; }>) satisfies ChartConfig; @@ -353,7 +386,7 @@ export default function PbnstPrfmncSttn() { const newChartConfig = dnOutUsage.reduce((config, element) => { config[element.label] = { label: element.label, - fill: element.fill + color: element.color ?? "gray" }; return config; }, {} as Record<string, { label: string; color: string; }>) satisfies ChartConfig; @@ -401,45 +434,44 @@ export default function PbnstPrfmncSttn() { chartData7: { label: '기타', color: "blue", - }, - chartData8: { - label: '청소화장실용수', - color: "magenta", - }, - chartData9: { - label: '세척살수용수', - color: "skyblue", - }, + } } satisfies ChartConfig; setLnInChartConfig(newChartConfig); } }, [inLineData]); - useEffect(() => { if (outLineData?.chartData1) { const newChartConfig = { chartData1: { - label: '조경용수', + label: '청소화장실용수', color: "red", }, chartData2: { + label: '세척살수용수', + color: "pink", + }, + chartData3: { + label: '조경용수', + color: "gray", + }, + chartData4: { label: '친수용수', color: "orange", }, - chartData3: { + chartData5: { label: '지하수충전', color: "yellow", }, - chartData4: { + chartData6: { label: '농업용수', color: "green", }, - chartData5: { + chartData7: { label: '하천등유지용수', color: "blue", }, - chartData6: { + chartData8: { label: '공업용수', color: "purple", }, @@ -461,27 +493,38 @@ export default function PbnstPrfmncSttn() { sigunText: "" } tableHeadMaker(Number(searchYear), Number(searchYear)); - pbnstPrfmncSearch(returnType, "", searchYear, searchYear) + getFlctsSttn(); + pbnstPrfmncSearch(1, returnType, "", searchYear, searchYear, fclts) }, []); useEffect(() => { - setbdstTable(prfmncRes); - if(prfmncRes?.[0]?.pbnstRuseFcltPk) setChartId(prfmncRes[0].pbnstRuseFcltPk) + pbnstPrfmncSearch(pageNation, ref.current, pbnRuseNm, fromDate, toDate, pbnstPrfmncSttn.fclts); + isResetPage(false) + }, [fclts, pageNation]); + + useEffect(() => { + const tableData = prfmncRes.map((e, idx) => { + + return ({ + ...e, + number: String(idx + 1) + }) + }) + setbdstTable(tableData) + + if (prfmncRes?.[0]?.pbnstRuseFcltId) setChartId(prfmncRes[0].pbnstRuseFcltId) }, [prfmncRes]); - - useEffect(() => { if (pbnRuseNm.length > 1) - pbnNmSearch("1", "23", ref.current, pbnRuseNm, searchYear); + pbnNmSearch("1", "23", ref.current, pbnRuseNm, searchYear, []); }, [pbnRuseNm]); const cellClick = (index: number | undefined, headId: string) => { if (index !== undefined && headId !== undefined) { - setChartId(prfmncRes[index].pbnstRuseFcltPk) + setChartId(prfmncRes[index].pbnstRuseFcltId) } } - return ( <> <ContentsDiv> @@ -495,44 +538,45 @@ export default function PbnstPrfmncSttn() { selectUnitWaterShedData={code.lgbsnCd} selectMiddleRegionData={code.mdmbsnCd} selectEnvPartData={code.lclgvrnmntCd} - rightChildren={<Button - className={btnClass} - onClick={() => { - setIsChHead(true); - }} - > - 조회항목설정 - </Button>} + rightChildren={undefined} + rightClassName="ml-auto mr-6 flex items-center space-x-1" + leftClassName="flex gap-5 w-9/12 m-4" + leftSelectClassName="w-1/6" + buttonOrder={["query", "init", "excel"]} leftChildren={<div className="col-span-5"> <div className="flex flex-row"> - <div className="flex flex-row items-center"> - <Label className="text-xl text-gray-600">하수 재이용년도</Label> + {/* <div className="flex flex-row items-center"> */} + <div className="flex flex-row items-center gap-x-4 flex-nowrap"> + {/* <Label className="w-full text-xl text-gray-600"> */} + <Label className="flex-none whitespace-nowrap text-xl text-gray-600"> + 하수 재이용년도</Label> <SelectBox - className="h-full text-2xl" + // className="w-full h-full text-2xl mx-4" + className="flex-none w-32 h-full text-2xl" label="" selectClass={""} selectArray={yearOptions} value={fromDate} onSelectValue={(val: string | number) => setFromDate(String(val))} /> + <span> ~</span> <SelectBox - className="h-full text-2xl" + // className="w-full h-full text-2xl mx-4" + className="flex-none w-32 h-full text-2xl mr-4" label="" selectClass={""} selectArray={yearOptions} value={toDate} onSelectValue={(val: string | number) => setToDate(String(val))} /> + <Label className="w-28 mt-2 text-xl"> + {"시설명"} + </Label> + <SearchInput + className={" h-full text-2xl"} + label={""} + textValue={pbnRuseNm || ""} + setText={setPbnRuseNm} + /> </div> - <SearchInputText - label={"하수처리시설"} - initValue={pbnRuseNm ?? ""} - setValue={(searchNm?: string) => { - setPbnRuseNm(searchNm || ""); - }} - setRtnData={function (rtnData: any[]): void { - // setbdstTable(rtnData); - }} - searchList={pbnstSttn.pbnstSearchData || []} - ></SearchInputText> {/* <SelectBox className="h-full text-2xl" label="재이용시설명" @@ -543,14 +587,14 @@ export default function PbnstPrfmncSttn() { </div> </div>} queryClick={function (returnType: ReturnType): void { - pbnstPrfmncSearch(returnType, pbnRuseNm ?? "", fromDate, toDate); + pbnstPrfmncSearch(1, returnType, pbnRuseNm ?? "", fromDate, toDate, fclts); if (fromDate && toDate) { const fromNum = Number(fromDate); const toNum = Number(toDate); if (fromNum > toNum) { toast.error("시작년도가 종료년도 이후로 설정할 수 없습니다."); } else { - if (toNum - fromNum > 5) { + if (toNum - fromNum > 4) { toast.error("최대 5년까지 선택 가능합니다."); } else { tableHeadMaker(fromNum, toNum); @@ -558,6 +602,7 @@ export default function PbnstPrfmncSttn() { } } } + isResetPage(true) }} excelClick={excelClick} initClick={function (): void { }} @@ -569,77 +614,100 @@ export default function PbnstPrfmncSttn() { <div className="col-span-6"> <ListDiv> <UiTable - headName={[]} + headName={undefined} tableData={bdstTable || []} headlist={pbnstPrfHeadList} - pageSize={20} - total={10000} + pageSize={5} + total={prfmncResCount} cellClick={(index: number | undefined, headId: string) => { cellClick(index ?? undefined, headId); }} - onPageChange={() => { } - } + onPageChange={setPageNation} index={0} + resetPageNo={resetPage} /> <hr /> <h3 className="mt-4 ml-2 text-2xl font-medium text-label">하수재이용실적 상세현황</h3> <SelectBox - className="h-full w-1/12 text-2xl" + className="h-full w-1/12 text-2xl mt-2" label="" selectClass={""} selectArray={selectYearList} value={year} onSelectValue={(val: string | number) => setYear(String(val))} /> - <h1 className="">장내·외 용도별 하수재이용실적 현황(계)</h1> + <h1 className="my-2">장내·외 용도별 하수재이용실적 현황(계)</h1> <div className="flex"> <div className="w-1/3"> - <MainChart chartConfig={chartConfig} chartData={chartData} chartTitle={"장내·외 용수 총 실적"} chartForm={CHART_FORM.DN} /> + {chartData.find((e) => e.val > 0) ? + <MainChart chartConfig={chartConfig} chartData={chartData} chartTitle={"장내·외 용수 총 실적(m³)"} chartForm={CHART_FORM.DN} /> + : + <div className="flex items-center justify-center text-2xl"> + 조회된 장내·외 용수 총 실적(m³) 데이터가 없습니다. + </div>} </div> <div className="w-1/3"> - <MainChart chartConfig={dnInChartConfig} chartData={dnInUsage} chartTitle={"장내용수 실적"} chartForm={CHART_FORM.DN} /> + {dnInUsage.find((e) => e.val > 0) ? + <MainChart chartConfig={dnInChartConfig} chartData={dnInUsage} chartTitle={"장내용수 실적(m³)"} chartForm={CHART_FORM.DN} /> + : + <div className="flex items-center justify-center text-2xl"> + 조회된 장내용수 실적(m³) 데이터가 없습니다. + </div>} </div> <div className="w-1/3"> - <MainChart chartConfig={dnOutChartConfig} chartData={dnOutUsage} chartTitle={"장외용수 실적"} chartForm={CHART_FORM.DN} /> + {dnOutUsage.find((e) => e.val > 0) ? + <MainChart chartConfig={dnOutChartConfig} chartData={dnOutUsage} chartTitle={"장외용수 실적(m³)"} chartForm={CHART_FORM.DN} /> + : + <div className="flex items-center justify-center text-2xl"> + 조회된 장외용수 실적(m³) 데이터가 없습니다. + </div>} </div> + </div> <h1 className="">장내·외 월별 하수재이용실적 현황</h1> <div className="flex"> <div className="w-1/3"> - <MainChart chartConfig={barChartConfig} chartData={barData} chartTitle={"월별 장내·외 용수 총 실적"} chartForm={CHART_FORM.DOUBLEBAR} /> + {barData.length > 0 ? + <MainChart chartConfig={barChartConfig} chartData={barData} chartTitle={"월별 장내·외 용수 총 실적(m³)"} chartForm={CHART_FORM.DOUBLEBAR} showLegend={true} /> + : + <div className="flex items-center justify-center text-2xl"> + 조회된 월별 장내·외 용수 총 실적(m³) 데이터가 없습니다. + </div>} </div> <div className="w-1/3"> - <MainChart chartConfig={lnInChartConfig} chartData={inLineData} chartTitle={"월별 장내 용수 실적"} chartForm={CHART_FORM.LINE} /> + {inLineData && Object.values(inLineData).every(arr => Array.isArray(arr) && arr.length === 0) ? + <div className="flex items-center justify-center text-2xl"> + 조회된 월별 장내 용수 실적(m³) 데이터가 없습니다. + </div> + : + <MainChart chartConfig={lnInChartConfig} chartData={inLineData} chartTitle={"월별 장내 용수 실적(m³)"} chartForm={CHART_FORM.LINE} /> + } </div> <div className="w-1/3"> - <MainChart chartConfig={lnOutChartConfig} chartData={outLineData} chartTitle={"월별 장외 용수 총 실적"} chartForm={CHART_FORM.LINE} /> - </div> + {outLineData && Object.values(outLineData).every(arr => Array.isArray(arr) && arr.length === 0) ? + <div className="flex items-center justify-center text-2xl"> + 조회된 월별 장외 용수 총 실적(m³) 데이터가 없습니다. + </div> + : + <MainChart chartConfig={lnOutChartConfig} chartData={outLineData} chartTitle={"월별 장외 용수 총 실적(m³)"} chartForm={CHART_FORM.LINE} /> + }</div> </div> - <h1 className="">하수재이용실적 현황</h1> + <h1 className="">하수재이용실적 현황 (단위: m³)</h1> <ListDiv className="col-span-8 pr-6 "> - <ScrollArea className="h-[60vh]"> - <div className="card-header"> - <h3 className="text-2xl font-medium text-label"> - {"하수처리수재이용 실적(m3) 등록결과"} - </h3> - </div> - - <UiTable - headName={undefined} - tableData={prfData || []} - headlist={monthHeadList} - pageSize={0} - total={0} - cellClick={undefined} - onPageChange={function (page: number): void { - throw new Error("Function not implemented."); - }}> - </UiTable> + <ScrollArea className={prfData && prfData.length > 0 ? "h-[60vh]" : ""}> + <Table variant="secondary"> + <TableHeader className="font-bold"> + {headMakeprfmnc(monthHeadList, [])} + </TableHeader> + <TableBody> + {/* {InputTableBody(prfData ?? [], monthHeadList, setPrfData)} */} + {InputTableBody(prfData ?? [], monthHeadList, setPrfData, false)} + </TableBody> + </Table> </ScrollArea> </ListDiv> - </ListDiv> </div> </div> diff --git a/apps/wr/app/stats/matStat/page.tsx b/apps/wr/app/stats/matStat/page.tsx deleted file mode 100644 index a147ff597..000000000 --- a/apps/wr/app/stats/matStat/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -'use client' -import { useWrStore } from "@/app/store"; -import { yearOptions } from "@/components/lib/func/commom"; -import { ContentsDiv, Title, SearchDiv, SearchFormLeft, SelectBox, UiTable } from "@common/business_components/ui"; -import ListDiv from "@common/business_components/ui/list_div"; -import { Button } from "@common/components/ui"; -import { useState } from "react"; - -export default function MatStat(){ -const { code, matStat } = useWrStore((state) => { - return state; - }); - const [pageNation, setPageNation] = useState(1); - const [searchYear, setSearchYear] = useState<string>( - String(new Date().getFullYear()) - ); - - return( - <> - <ContentsDiv> - <div className="grid grid-cols-4 gap-4"> - <div className="col-span-4"> - <Title title={"재이용 통계"} /> - <SearchDiv> - <SearchFormLeft> - <SelectBox - className="w-full h-full text-2xl" - label="통계년도" - selectClass={""} - selectArray={yearOptions} - value={searchYear} - onSelectValue={(val: string | number) => - setSearchYear(String(val)) - } - /> - <div className="flex flex-row space-x-2 items-center"> - <Button className="" size={"sm"} onClick={(e) => {}}> - 조회 - </Button> - <Button className="" size={"sm"} onClick={(e) => {}}> - 다운로드 - </Button> - <Button className="" size={"sm"} onClick={(e) => {}}> - 전체통계다운로드 - </Button> - </div> - </SearchFormLeft> - {/* <SearchFormRight> - - </SearchFormRight> */} - </SearchDiv> - </div> - </div> - <div className="grid grid-cols-6 gap-4"> - <div className="col-span-6 h-full min-h-0"> - <ListDiv className="min-h-0"> - {/* <ScrollArea className="h-[60vh]"> */} - <UiTable - tableData={[]} - headlist={matStat.matStatHeadList} - pageSize={20} - total={10000} - headName={"건축물 목록"} - cellClick={(val, headId) => {}} - onPageChange={setPageNation} - ></UiTable> - {/* </ScrollArea> */} - </ListDiv> - </div> - </div> - </ContentsDiv> - </> - ) -} \ No newline at end of file diff --git a/apps/wr/app/stats/rainStat/page.tsx b/apps/wr/app/stats/rainStat/page.tsx index 08990028f..17e2d4bae 100644 --- a/apps/wr/app/stats/rainStat/page.tsx +++ b/apps/wr/app/stats/rainStat/page.tsx @@ -16,50 +16,44 @@ import { import ListDiv from "@common/business_components/ui/list_div"; import { Button } from "@common/components/ui"; import { useEffect, useState } from "react"; +import toast from "react-hot-toast"; +import { useTranslation } from "react-i18next"; export default function RainStat() { const { code, rainStat, searchRainStatList } = useWrStore((state) => state); - const { rainStatList, excelList, rainDetailList } = rainStat; + const { rainStatList, rainDetailList } = rainStat; const [searchYear, setSearchYear] = useState<string>( String(new Date().getFullYear()) ); - const [exData, setExData] = useState<Array<Record<string, string | number>>>(); + //const [exData, setExData] = useState<Array<Record<string, string | number>>>(); const [bdstTable, setBdstTable] = useState<RainStatProps[]>(); + const { t } = useTranslation('base'); useEffect(() => { - searchRainStatList(searchYear); - }, [code.cityCd, code.sggCd, searchRainStatList, searchYear]) + searchRainStatList(searchYear ,code.cityCd); + }, [code.cityCd, searchRainStatList]) const excelDetailDownLoad=() =>{ const sidoList = new Set<string>(); const sidoDataMap = new Map<string, (string | number)[]>(); // 시도별 데이터를 저장할 Map const sidoDetailMap = new Map<string, (string | number)[][]>(); // 시도별 상세 데이터를 저장할 Map - // 먼저 시도별 상세 데이터를 그룹화 - rainDetailList.forEach((item: RainStatProps[]) => { + // 복사된 배열을 사용 + const transformedRainDetailList = rainDetailList.map((originItem: RainStatProps[]) => { + const item = [...originItem]; // 원본 복사 const sidoVal = item.at(0)?.toString().trim(); const sigunVal = item.at(1)?.toString().trim(); - let sidoText = ''; - let sigunText = ''; - const sido = code.cityCd.find((city) => city.val === sidoVal); const sigun = code.sggCd.find((sgg) => sgg.val === sigunVal); - sidoText = sido?.text || ''; - sigunText = sigun?.text || ''; - - // 시도, 시군구 코드를 텍스트로 변환하여 데이터에 할당 - if (item.length > 0) { - item[0] = sidoText as unknown as RainStatProps; - } - if (item.length > 1) { - item[1] = sigunText as unknown as RainStatProps; - } - if (item.length > 2) { - item[2] = sidoText+' '+sigunText as unknown as RainStatProps; - } + const sidoText = sido?.text || ''; + const sigunText = sigun?.text || ''; + + if (item.length > 0) item[0] = sidoText as unknown as RainStatProps; + if (item.length > 1) item[1] = sigunText as unknown as RainStatProps; + if (item.length > 2) item[2] = `${sidoText} ${sigunText}` as unknown as RainStatProps; // 시도별 상세 데이터 수집 if (sidoText) { @@ -72,53 +66,40 @@ export default function RainStat() { // 시도별 합계 데이터 수집 if (sidoText && !sidoList.has(sidoText)) { sidoList.add(sidoText); - // 시도별 초기 데이터 구조 생성 (예: [시도명, 0, 0, 0, ...]) const initialData: (string | number)[] = [sidoText]; - // 데이터의 나머지 컬럼들을 0으로 초기화 (실제 컬럼 수에 맞게 조정 필요) for (let i = 1; i < item.length; i++) { initialData.push(0); } sidoDataMap.set(sidoText, initialData); } + + return item; }); - // 시도별 합계 계산 - rainDetailList.forEach((item: RainStatProps[]) => { + // 이후 합계 계산에서도 transformedRainDetailList 사용 + transformedRainDetailList.forEach((item: RainStatProps[]) => { const sidoText = item[0]?.toString(); if (sidoText && sidoDataMap.has(sidoText)) { const sidoData = sidoDataMap.get(sidoText)!; - // 각 컬럼의 값을 합계에 더하기 (1번 인덱스부터 시작) for (let i = 1; i < item.length && i < sidoData.length; i++) { if(i == 2){ sidoData[i] = '합계'; - } else if(i == 3){ - // 해당 시도의 데이터 개수를 계산 - const sidoDataCount = rainDetailList.filter((data: unknown[]) => + } else if (i == 3) { + const sidoDataCount = transformedRainDetailList.filter((data: unknown[]) => Array.isArray(data) && data.length > 0 && String(data[0]) === sidoText ).length; sidoData[i] = sidoDataCount; } else if( i == 4){ sidoData[i] = ''; - } else if(i == 6){ - // 법정 대상 건축물 개수 (인덱스 6이 'Y'인 경우) - const bumCnt = rainDetailList.filter((data: unknown[]) => + } else if (i == 6) { + const bumCnt = transformedRainDetailList.filter((data: unknown[]) => Array.isArray(data) && data.length > 6 && String(data[0]) === sidoText ).filter((data: unknown[]) => String(data[6]) === '대상').length; sidoData[i] = bumCnt; - } else if(i==8){ - const value = Number(item[8]) || 0; + } else if ([8, 11, 12, 14].includes(i)) { + const value = Number(item[i]) || 0; sidoData[i] = (Number(sidoData[i]) + value) as string | number; - } else if(i==11){ - const value = Number(item[11]) || 0; - sidoData[i] = (Number(sidoData[i]) + value) as string | number; - } else if(i==12){ - const value = Number(item[12]) || 0; - sidoData[i] = (Number(sidoData[i]) + value) as string | number; - } else if(i==14){ - const value = Number(item[14]) || 0; - sidoData[i] = (Number(sidoData[i]) + value) as string | number; - } - else { + } else { sidoData[i] = ''; } } @@ -144,7 +125,7 @@ export default function RainStat() { }); const sidoSummaryList = Array.from(sidoDataMap.values()); - const sidoTotalSummary = [['전체', '', '합계', 0, '', '', 0, '', '', 0, 0, 0, 0, 0, 0, '', 0]]; + const sidoTotalSummary = [['전국', '', '합계', 0, '', '', 0, '', '', 0, 0, 0, 0, 0, 0, '']]; sidoSummaryList.forEach((item: (string | number)[]) => { item.forEach((value: string | number, index: number) => { if(typeof value === 'number') { @@ -158,48 +139,77 @@ export default function RainStat() { // sidoSummaryList와 rainDetailList를 합쳐서 엑셀 데이터 생성 const excelData = [...sidoTotalSummary, ...sidoSummaryList, ...summaryExcelData]; + + console.log(' excelData ', excelData) excelDownload( excelData as unknown as [][], "빗물이용시설 통계", "sample_rainStats.xlsx", - "A1:V3", + "A1:O3", 4 ); } const excelClick = () => { + if(bdstTable){ + const tbData = bdstTable .map((item: RainStatProps) => { + return { + "시도": item.cityText, + "개소": item.tmp, + "설치비(백만원)": item.instlCst, + "저류조용량(m³/년)": item.dtfcltCpct, + "연간사용량(m³/년)": item.wtrUsqty, + "조경용수": item.lndwtr, + "청소화장실용수": item.cleanToilUswtr, + "세척살수용수": item.resn, + "농업용수": item.farmUswtr, + "공업용수": item.indstUswtr, + "친수용수": item.hydUswtr, + "기타": item.ithlEtcUswtr, + "연간운영비(백만원/년)": item.anlOperCst, + }; + }) downloadExcel({ - data: (exData as unknown as JSON[]) ?? [], + data: (tbData as unknown as JSON[]) ?? [], fileName: "재이용통계", sheetNameArray: ["Sheet1"], headerRange: "A1:M1" }) + }else{ + toast.error(t('excelDownErrMsg')); + } } - - useEffect(() => { - const transExcel = Array.isArray(excelList) ? excelList : [excelList]; - const excelData = tableData(transExcel, { code }); - const tbData = Array.isArray(excelData) - ? excelData.map((item: RainStatProps) => { - return { - "시도": item.cityText, - "개소": item.tmp, - "설치비(백만원)": item.instlCst, - "저류조용량(m³/년)": item.dtfcltCpct, - "연간사용량(m³/년)": item.wtrUsqty, - "조경용수": item.lndwtr, - "청소화장실용수": item.cleanToilUswtr, - "세척살수용수": item.resn, - "농업용수": item.farmUswtr, - "공업용수": item.indstUswtr, - "친수용수": item.hydUswtr, - "기타": item.ithlEtcUswtr, - "연간운영비(백만원/년)": item.anlOperCst, - }; - }) - : []; - setExData(tbData); - }, [code, excelList]) + + // useEffect(() => { + // if(excelList && excelList.length>0){ + // console.log('excelList ', excelList) + + // const transExcel = Array.isArray(excelList) ? excelList : [excelList]; + // const excelData = tableData(transExcel, { code }); + // const tbData = Array.isArray(excelData) + // ? excelData + // .filter((item: RainStatProps) => item.cityText !== '없음') + // .map((item: RainStatProps) => { + // return { + // "시도": item.cityText, + // "개소": item.tmp, + // "설치비(백만원)": item.instlCst, + // "저류조용량(m³/년)": item.dtfcltCpct, + // "연간사용량(m³/년)": item.wtrUsqty, + // "조경용수": item.lndwtr, + // "청소화장실용수": item.cleanToilUswtr, + // "세척살수용수": item.resn, + // "농업용수": item.farmUswtr, + // "공업용수": item.indstUswtr, + // "친수용수": item.hydUswtr, + // "기타": item.ithlEtcUswtr, + // "연간운영비(백만원/년)": item.anlOperCst, + // }; + // }) + // : []; + // setExData(tbData); + // } + // }, [code, excelList]) useEffect(() => { const transData = Array.isArray(rainStatList) ? rainStatList : [rainStatList]; @@ -208,10 +218,13 @@ export default function RainStat() { ? data.map((item: RainStatProps) => { return { ...item, - ctpvCd: item.cityText + ctpvCd: item.cityText==='전체'?'전국': item.cityText, + cityText: item.cityText==='전체'?'전국': item.cityText, }; - }) + }).filter((item: RainStatProps) => item.cityText !== '없음') : []; + + setBdstTable(tbData); }, [code, rainStatList]); @@ -236,7 +249,7 @@ export default function RainStat() { /> </div> <div className="flex flex-row space-x-2 items-center"> - <Button className="" size={"sm"} onClick={() => { }}> + <Button className="" size={"sm"} onClick={() => { searchRainStatList(searchYear ,code.cityCd); }}> 조회 </Button> <Button className="" size={"sm"} onClick={() => excelClick()}> diff --git a/apps/wr/app/stats/rswtStat/page.tsx b/apps/wr/app/stats/rswtStat/page.tsx index c0741433c..99a8bc4f3 100644 --- a/apps/wr/app/stats/rswtStat/page.tsx +++ b/apps/wr/app/stats/rswtStat/page.tsx @@ -14,52 +14,30 @@ import { import ListDiv from "@common/business_components/ui/list_div"; import { Button } from "@common/components/ui"; import { useEffect, useState } from "react"; +import toast, { Toaster } from "react-hot-toast"; +import { useTranslation } from "react-i18next"; export default function RswtStat() { const { code, rswtStat, searchRswtStatList } = useWrStore((state) => state); - const { rswtStatList, excelList, rswtDetailList } = rswtStat; + const { rswtStatList, rswtDetailList } = rswtStat; const [rswtTable, setRswtTable] = useState<RswtStatProps[]>(); const [searchYear, setSearchYear] = useState<string>( String(new Date().getFullYear()) ); - const [excelData, setExceData] = useState<Record<string, string | number>[]>(); + const { t } = useTranslation('base'); useEffect(() => { - searchRswtStatList(searchYear) - }, [searchRswtStatList, searchYear]) + searchRswtStatList(searchYear, code.cityCd) + }, [searchRswtStatList, code.cityCd]) const excelClick = () => { - downloadExcel({ - data: (excelData as unknown as JSON[]) ?? [], - fileName: "중수도재이용통계", - sheetNameArray: ["Sheet1"], - headerRange: "A1:O1" - }) - } - useEffect(() => { - const transData = Array.isArray(rswtStatList) ? rswtStatList : [rswtStatList]; - const data = tableData(transData, { code }); - const tbData = Array.isArray(data) - ? data.map((item: RswtStatProps) => { - return { - ...item, - ctpvCd: item.cityText - }; - }) - : []; - setRswtTable(tbData); - }, [code, rswtStatList]) - - useEffect(() => { - const transExcel = Array.isArray(excelList) ? excelList : [excelList]; - const exData = tableData(transExcel, { code }); - const tbData = Array.isArray(exData) ? - exData.map((item: RswtStatProps) => { + if (rswtTable) { + const data = rswtTable.map((item: RswtStatProps) => { return { "시도": item.cityText, "개소": item.ctpvCnt, - "건축연면적(m³)": item.archArea, + // "건축연면적(m³)": item.archArea, "처리용량(m³)": item.filtrTrtmfctCpct, "물사용량(m³/년)": item.wtrUsqty, "중수도+대체수이용량(m³/년)": item.rswtAltSum, @@ -73,17 +51,44 @@ export default function RswtStat() { "기타": item.ithlEtcUswtr, "연간운영비(백만원/년)": item.anlOperCst, }; - }) : []; - setExceData(tbData); - }, [code, excelList]) + }) + + downloadExcel({ + data: (data as unknown as JSON[]) ?? [], + fileName: "중수도재이용통계", + sheetNameArray: ["Sheet1"], + headerRange: "A1:N1" + }) + } else { + toast.error(t("excelDownErrMsg")) + } + } + + useEffect(() => { + const transData = Array.isArray(rswtStatList) ? rswtStatList : [rswtStatList]; + const cityData = tableData(transData, { code }); + const tbData = Array.isArray(cityData) + ? cityData.map((item: RswtStatProps) => { + return { + ...item, + ctpvCd: item.cityText === '전체' ? '전국' : item.cityText, + cityText: item.cityText === '전체' ? '전국' : item.cityText + }; + }) : []; + + setRswtTable(tbData); + }, [code, rswtStatList]); + + const excelDetailDownLoad = () => { - const excelDetailDownLoad=() =>{ const sidoList = new Set<string>(); const sidoDataMap = new Map<string, (string | number)[]>(); // 시도별 데이터를 저장할 Map const sidoDetailMap = new Map<string, (string | number)[][]>(); // 시도별 상세 데이터를 저장할 Map // 먼저 시도별 상세 데이터를 그룹화 - rswtDetailList.forEach((item: RswtStatProps[]) => { + const transformedRswtDetailList = rswtDetailList.map((originItem: RswtStatProps[]) => { + // rswtDetailList.forEach((item: RswtStatProps[]) => { + const item = [...originItem]; // 원본 복사 const sidoVal = item.at(0)?.toString().trim(); const sigunVal = item.at(1)?.toString().trim(); let sidoText = ''; @@ -94,7 +99,7 @@ export default function RswtStat() { sidoText = sido?.text || ''; sigunText = sigun?.text || ''; - + // 시도, 시군구 코드를 텍스트로 변환하여 데이터에 할당 if (item.length > 0) { item[0] = sidoText as unknown as RswtStatProps; @@ -103,7 +108,7 @@ export default function RswtStat() { item[1] = sigunText as unknown as RswtStatProps; } if (item.length > 2) { - item[2] = sidoText+' '+sigunText as unknown as RswtStatProps; + item[2] = sidoText + ' ' + sigunText as unknown as RswtStatProps; } // 시도별 상세 데이터 수집 @@ -113,7 +118,7 @@ export default function RswtStat() { } sidoDetailMap.get(sidoText)!.push(item as unknown as (string | number)[]); } - + // 시도별 합계 데이터 수집 if (sidoText && !sidoList.has(sidoText)) { sidoList.add(sidoText); @@ -125,50 +130,51 @@ export default function RswtStat() { } sidoDataMap.set(sidoText, initialData); } + return item; }); // 시도별 합계 계산 - rswtDetailList.forEach((item: RswtStatProps[]) => { + transformedRswtDetailList.forEach((item: RswtStatProps[]) => { const sidoText = item[0]?.toString(); if (sidoText && sidoDataMap.has(sidoText)) { const sidoData = sidoDataMap.get(sidoText)!; // 각 컬럼의 값을 합계에 더하기 (1번 인덱스부터 시작) for (let i = 1; i < item.length && i < sidoData.length; i++) { - if(i == 2){ + if (i == 2) { sidoData[i] = '합계'; - } else if(i == 3){ + } else if (i == 3) { // 해당 시도의 데이터 개수를 계산 - const sidoDataCount = rswtDetailList.filter((data: unknown[]) => + const sidoDataCount = rswtDetailList.filter((data: unknown[]) => Array.isArray(data) && data.length > 0 && String(data[0]) === sidoText ).length; sidoData[i] = sidoDataCount; - } else if( i == 4){ + } else if (i == 4) { sidoData[i] = ''; - } else if(i == 6){ + } else if (i == 6) { // 법정 대상 건축물 개수 (인덱스 6이 'Y'인 경우) - const bumCnt = rswtDetailList.filter((data: unknown[]) => + const bumCnt = rswtDetailList.filter((data: unknown[]) => Array.isArray(data) && data.length > 6 && String(data[0]) === sidoText ).filter((data: unknown[]) => String(data[6]) === '대상').length; sidoData[i] = bumCnt; - } else if(i==9){ + } else if (i == 9) { const value = Number(item[9]) || 0; sidoData[i] = (Number(sidoData[i]) + value) as string | number; - } else if(i==10){ + } else if (i == 10) { const value = Number(item[10]) || 0; sidoData[i] = (Number(sidoData[i]) + value) as string | number; - } else if(i==11){ + } else if (i == 11) { const value = Number(item[11]) || 0; sidoData[i] = (Number(sidoData[i]) + value) as string | number; - } else if(i==12){ + } else if (i == 12) { const value = Number(item[12]) || 0; sidoData[i] = (Number(sidoData[i]) + value) as string | number; - }else if(i==13){ + } else if (i == 13) { const value = Number(item[13]) || 0; sidoData[i] = (Number(sidoData[i]) + value) as string | number; - } else if(i==14){ + } else if (i == 14) { const value = Number(item[14]) || 0; sidoData[i] = (Number(sidoData[i]) + value) as string | number; - } else if(i==16){ + } else if (i == 16) { const value = Number(item[16]) || 0; sidoData[i] = (Number(sidoData[i]) + value) as string | number; } else { @@ -177,15 +183,15 @@ export default function RswtStat() { } } }); - + // 시도별로 합계와 상세 데이터를 순서대로 배치 const summaryExcelData: (string | number)[][] = []; - + // 시도별로 순회하면서 합계와 상세 데이터를 추가 Array.from(sidoList).forEach((sidoText) => { const sidoSummary = sidoDataMap.get(sidoText); const sidoDetails = sidoDetailMap.get(sidoText) || []; - + if (sidoSummary) { // 시도 합계 데이터 추가 summaryExcelData.push(sidoSummary); @@ -197,10 +203,10 @@ export default function RswtStat() { }); const sidoSummaryList = Array.from(sidoDataMap.values()); - const sidoTotalSummary = [['전체', '', '합계', 0, '', '', 0, '', '', 0, 0, 0, 0, 0, 0, '', 0]]; + const sidoTotalSummary = [['전국', '', '합계', 0, '', '', 0, '', '', 0, 0, 0, 0, 0, 0, '']]; sidoSummaryList.forEach((item: (string | number)[]) => { item.forEach((value: string | number, index: number) => { - if(typeof value === 'number') { + if (typeof value === 'number') { if (sidoTotalSummary[0] && index < sidoTotalSummary[0].length) { sidoTotalSummary[0][index] = Number(sidoTotalSummary[0][index]) + Number(value); } @@ -209,13 +215,13 @@ export default function RswtStat() { }); // sidoSummaryList와 rainDetailList를 합쳐서 엑셀 데이터 생성 - const excelData = [...sidoTotalSummary,...sidoSummaryList, ...summaryExcelData]; + const excelData = [...sidoTotalSummary, ...sidoSummaryList, ...summaryExcelData]; excelDownload( excelData as unknown as [][], "중수도시설 통계", "sample_rswtStats.xlsx", - "A1:Q3", + "A1:P3", 4 ); } @@ -242,7 +248,7 @@ export default function RswtStat() { className="" size="sm" onClick={() => { - searchRswtStatList(searchYear) + searchRswtStatList(searchYear, code.cityCd) }} > 조회 @@ -283,6 +289,7 @@ export default function RswtStat() { </ListDiv> </div> </div> + <Toaster position="top-center"></Toaster> </ContentsDiv> </> ); diff --git a/apps/wr/app/store/common/auth/index.tsx b/apps/wr/app/store/common/auth/index.tsx new file mode 100644 index 000000000..21be6e01c --- /dev/null +++ b/apps/wr/app/store/common/auth/index.tsx @@ -0,0 +1,60 @@ +import { getFetchWithCount } from "@/components/lib/api"; +import toast from "react-hot-toast"; + +export type authState = { + auth: { + mngAuthrtYn: string; + inqAuthrtYn: string; + regAuthrtYn: string; + mdfcnAuthrtYn: string; + delAuthrtYn: string; + }; +}; + +export type authType = authState & authActions; + +export const authStateExport: authState = { + auth: { + mngAuthrtYn: "Y", + inqAuthrtYn: "Y", + regAuthrtYn: "Y", + mdfcnAuthrtYn: "Y", + delAuthrtYn: "Y", + }, +}; + +export type authActions = { + getMenuAuthrts: (menuCd: number) => void; +}; + +export const authActionsExport: (set: any) => authActions = (set: any) => { + return { + getMenuAuthrts: async (menuCd: number) => { + try{ + + const response = await getFetchWithCount( 'LOCAL', + + `/cms-service/api/v1/menu-authrts/pageinfo/${menuCd}` + ); + set((state: authState) => { + return { + ...state, + auth: { + mngAuthrtYn: response[0].mngAuthrtYn, + inqAuthrtYn: response[0].mngAuthrtYn, + regAuthrtYn: response[0].mngAuthrtYn, + mdfcnAuthrtYn: response[0].mngAuthrtYn, + delAuthrtYn: response[0].mngAuthrtYn, + }, + }; + }); + + }catch(e){ + + toast.error('권한 정보를 가져오는 중 오류가 발생했습니다. 관리자에게 문의하세요.'); + console.error('Error fetching menu authorities:', e); + + } + }, + }; +}; diff --git a/apps/wr/app/store/common/auth/type.tsx b/apps/wr/app/store/common/auth/type.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/apps/wr/app/store/common/code/basicCode.tsx b/apps/wr/app/store/common/code/basicCode.tsx new file mode 100644 index 000000000..12b01f952 --- /dev/null +++ b/apps/wr/app/store/common/code/basicCode.tsx @@ -0,0 +1,223 @@ +import type { SelectDataType } from "@common/business_components"; + +export const numSize = 12; +export const txtSize = 20; + +export const yn: SelectDataType[] = [ + { val: "Y", text: "Y" }, + { val: "N", text: "N" }, +]; +export const bdstFilterYn: SelectDataType[] = [ + { val: "Y", text: "Y" }, + { val: "N", text: "N" }, + { val: "D", text: "확인불가" }, + { val: "F", text: "미가동" }, +]; +// 최우수(그린1)/우수(그린2), 우량(그린3), 일반(그린4), 확인불가 체크 +export const bdstGrntchClass: SelectDataType[] = [ + { val: "최우수(그린1)", text: "최우수(그린1)" }, + { val: "우수(그린2)", text: "우수(그린2)" }, + { val: "우량(그린3)", text: "우량(그린3)" }, + { val: "일반(그린4)", text: "일반(그린4)" }, + { val: "확인불가", text: "확인불가" }, +]; +export const bdstIncenYn: SelectDataType[] = [ + { val: "Y", text: "Y" }, + { val: "N", text: "N" }, + { val: "D", text: "확인불가" }, +]; +//처리장연계/자체+연계/자체처리 +export const erctUswtrPrcsMeth: SelectDataType[] = [ + { val: "처리장연계", text: "처리장연계" }, + { val: "자체+연계", text: "자체+연계" }, + { val: "자체처리", text: "자체처리" }, +]; + +export const rusePrcsMthdCd: SelectDataType[] = [ + { val: "물리학적처리", text: "물리학적처리" }, + { val: "생물학적처리", text: "생물학적처리" }, +]; + +export const ruseSeCd: SelectDataType[] = [ + { val: "신규", text: "신규" }, + { val: "증설", text: "증설" }, + { val: "관로", text: "관로" }, + { val: "감량", text: "감량" }, + { val: "개량", text: "개량" }, +]; + +export const ruseBizMthdCd: SelectDataType[] = [ + { val: "재정", text: "재정" }, + { val: "민간투자", text: "민간투자" }, + { val: "자체", text: "자체" }, +]; + +//공통 건축물 용도 +export const bdstUsgCd: SelectDataType[] = [ + { val: "-1", text: "확인불가" }, + { val: "1", text: "운동장" }, + { val: "2", text: "체육관" }, + { val: "3", text: "공공업무시설" }, + { val: "4", text: "공공기관의청사" }, + { val: "5", text: "아파트" }, + { val: "6", text: "연립주택" }, + { val: "7", text: "다세대주택" }, + { val: "8", text: "기숙사" }, + { val: "9", text: "초등학교" }, + { val: "10", text: "중학교" }, + { val: "11", text: "고등학교" }, + { val: "12", text: "전문대학" }, + { val: "13", text: "대학및대학교" }, + { val: "14", text: "골프장" }, + { val: "15", text: "대규모점포" }, + { val: "16", text: "기타" }, +]; + +export const rswtBdstType: SelectDataType[] = [ + { text: "숙박업", val: "17" }, + { text: "목욕장업", val: "18" }, + { text: "공장", val: "19" }, + { text: "발전시설", val: "20" }, + { text: "관광단지개발사업", val: "21" }, + { text: "도시개발사업", val: "22" }, + { text: "산업단지개발사업", val: "23" }, + { text: "택지개발사업", val: "24" }, + { text: "대규모점포(대형마트)", val: "25" }, + { text: "대규모점포(전문점)", val: "26" }, + { text: "대규모점포(백화점)", val: "27" }, + { text: "대규모점포(쇼핑센터)", val: "28" }, + { text: "대규모점포(복합쇼핑몰)", val: "29" }, + { text: "물류시설", val: "30" }, + { text: "운수시설(여객자동차터미널)", val: "31" }, + { text: "운수시설(철도시설)", val: "32" }, + { text: "운수시설(공항시설)", val: "33" }, + { text: "운수시설(항만시설)", val: "34" }, + { text: "업무시설(공공업무시설)", val: "35" }, + { text: "업무시설(금융업소)", val: "36" }, + { text: "업무시설(사무소)", val: "37" }, + { text: "업무시설(결혼상담소 등 소개업소)", val: "38" }, + { text: "업무시설(출판사)", val: "39" }, + { text: "업무시설(신문사)", val: "40" }, + { text: "업무시설(오피스텔)", val: "41" }, + { text: "교정시설", val: "42" }, + { text: "방송국", val: "43" }, + { text: "전신전화국", val: "44" }, + { text: "기타", val: "45" }, +]; + +export const orgwtKndCd = [ + { text: "오수(변기 오수 제외)", val: "1" }, + { text: "오수", val: "2" }, + { text: "폐수", val: "3" }, + { text: "오수 및 폐수", val: "4" }, + { text: "기타", val: "5" }, +]; + +export const chBuCd = [ + { val: "증축", text: "증축" }, + { val: "개축", text: "개축" }, + { val: "재축", text: "재축" }, +]; + +export const dmdUszCd = [ + { val: "조경용수", text: "조경용수" }, + { val: "청소화장실용수", text: "청소화장실용수" }, + { val: "농업용수", text: "농업용수" }, + { val: "공업용수", text: "공업용수" }, + { val: "친수용수", text: "친수용수" }, + { val: "기타", text: "기타" }, +]; + +export const ruseMngMtd = [ + { val: "자체운영", text: "자체운영" }, + { val: "공기업", text: "공기업" }, + { val: "민간(대행)", text: "민간(대행)" }, +]; + +export const pplQulity = [ + { val: "도복장강관", text: "도복장강관" }, + { val: "PE관", text: "PE관" }, + { val: "스테인레스관", text: "스테인레스관" }, + { val: "덕타일주철관", text: "덕타일주철관" }, + { val: "유리섬유복합관", text: "유리섬유복합관" }, + { val: "기타", text: "기타" }, +]; +export const pplLenght = [ + { val: "D100", text: "D100" }, + { val: "D200", text: "D200" }, + { val: "D250", text: "D250" }, + { val: "D300", text: "D300" }, + { val: "D350", text: "D350" }, + { val: "D400", text: "D400" }, + { val: "D450", text: "D450" }, + { val: "D500", text: "D500" }, + { val: "D700", text: "D700" }, + { val: "D900", text: "D900" }, + { val: "D1000​", text: "D1000​" }, +]; + +export const rainFcltCd = [ + { val: "지붕", text: "지붕" }, + { val: "건물옥상", text: "건물옥상" }, + { val: "골프장 연못", text: "골프장 연못" }, + { val: "운동장 바닥", text: "운동장 바닥" }, + { val: "운동장 지붕", text: "운동장 지붕" }, + { val: "주차장", text: "주차장" }, + { val: "태양광패널", text: "태양광패널" }, + { val: "연못", text: "연못" }, + { val: "기타", text: "기타" }, + { val: "확인불가", text: "확인불가" }, +]; + +//물리화학적처리(스크린, 응집, 침전)/생물학적처리(활성슬러지법 등)/소독(염소, UV 등)/기타 +export const rswtPrscCdMthdCd = [ + { + val: "물리화학적처리(스크린,응집,침전)", + text: "물리화학적처리(스크린,응집,침전)", + }, + { + val: "생물학적처리(활성슬러지법 등)", + text: "생물학적처리(활성슬러지법 등)", + }, + { val: "소독(염소, UV 등)", text: "소독(염소, UV 등)" }, + { val: "기타", text: "기타" }, +]; + +export const htdPowerMthdCd= [ + { + val: "해당없음", + text: "해당없음", + }, + { + val: "석탄화력발전", + text: "석탄화력발전", + }, + { + val: "화력발전", + text: "화력발전", + }, + { + val: "수력발전", + text: "수력발전", + }, + { + val: "태양관발전", + text: "태양관발전", + }, +]; + +//(공업용수/양식장/농업용수) +export const htdUtztnUsgCd= [ + { + val: "공업용수", + text: "공업용수", + }, + { + val: "양식장", + text: "양식장", + }, + { + val: "농업용수", + text: "농업용수", + }, +]; diff --git a/apps/wr/app/store/common/code/index.ts b/apps/wr/app/store/common/code/index.ts index e7fca8180..e33b14f94 100644 --- a/apps/wr/app/store/common/code/index.ts +++ b/apps/wr/app/store/common/code/index.ts @@ -1,184 +1,17 @@ import { getFetch } from "@/components/lib/api"; import type { EnvPartSelectDataType, SelectDataType } from "@common/business_components/ui"; - - - +import { bdstFilterYn, bdstGrntchClass, bdstIncenYn, bdstUsgCd, chBuCd, dmdUszCd, erctUswtrPrcsMeth, htdPowerMthdCd, htdUtztnUsgCd, orgwtKndCd, pplLenght, pplQulity, rainFcltCd, rswtPrscCdMthdCd, ruseBizMthdCd, ruseMngMtd, rusePrcsMthdCd, ruseSeCd, yn } from "./basicCode"; export const btnClass = "p-4 m-2 text-white rounded-sm text-lg font-medium " -const yn: SelectDataType[] = [ - { val: 'Y', text: 'Y' }, - { val: 'N', text: 'N' }, -] -//공통 건축물 용도 -const bdstUsgCd: SelectDataType[] = [ - { val: '-1', text: "확인불가" }, - { val: '1', text: "운동장" }, - { val: '2', text: "체육관" }, - { val: '3', text: "공공업무시설" }, - { val: '4', text: "공공기관의청사" }, - { val: '5', text: "아파트" }, - { val: '6', text: "연립주택" }, - { val: '7', text: "다세대주택" }, - { val: '8', text: "기숙사" }, - { val: '9', text: "초등학교" }, - { val: '10', text: "중학교" }, - { val: '11', text: "고등학교" }, - { val: '12', text: "전문대학" }, - { val: '13', text: "대학및대학교" }, - { val: '14', text: "골프장" }, - { val: '15', text: "대규모점포" }, - { val: '16', text: "기타" }, -]; -//빗물 집수면 종류 용도 -// const rainFcltCd = [ -// { val: "1", text: "지붕" }, -// { val: "2", text: "건물옥상" }, -// { val: "3", text: "골프장 연못" }, -// { val: "4", text: "운동장 바닥" }, -// { val: "5", text: "운동장 지붕" }, -// { val: "6", text: "주차장" }, -// { val: "7", text: "태양광패널" }, -// { val: "8", text: "연못" }, -// { val: "9", text: "기타" }, -// { val: "10", text: "확인불가" }, -// ]; -// 중수도 필수 원수종류 -const rswtBdstType: SelectDataType[] = [ - { text: "숙박업", val: "17" }, - { text: "목욕장업", val: "18" }, - { text: "공장", val: "19" }, - { text: "발전시설", val: "20" }, - { text: "관광단지개발사업", val: "21" }, - { text: "도시개발사업", val: "22" }, - { text: "산업단지개발사업", val: "23" }, - { text: "택지개발사업", val: "24" }, - { text: "대규모점포(대형마트)", val: "25" }, - { text: "대규모점포(전문점)", val: "26" }, - { text: "대규모점포(백화점)", val: "27" }, - { text: "대규모점포(쇼핑센터)", val: "28" }, - { text: "대규모점포(복합쇼핑몰)", val: "29" }, - { text: "물류시설", val: "30" }, - { text: "운수시설(여객자동차터미널)", val: "31" }, - { text: "운수시설(철도시설)", val: "32" }, - { text: "운수시설(공항시설)", val: "33" }, - { text: "운수시설(항만시설)", val: "34" }, - { text: "업무시설(공공업무시설)", val: "35" }, - { text: "업무시설(금융업소)", val: "36" }, - { text: "업무시설(사무소)", val: "37" }, - { text: "업무시설(결혼상담소 등 소개업소)", val: "38" }, - { text: "업무시설(출판사)", val: "39" }, - { text: "업무시설(신문사)", val: "40" }, - { text: "업무시설(오피스텔)", val: "41" }, - { text: "교정시설", val: "42" }, - { text: "방송국", val: "43" }, - { text: "전신전화국", val: "44" }, - { text: "기타", val: "45" } -]; - -const orgwtKndCd = [ - { text: "오수(변기 오수 제외)", val: "1" }, - { text: "오수", val: "2" }, - { text: "폐수", val: "3" }, - { text: "오수 및 폐수", val: "4" }, - { text: "기타", val: "5" }, -]; - -const chBuCd = [ - { val: "증축", text: "증축" }, - { val: "개축", text: "개축" }, - { val: "재축", text: "재축" }, -] +const laeSecd = '41' - -const dmdUszCd = [ - { val: "조경용수", text: "조경용수" }, - { val: "청소화장실용수", text: "청소화장실용수" }, - { val: "농업용수", text: "농업용수" }, - { val: "공업용수", text: "공업용수" }, - { val: "친수용수", text: "친수용수" }, - { val: "기타", text: "기타" }, - -] - -const ruseMngMtd = [ - { val: "자체운영", text: "자체운영" }, - { val: "공기업", text: "공기업" }, - { val: "민간(대행)", text: "민간(대행)" }, -] - - -const pplQulity = [ - { val: "도복장강관", text: "도복장강관" }, - { val: "PE관", text: "PE관" }, - { val: "스테인레스관", text: "스테인레스관" }, - { val: "덕타일주철관", text: "덕타일주철관" }, - { val: "유리섬유복합관", text: "유리섬유복합관" }, - { val: "기타", text: "기타" }, - -] -const pplLenght = [ - { val: "D100", text: "D100" }, - { val: "D200", text: "D200" }, - { val: "D250", text: "D250" }, - { val: "D300", text: "D300" }, - { val: "D350", text: "D350" }, - { val: "D400", text: "D400" }, - { val: "D450", text: "D450" }, - { val: "D500", text: "D500" }, - { val: "D700", text: "D700" }, - { val: "D900", text: "D900" }, - { val: "D1000​", text: "D1000​" }, - -] export const buildingType = { RST:"RST", RNB:"RNB", HTB:"HTB" } - -const rainFcltCd = [ - { val: "지붕", text: "지붕" }, - { val: "건물옥상", text: "건물옥상" }, - { val: "골프장 연못", text: "골프장 연못" }, - { val: "운동장 바닥", text: "운동장 바닥" }, - { val: "운동장 지붕", text: "운동장 지붕" }, - { val: "주차장", text: "주차장" }, - { val: "태양광패널", text: "태양광패널" }, - { val: "연못", text: "연못" }, - { val: "기타", text: "기타" }, - { val: "확인불가", text: "확인불가" }, -]; - -export type comCodeState = { - code: { - isInit: boolean; - chBuCd: SelectDataType[] - rainFcltCd: SelectDataType[]; - bdstUsgCd: SelectDataType[]; - cityCd: SelectDataType[]; - sggCd: SelectDataType[]; - rvbsnCd: SelectDataType[]; - mdmbsnCd: SelectDataType[]; - lgbsnCd: SelectDataType[]; - lclgvrnmntCd: EnvPartSelectDataType[]; - orgwtKndCd: SelectDataType[]; - yn: SelectDataType[]; - pplQulity: SelectDataType[]; - pplLenght: SelectDataType[]; - dmdUszCd:SelectDataType[] - buildingType: { - RST:string, - RNB:string, - HTB:string - } - ruseMngMtd:SelectDataType[] - }; -}; - -export type comCodeType = comCodeState & comCodeActions; - export const getSggCd = async (set: any) => { const sgg = await getFetch("common", "/addr/sgg"); /* const sggCd = sgg.map((item: any) => ({ @@ -187,7 +20,7 @@ export const getSggCd = async (set: any) => { })); */ const sggCd = [ - { text: "전체", val: '0' }, + // { text: "전체", val: '0' }, ...sgg .map((item: any) => ({ text: item.gugun || '', @@ -304,6 +137,49 @@ export const getEnvPartCd = async (set: any) => { + +export type comCodeState = { + code: { + isInit: boolean; + chBuCd: SelectDataType[] + rainFcltCd: SelectDataType[]; + bdstUsgCd: SelectDataType[]; + cityCd: SelectDataType[]; + sggCd: SelectDataType[]; + rvbsnCd: SelectDataType[]; + mdmbsnCd: SelectDataType[]; + lgbsnCd: SelectDataType[]; + lclgvrnmntCd: EnvPartSelectDataType[]; + orgwtKndCd: SelectDataType[]; + yn: SelectDataType[]; + pplQulity: SelectDataType[]; + pplLenght: SelectDataType[]; + dmdUszCd:SelectDataType[] + buildingType: { + RST:string, + RNB:string, + HTB:string + } + ruseMngMtd:SelectDataType[] + ruseSeCd: SelectDataType[]; + ruseBizMthdCd: SelectDataType[]; + rusePrcsMthdCd: SelectDataType[]; + erctUswtrPrcsMeth: SelectDataType[]; + bdstFilterYn: SelectDataType[]; + bdstIncenYn: SelectDataType[]; + bdstGrntchClass:SelectDataType[]; + rswtPrscCdMthdCd:SelectDataType[]; + htdPowerMthdCd:SelectDataType[] + htdUtztnUsgCd:SelectDataType[] + userSiSggCd : string; + + }; +}; + +export type comCodeType = comCodeState & comCodeActions; + + + export const comCodeStateExport: comCodeState = { code: { isInit: false, @@ -323,6 +199,17 @@ export const comCodeStateExport: comCodeState = { dmdUszCd:dmdUszCd, buildingType:buildingType, ruseMngMtd:ruseMngMtd, + ruseSeCd:ruseSeCd, + ruseBizMthdCd: ruseBizMthdCd, + rusePrcsMthdCd:rusePrcsMthdCd, + erctUswtrPrcsMeth:erctUswtrPrcsMeth, + bdstFilterYn:bdstFilterYn, + bdstIncenYn:bdstIncenYn, + bdstGrntchClass:bdstGrntchClass, + rswtPrscCdMthdCd:rswtPrscCdMthdCd, + htdPowerMthdCd:htdPowerMthdCd, + htdUtztnUsgCd:htdUtztnUsgCd, + userSiSggCd:laeSecd, }, }; @@ -330,9 +217,6 @@ export type comCodeActions = { codeInit: () => void; }; - - - export const codeActionExport: (set: any) => comCodeActions = (set: any) => { return { codeInit: async () => { diff --git a/apps/wr/app/store/common/login/index.tsx b/apps/wr/app/store/common/login/index.tsx new file mode 100644 index 000000000..2ddd5b495 --- /dev/null +++ b/apps/wr/app/store/common/login/index.tsx @@ -0,0 +1,32 @@ +import { cmsPostFectch } from "@/components/lib/api"; + +export const isLogin = async () => { + const headers = { + accept: "application/json", + "Content-Type": "application/json", + "X-Site-Id": "WR", + "Access-Control-Expose-Headers" : ["Authorization","refresh-token","token-id"] + }; + const param = { userId: "tuser10003", password: "test1234!" }; + await cmsPostFectch("/user-service/login",param,headers); +}; + +// export type LoginState = { +// login: {} as any; +// }; + +// export type loginType = LoginState & LoginActions; + +// export const loginState: LoginState = { +// login: {}, +// }; + +// export type LoginActions = { +// isLogin: () => void; +// }; + +// export const loginActionExport: (set: any) => LoginActions = (set: any) => { +// return { +// isLogin: async () => {}, +// }; +// }; diff --git a/apps/wr/app/store/common/searchCode/index.ts b/apps/wr/app/store/common/searchCode/index.ts index 8f05eca09..03d1b0fc5 100644 --- a/apps/wr/app/store/common/searchCode/index.ts +++ b/apps/wr/app/store/common/searchCode/index.ts @@ -7,16 +7,56 @@ import { import type { pubHasuType } from "../../ruseFcltMng/fcltReg/pbnstReg/type"; import type { DemandCompType } from "@common/business_components/ui/sdmInsertForm/form"; import type { conCostFeeType, rateFeeType } from "./type"; +import toast from "react-hot-toast"; + + +export const comFcltStateExport: comFcltState = { + searchFclts: [] as pubHasuType[], + demandCompList: [], + checkDemand: { + index: 0, + dmdInfoId: "", + bzmnNo: "", + dmdCoNm: "", + daddr: "", + telno: "", + rgtr: "", + useUsgCd: "", + ctpvCd: "", + sggCd: "", + dmdAplyPswd: "", + wntUntprc: 0, + dmndTmng: "", + dmndQty: 0, + grssClfg: 0, + tn: 0, + combResdch: 0, + tph: 0, + trbt: 0, + ph: 0, + blgcOxygDmndQnt: 0, + clIonQnt: 0, + smll: 0, + cdtnv: 0, + crmty: 0, + rmrk: "", + reqDt: "" + }, + constCostRes:[], + rateFeeRes:[], +}; + +export type comFcltType = comFcltState & comFcltActions; + export type comFcltState = { - searchFclts: pubHasuType; + searchFclts: pubHasuType[]; demandCompList: DemandCompType[]; - dmdIsSucees: string; checkDemand: DemandCompType; constCostRes:conCostFeeType[], rateFeeRes:rateFeeType[], }; -export type comFcltActions = { + type comFcltActions = { getSwtrmtFcltNm: (searchNm: string) => void; getDemandCompList: (searchNm: string, cnt: number) => void; regDemandCompany: (data: DemandCompType) => void; @@ -25,9 +65,10 @@ export type comFcltActions = { getRateFee: (year: string) => void; }; -export const searchActions: (set: any) => comFcltActions = (set: any) => { +export const comFcltSearchAction: (set: any) => comFcltActions = (set: any) => { return { getSwtrmtFcltNm: async (searchNm: string) => { + const fcltNm = await getFetch( "common", `/prcsFclt/info/1/11/${searchNm}` @@ -40,6 +81,7 @@ export const searchActions: (set: any) => comFcltActions = (set: any) => { addr: item.addr, fctCpc: item.fctCpc, })); + set((state: comFcltState) => ({ ...state, searchFclts: searchFclts, @@ -51,10 +93,13 @@ export const searchActions: (set: any) => comFcltActions = (set: any) => { `/reuseFc/request/${searchNm}/${cnt}` ); - console.log(coList); - set((state: comFcltState) => { - return (state.demandCompList = [...coList]); - }); + + if(coList&&coList.length>0){ + + set((state: comFcltState) => { + return (state.demandCompList = [...coList]); + }); + } }, regDemandCompany: async (data: DemandCompType) => { try { @@ -63,18 +108,20 @@ export const searchActions: (set: any) => comFcltActions = (set: any) => { data, fetchType.WR ); - let message = ""; if (response.status === 200) { - message = "success"; - } else { - message = "fail"; + toast.success('수요처가 등록되었습니다. ') + set((state: comFcltState) => { + + return { + ...state, + dmdIsSucees: "success" + } + }); } - set((state: comFcltState) => { - state.dmdIsSucees = message; - return state; - }); } catch (err) { console.log(err); + toast.error('수요처가 등록 오류입니다.. ') + } }, updateDemandCompany: async (data: DemandCompType) => { @@ -84,16 +131,21 @@ export const searchActions: (set: any) => comFcltActions = (set: any) => { let message = ""; if (response.status === 200) { message = "success"; + toast.success('수요처 정보가 변경되었습니다. ' ) } else { message = "fail"; } set((state: comFcltState) => { - state.dmdIsSucees = message; - return state; + return { + ...state, + dmdIsSucees: message + } }); } catch (err) { console.log(err); + toast.success('수요처 정보가 오류입니다. ' ) + } }, getRateFee: async (year: string) => { @@ -102,7 +154,11 @@ export const searchActions: (set: any) => comFcltActions = (set: any) => { const rateFeeRes = await getFetch(fetchType.COMMON, url); set((state: comFcltState) => { - return state.rateFeeRes = rateFeeRes;; + + return { + ...state, + rateFeeRes: rateFeeRes + } }); } catch (e) { console.log(" error ", e); @@ -112,9 +168,15 @@ export const searchActions: (set: any) => comFcltActions = (set: any) => { const url = `/prcsFclt/CstRuse/${year}`; const constCost = await getFetch(fetchType.COMMON, url); set((state: comFcltState) => { - return state.constCostRes = constCost;; + + return { + ...state, + constCostRes: constCost + } + }); }catch (e) { + console.log('getConstCostFee ', e) } } diff --git a/apps/wr/app/store/common/type/header.tsx b/apps/wr/app/store/common/type/header.tsx index f064b7580..29780ba03 100644 --- a/apps/wr/app/store/common/type/header.tsx +++ b/apps/wr/app/store/common/type/header.tsx @@ -22,6 +22,7 @@ export type TableUpperProps = { selectData ?:string; // type 이 seelct 일경우 selectarray 정하기 위해 row?:number; mergeCell?:boolean // 셀을 합칠경우 + maxLength?:number readonly?:boolean }; diff --git a/apps/wr/app/store/common/type/menu/index.tsx b/apps/wr/app/store/common/type/menu/index.tsx new file mode 100644 index 000000000..46df1cf51 --- /dev/null +++ b/apps/wr/app/store/common/type/menu/index.tsx @@ -0,0 +1,19 @@ +export interface MenuItem { + menuCd: number; + menuKornNm: string; + menuEngNm: string; + parentId: number | null; + sortSeq: number; + iconNm?: string; + children: MenuItem[] | null; + levelNo: number; + urlPath: string; + menuLnkgTypeCd: string; + menuLnkgSn: number; + menuPath: string; + qrcd: string; + useYn: string; + otptYn: string; + newWndwYn: string; + requiredUrlPath: boolean; + } \ No newline at end of file diff --git a/apps/wr/app/store/matDmd/coMatRvw/headList.tsx b/apps/wr/app/store/matDmd/coMatRvw/headList.tsx index 7b43e4b00..16d2107c6 100644 --- a/apps/wr/app/store/matDmd/coMatRvw/headList.tsx +++ b/apps/wr/app/store/matDmd/coMatRvw/headList.tsx @@ -1,4 +1,4 @@ -import type { CommonTableProps, TableUpperProps } from "../../common/type/header"; +import type { CommonTableProps } from "../../common/type/header"; @@ -20,7 +20,7 @@ export const coRvwHeadListExport: CommonTableProps[]= [ }, { id: "daddr", - title: "공장주소", + title: "주소", visible: true, }, { @@ -29,12 +29,12 @@ export const coRvwHeadListExport: CommonTableProps[]= [ visible: true, }, { - id: "regDt", //바꾸기 + id: "dmndTmng", title: "신청일자", visible: true, }, { - id: "state", //바꾸기 + id: "splyFcltFlg", title: "진행상태", visible: true, }, diff --git a/apps/wr/app/store/matDmd/coMatRvw/index.tsx b/apps/wr/app/store/matDmd/coMatRvw/index.tsx index 89836957b..9e857d4d3 100644 --- a/apps/wr/app/store/matDmd/coMatRvw/index.tsx +++ b/apps/wr/app/store/matDmd/coMatRvw/index.tsx @@ -1,13 +1,18 @@ -import { constCostHeadListExport, coRvwHeadListExport, coRvwNodeListExport, rateFeeHeadListExport } from "./headList"; +import { fetchType, getFetchWithCount, putFectch } from "@/components/lib/api"; +import toast from "react-hot-toast"; import type { CommonTableProps } from "../../common/type/header"; +import { + constCostHeadListExport, + coRvwHeadListExport, + coRvwNodeListExport, + rateFeeHeadListExport, +} from "./headList"; import type { DedMatUpdateType, DmdCoReqType } from "./type"; -import { fetchType, getFetch, putFectch } from "@/components/lib/api"; -import toast from "react-hot-toast/headless"; const coRvwHeadList = coRvwHeadListExport; const coRvwNodeList = coRvwNodeListExport; -const rateFeeHeadList =rateFeeHeadListExport; -const constCostHeadList=constCostHeadListExport +const rateFeeHeadList = rateFeeHeadListExport; +const constCostHeadList = constCostHeadListExport; export type coRvwState = { coRvw: { isInit: boolean; @@ -16,6 +21,7 @@ export type coRvwState = { coRvwNodeList: CommonTableProps[]; rateFeeHeadList: CommonTableProps[]; constCostHeadList: CommonTableProps[]; + coMatRvwCount:number; }; }; @@ -25,8 +31,9 @@ export const coRvwStateExport: coRvwState = { coMatRvwRes: [], coRvwHeadList: coRvwHeadList, coRvwNodeList: coRvwNodeList, - rateFeeHeadList:rateFeeHeadList, - constCostHeadList:constCostHeadList + rateFeeHeadList: rateFeeHeadList, + constCostHeadList: constCostHeadList, + coMatRvwCount:1 }, }; @@ -37,7 +44,7 @@ type coRvwActions = { pageNo: number, pageCnt: number, startYear: string, - endYear: string, + endYear: string ) => void; coMatRvwUpdate: ( splyCd: string, @@ -50,7 +57,7 @@ type coRvwActions = { startYear: string, endYear: string, sggCd: string, - dmdCoNm: string, + dmdCoNm: string ) => void; coMatRvwInqua: ( pageNo: number, @@ -65,10 +72,10 @@ const coMatInit = async ( pageNo: number, pageCnt: number, startYear: string, - endYear: string, + endYear: string ) => { - const url = `/reuseFc/request/infoList/sub/${pageNo}/${pageCnt}/${startYear}/${endYear}`; - const coMatRvwRes = await getFetch(fetchType.WR, url); + const url = `/reuseFc/request/infoList/Sub/${pageNo}/${pageCnt}/${startYear}/${endYear}`; + const coMatRvwRes = await getFetchWithCount(fetchType.WR, url); return coMatRvwRes; }; @@ -79,7 +86,7 @@ export const coRvwActionsExport: (set: any) => coRvwActions = (set: any) => { pageNo: number, pageCnt: number, startYear: string, - endYear: string, + endYear: string ) => { const coMatRvwRes = await coMatInit(pageNo, pageCnt, startYear, endYear); @@ -88,7 +95,8 @@ export const coRvwActionsExport: (set: any) => coRvwActions = (set: any) => { coRvw: { ...state.coRvw, init: false, - coMatRvwRes: coMatRvwRes, + coMatRvwRes: coMatRvwRes.data, + coMatRvwCount:coMatRvwRes.count }, })); }, @@ -98,27 +106,40 @@ export const coRvwActionsExport: (set: any) => coRvwActions = (set: any) => { data: DedMatUpdateType ) => { try { - const isUpdate = await putFectch( + const response = await putFectch( `/reuseFc/request/cst/${splyCd}/${dmdCd}`, data ); + if (response.status === 200) { - const coMatRvwRes = coMatInit( - 1, - 10, - new Date().getFullYear().toString(), - new Date().getFullYear().toString(), - ); - return set((state: coRvwState) => ({ - ...state, - coRvw: { - ...state.coRvw, - init: false, - coMatRvwRes: coMatRvwRes, - }, - })); + if(data.flg ==='Sub'){ + + toast.success("매칭 검토로 저장되었습니다."); + }else{ + toast.success("매칭 검토완료로 저장되었습니다."); + } + + const coMatRvwRes = await coMatInit( + 1, + 10, + new Date().getFullYear().toString(), + new Date().getFullYear().toString() + ); + return set((state: coRvwState) => ({ + ...state, + coRvw: { + ...state.coRvw, + coMatRvwRes: coMatRvwRes.data, + coMatRvwCount:coMatRvwRes.count, + init: false, + }, + })); + } else { + toast.error("매칭 검토 저장에 실패했습니다."); + } } catch (e) { toast.error("매칭 검토 저장 오류입니다."); + console.error("coMatRvwUpdate error", e); } }, coMatRvwSearch: async ( @@ -127,9 +148,9 @@ export const coRvwActionsExport: (set: any) => coRvwActions = (set: any) => { startYear: string, endYear: string, sggCd: string, - dmdCoNm: string, + dmdCoNm: string ) => { - let url = `/reuseFc/request/infoList/sub/${pageNo}/${pageCnt}/${startYear}/${endYear}`; + let url = `/reuseFc/request/infoList/Sub/${pageNo}/${pageCnt}/${startYear}/${endYear}`; if (sggCd.length > 0) { url += `/${sggCd}`; @@ -142,14 +163,15 @@ export const coRvwActionsExport: (set: any) => coRvwActions = (set: any) => { url += `/0`; } - console.log(' url ', url) - const coMatRvwRes = await getFetch(fetchType.WR, url); + console.log(" url ", url); + const coMatRvwRes = await getFetchWithCount(fetchType.WR, url); return set((state: coRvwState) => ({ ...state, coRvw: { ...state.coRvw, - coMatRvwRes: coMatRvwRes, + coMatRvwRes: coMatRvwRes.data, + coMatRvwCount:coMatRvwRes.count, }, })); }, @@ -161,23 +183,24 @@ export const coRvwActionsExport: (set: any) => coRvwActions = (set: any) => { sggCd: string ) => { try { - let url = `/reuseFc/request/infoList/sub/${pageNo}/${pageCnt}/${startYear}/${endYear}`; + let url = `/reuseFc/request/infoList/Sub/${pageNo}/${pageCnt}/${startYear}/${endYear}`; if (sggCd.length > 0) { url += `/${sggCd}/0`; - }else if(sggCd ==='0'){ + } else if (sggCd === "0") { url += `/0/0`; } - const coMatRvwRes = await getFetch(fetchType.WR, url); + const coMatRvwRes = await getFetchWithCount(fetchType.WR, url); return set((state: coRvwState) => ({ ...state, coRvw: { ...state.coRvw, - coMatRvwRes: coMatRvwRes, + coMatRvwRes: coMatRvwRes.data, + coMatRvwCount:coMatRvwRes.count, }, })); } catch (e) { console.log(" error ", e); } - } + }, }; }; diff --git a/apps/wr/app/store/matDmd/coMatRvw/type.tsx b/apps/wr/app/store/matDmd/coMatRvw/type.tsx index 9f146c0fb..34990c6b3 100644 --- a/apps/wr/app/store/matDmd/coMatRvw/type.tsx +++ b/apps/wr/app/store/matDmd/coMatRvw/type.tsx @@ -1,173 +1,162 @@ +import type { ChkClass } from "@common/business_components/ui"; import { z } from "zod"; - const DemandInfoSchema = z.object({ - dmdInfoSn: z.number(), - dmdInfoFk: z.string(), - dmndTmng: z.coerce.date(), - dmndQty: z.number(), - wntUntprc: z.number(), - grssClfg: z.number(), - combResdch: z.number(), - trbt: z.number(), - blgcOxygDmndQnt: z.number(), - smll: z.number(), - crmty: z.number().nullish(), - tn: z.number(), - tph: z.number(), - ph: z.number(), - clIonQnt: z.number(), - cdtnv: z.number(), - rmrk: z.string(), - useUsgCd: z.string(), - mtchnCmptnYmd: z.string().nullish(), - rvwCmptnYmd: z.string().nullish() - }); - - const DemandMatchingSchema = z.object({ - dmdInfoFk: z.string().nullish(), - splyFcltFk: z.string().nullish(), - splyFcltFlg: z.string().nullish(), - pbnstRuseFcltBizFk: z.number().nullish(), - splyBgngDay: z.coerce.date().nullish(), - splyEndDay: z.coerce.date().nullish(), - splyCpct: z.number().nullish(), - splyFmYn: z.string().nullish(), - useUsgCd: z.string().nullish(), - dmdInfoSn: z.number().nullish(), - splyCndtList:z.array(z.string().nullish() ).nullish() - }); - - const DemandCoSchema = z.object({ - dmdInfoPk: z.string(), - dmdCoNm: z.string(), - ctpvCd: z.string(), - sggCd: z.string(), - daddr: z.string(), - splyBgngDay: z.coerce.date(), - splyEndDay: z.coerce.date(), - splyCpct: z.number(), - splyFmYn: z.string().nullish(), - telno: z.string(), - useUsgCd: z.string().nullish(), - regDt: z.coerce.date().nullish(), - rgtr: z.string().nullish(), - mdfcnDt: z.coerce.date().nullish(), - mdfr: z.string().nullish(), - dmdAplyPswd: z.string(), - demandInfo: z.array(DemandInfoSchema.nullish()).nullish(), // 안쓰는 거임 - demandMatchings: z.array(DemandMatchingSchema.nullish()).nullish()// 안쓰는 거임 - }); - - const DemandCstSchema = z.object({ - dmdInfoSn: z.number(), - usgPrcsProcs: z.string().nullish(), - cstrnCst: z.number(), - fcltSbsdCstrnCst: z.number(), - prcsFcltCpctCstrnCst: z.number() - }); - - export const DmdCoReqSchema = z.object({ - cityText:z.string().nullish(), - sggText:z.string().nullish(), - dmdInfoSn: z.number(), - dmdInfoFk: z.string(), - dmndTmng: z.coerce.date(), - dmndQty: z.number(), - wntUntprc: z.number().nullish(), - grssClfg: z.number().nullish(), - combResdch: z.number().nullish(), - trbt: z.number().nullish(), - blgcOxygDmndQnt: z.number(), - smll: z.number().nullish(), - crmty: z.number().nullish(), - tn: z.number().nullish(), - tph: z.number().nullish(), - ph: z.number().nullish(), - clIonQnt: z.number().nullish(), - cdtnv: z.number(), - rmrk: z.string().nullish(), - useUsgCd: z.string().nullish(), - mtchnCmptnYmd: z.string().nullish(), - rvwCmptnYmd: z.string().nullish(), - demandCo: DemandCoSchema.nullish(), - demandMatching: DemandMatchingSchema.nullish(), - demandCst: DemandCstSchema, - ruseUtztnFcltNm:z.string().nullish(), - totalConstCost:z.number().nullish(), +const DemandInfoSchema = z.object({ + dmdInfoSn: z.number(), + dmdInfoId: z.string().nullish(), + dmndTmng: z.coerce.date().nullish(), + dmndQty: z.number().nullish(), + wntUntprc: z.number().nullish(), + grssClfg: z.number().nullish(), + combResdch: z.number().nullish(), + trbt: z.number().nullish(), + blgcOxygDmndQnt: z.number(), + smll: z.number().nullish(), + crmty: z.number().nullish(), + tn: z.number().nullish(), + tph: z.number().nullish(), + ph: z.number(), + clIonQnt: z.number().nullish(), + cdtnv: z.number().nullish(), + rmrk: z.string().nullish(), + useUsgCd: z.string().nullish(), +}); - }); +const DemandMatchingSchema = z.object({ + dmdInfoId: z.string().nullish(), + pbnstRuseFcltId: z.string().nullish(), + splyFcltFlg: z.string().nullish(), + pbnstRuseFcltBizSn: z.number().nullish(), + splyBgngDay: z.coerce.date().nullish(), + splyEndDay: z.coerce.date().nullish(), + splyCpct: z.number().nullish(), + splyFmYn: z.string().nullish(), + useUsgCd: z.string().nullish(), + dmdInfoSn: z.number().nullish(), + splyCndtList: z.array(z.string().nullish()).nullish(), + mtchnCmptnYmd: z.string().nullish(), + rvwCmptnYmd: z.string().nullish(), +}); +const DemandCoSchema = z.object({ + dmdInfoId: z.string().nullish(), + dmdCoNm: z.string().nullish(), + ctpvCd: z.string().nullish(), + sggCd: z.string().nullish(), + daddr: z.string().nullish(), + splyBgngDay: z.coerce.date().nullish(), + splyEndDay: z.coerce.date().nullish(), + splyCpct: z.number().nullish(), + splyFmYn: z.string().nullish(), + telno: z.string().nullish(), + useUsgCd: z.string().nullish(), + regDt: z.coerce.date().nullish(), + rgtr: z.string().nullish(), + mdfcnDt: z.coerce.date().nullish(), + mdfr: z.string().nullish(), + dmdAplyPswd: z.string().nullish(), + demandInfo: z.array(DemandInfoSchema.nullish()).nullish(), // 안쓰는 거임 + demandMatchings: z.array(DemandMatchingSchema.nullish()).nullish(), // 안쓰는 거임 +}); + +const DemandCstSchema = z.object({ + dmdInfoSn: z.number().nullish(), + usgPrcsProcs: z.string().nullish(), + cstrnCst: z.number().default(0), + fcltSbsdCstrnCst: z.number().default(0), + prcsFcltCpctCstrnCst: z.number().default(0), +}); + +export const DmdCoReqSchema = z.object({ + cityText: z.string().nullish(), + sggText: z.string().nullish(), + dmdInfoSn: z.number().default(0), + dmdInfoId: z.string().default(''), + dmndTmng: z.coerce.date().nullish(), + dmndQty: z.number().nullish(), // null 또는 undefined 허용 + blgcOxygDmndQnt: z.number().nullish(), + wntUntprc: z.number().nullish(), + grssClfg: z.number().nullish(), + combResdch: z.number().nullish(), + trbt: z.number().nullish(), + smll: z.number().nullish(), + crmty: z.number().nullish(), + tn: z.number().nullish(), + tph: z.number().nullish(), + ph: z.number().nullish(), + clIonQnt: z.number().nullish(), + cdtnv: z.number().nullish(), + rmrk: z.string().nullish(), + useUsgCd: z.string().nullish(), + mtchnCmptnYmd: z.string().nullish(), + rvwCmptnYmd: z.string().nullish(), + demandCo: DemandCoSchema.nullish(), + demandMatching: DemandMatchingSchema.nullish(), + demandCst: DemandCstSchema.nullish(), + ruseUtztnFcltNm: z.string().nullish(), + splyCndtList: z.array( + z.object({ + ruseUtztnFcltNm: z.string(), + ruseUtztnFcltCd: z.string(), + }) + ).nullish(), + totalConstCost: z.number().nullish(), +}); export type DmdCoReqType = z.infer<typeof DmdCoReqSchema>; export type DmdMatTableType = { - cityText:string, - sggText:string, - dmdCoNm:string, - daddr:string, - telno:string, - regDt:string, - splyFcltFlg:string -} + cityText: string; + sggText: string; + dmdCoNm: string; + daddr: string; + telno: string; + regDt: string; + splyFcltFlg: string; +}; - -export type DedMatUpdateType = -{ +export type DedMatUpdateType = { dmdInfoSn: number; usgPrcsProcs: string; cstrnCst: number; fcltSbsdCstrnCst: number; prcsFcltCpctCstrnCst: number; rgtr: string; -} - + flg?:string; +}; export type RvwHistTableType = { - cityText:string, - sggText:string, - dmdCoNm:string, - daddr:string, - telno:string, - regDt:string, - rvwCmptnYmd:string, - mtchnCmptnYmd:string, - ruseUtztnFcltNm:string, - dmndQty:string, - useUsgCd:string, - splyFcltFlg:string - completeYn:boolean - dmdInfoSn :string + cityText: string; + sggText: string; + dmdCoNm: string; + daddr: string; + telno: string; + regDt: string; + rvwCmptnYmd: string; + mtchnCmptnYmd: string; + ruseUtztnFcltNm: string; + dmndQty: string; + useUsgCd: string; + splyFcltFlg: string; + completeYn: ChkClass; + dmdInfoSn: string; +}; -} - -export type RvwHistTableType = { - cityText:string, - sggText:string, - dmdCoNm:string, - daddr:string, - telno:string, - regDt:string, - rvwCmptnYmd:string, - mtchnCmptnYmd:string, - ruseUtztnFcltNm:string, - dmndQty:string, - useUsgCd:string, - splyFcltFlg:string - completeYn:boolean - dmdInfoSn :string - -} - - - -export type conCostFeeType= { - yr: string, - fctCpc: string, - fctCd: string, - stdCost: string -} +export type conCostFeeType = { + yr: string; + fctCpc: string; + fctCd: string; + stdCost: string; +}; export type rateFeeType = { - yr: string, - incExpCd: string, - stdCost: number, - rt: number -} \ No newline at end of file + yr: string; + incExpCd: string; + stdCost: number; + rt: number; +}; + + +// 공급 후보자 타입 정의 +export type SupplyCandidate = { + ruseUtztnFcltNm: string; + ruseUtztnFcltCd: string; +}; \ No newline at end of file diff --git a/apps/wr/app/store/matDmd/coSttn/excel.tsx b/apps/wr/app/store/matDmd/coSttn/excel.tsx index 39d669664..84c982292 100644 --- a/apps/wr/app/store/matDmd/coSttn/excel.tsx +++ b/apps/wr/app/store/matDmd/coSttn/excel.tsx @@ -1,16 +1,17 @@ -import type { customCoSttnBdst } from "."; +import type { SelectDataType } from "@common/business_components/ui"; +import moment from "moment"; -export const excelCoSttnParsing = (data: customCoSttnBdst[]): string[][] => { +export const excelCoSttnParsing = (data: any[],sidoCode:SelectDataType[] , sggCode:SelectDataType[]): string[][] => { const excelExportData: string[][] = []; - data.forEach((item) => { excelExportData.push([ - String(item.ctpvCd), - String(item.sggCd), + sidoCode.find((sido) => sido.val === item.ctpvCd.toString().trim() )?.text ?? "", + sggCode.find((sgg) => sgg.val === item.sggCd.toString().trim() )?.text ?? "", item.dmdCoNm ?? "", item.daddr ?? "", - String(item.dmdInfoPk ?? ""), - item.telno ?? "" + String(item.dmdInfoId ?? ""), + item.telno ?? "", + moment(item.regDt).format("YYYY-MM-DD") ?? "" ]); }); return excelExportData; diff --git a/apps/wr/app/store/matDmd/coSttn/headList.tsx b/apps/wr/app/store/matDmd/coSttn/headList.tsx index 80fed0d76..b3b87aac8 100644 --- a/apps/wr/app/store/matDmd/coSttn/headList.tsx +++ b/apps/wr/app/store/matDmd/coSttn/headList.tsx @@ -35,7 +35,7 @@ export const coSttHeadListExport: z.infer<typeof sttnHeadListType> = [ visible: true, }, { - id: "dmdInfoPk", + id: "dmdInfoId", title: "사업자번호", visible: true, }, @@ -44,6 +44,11 @@ export const coSttHeadListExport: z.infer<typeof sttnHeadListType> = [ title: "전화번호", visible: true, }, + { + id: "regDt", + title: "등록일자", + visible: true, + }, ]; export const coSttNodeListExport: CommonTableProps[] = [ @@ -80,7 +85,7 @@ export const coSttNodeListExport: CommonTableProps[] = [ colSpan: 1, }, { - id: "dmdInfoPk", + id: "dmdInfoId", title: "사업자번호", visible: true, row: 1, diff --git a/apps/wr/app/store/matDmd/coSttn/index.tsx b/apps/wr/app/store/matDmd/coSttn/index.tsx index 0de37bbfc..cc1db4566 100644 --- a/apps/wr/app/store/matDmd/coSttn/index.tsx +++ b/apps/wr/app/store/matDmd/coSttn/index.tsx @@ -1,8 +1,7 @@ import type { CommonTableProps } from "@/app/store/common/type/header"; -import { fetchType, getFetch, getFetchWithCount } from "@/components/lib/api"; +import { fetchType, getFetchWithCount } from "@/components/lib/api"; +import type { DemandCompType } from "@common/business_components/ui/sdmInsertForm/form"; import { coSttHeadListExport, coSttNodeListExport } from "./headList"; -import { excelCoSttnParsing } from "./excel"; -import type { DemandCompType } from "@common/business_components/ui"; // 테이블 헤드 리스트 const coSttNodeList = coSttNodeListExport; @@ -53,6 +52,8 @@ export const coSttnActionsExport: (set: any) => coSttnActions = ( `/reuseFc/request/${pageNo}/${pageCnt}/${con}` ); + console.log('coSttnRes ', coSttnRes) + set((state: coSttnState) => { return { ...state, @@ -75,14 +76,13 @@ export const coSttnActionsExport: (set: any) => coSttnActions = ( `/reuseFc/request/${pageNo}/${pageCnt}/${con}` ); if (Number(pageCnt) >= 10000) { - const coExcel = excelCoSttnParsing(coSttnRes.data); set((state: coSttnState) => { return { ...state, coSttn: { ...state.coSttn, - excelList: coExcel, - isInit: false, + excelList: coSttnRes.data, + //isInit: false, }, }; }); @@ -104,7 +104,7 @@ export const coSttnActionsExport: (set: any) => coSttnActions = ( }, coSttnTxtSearch: async (pageNo:string, pageCnt:string, con: string, txt: string) => { try { - const coSttnRes = await getFetch( + const coSttnRes = await getFetchWithCount( fetchType.WR, `/reuseFc/request/${pageNo}/${pageCnt}/${con}/${txt}`) set((state: coSttnState) => { @@ -112,7 +112,8 @@ export const coSttnActionsExport: (set: any) => coSttnActions = ( ...state, coSttn: { ...state.coSttn, - coSttnRes: coSttnRes, + coSttnRes: coSttnRes.data, + coSttnResCount: coSttnRes.count, isInit: false, }}}) } catch (erTxt) { diff --git a/apps/wr/app/store/matDmd/drgtSttn/headList.tsx b/apps/wr/app/store/matDmd/drgtSttn/headList.tsx index 40eae47e3..dcdc655b5 100644 --- a/apps/wr/app/store/matDmd/drgtSttn/headList.tsx +++ b/apps/wr/app/store/matDmd/drgtSttn/headList.tsx @@ -1,5 +1,19 @@ +import { z } from "zod"; import type { CommonTableProps } from "../../common/type/header"; +export const drgtZod = z.object({ + ctpvCd: z.string().default('').nullish(), + sggNm: z.string().default('').nullish(), + normal: z.string().default(''), + attention: z.string().default(''), + caution: z.string().default(''), + warning: z.string().default(''), + ranking: z.string().default(''), + year: z.string().default(''), + month: z.string().default(''), + drgtState: z.string().default('') +}) + export const drgtHeadListExport: CommonTableProps[] = [ { id: "ctpvCd", @@ -10,21 +24,13 @@ export const drgtHeadListExport: CommonTableProps[] = [ colSpan: 1, }, { - id: "sggCd", + id: "sggNm", title: "시군구", visible: true, row: 1, rowSpan: 1, colSpan: 1, }, - { - id: "haengjung", - title: "행정동", - visible: true, - row: 1, - rowSpan: 1, - colSpan: 1, - }, { id: "normal", title: "정상(횟수)", @@ -83,16 +89,9 @@ export const drgtDetailHeadListExport: CommonTableProps[] = [ row: 1, rowSpan: 1, colSpan: 1, - },{ - id: "day", - title: "일", - visible: true, - row: 1, - rowSpan: 1, - colSpan: 1, }, { - id: "state", + id: "drgtState", title: "가뭄상태", visible: true, row: 1, diff --git a/apps/wr/app/store/matDmd/drgtSttn/index.tsx b/apps/wr/app/store/matDmd/drgtSttn/index.tsx index edbbc2f28..f6257ebaf 100644 --- a/apps/wr/app/store/matDmd/drgtSttn/index.tsx +++ b/apps/wr/app/store/matDmd/drgtSttn/index.tsx @@ -1,27 +1,170 @@ +import { fetchType, getFetch, postFectch } from "@/components/lib/api"; +import type { z } from "zod"; import type { CommonTableProps } from "../../common/type/header"; -import { drgtHeadListExport } from "./headList"; +import { drgtHeadListExport, drgtZod } from "./headList"; const drgtHeadList = drgtHeadListExport; +export type customDrgt = z.infer<typeof drgtZod>; + +const drgtRes: customDrgt[] = []; + export type drgtState = { - drgt:{ - drgtHeadList: CommonTableProps[]; - } + drgt: { + drgtHeadList: CommonTableProps[]; + drgtRes: customDrgt[]; + drgtMoRes: customDrgt[]; + isInit: boolean; + } } type drgtActions = { + drgtInit: () => void + drgtSearch: (fromDate: string, toDate: string, sido: string[]) => void + drgtModalSearch: (fromDate: string, toDate: string, sgg?: string) => void }; export type drgtType = drgtState & drgtActions; export const drgtStateExport: drgtState = { - drgt: { - drgtHeadList: drgtHeadList, - } + drgt: { + drgtHeadList: drgtHeadList, + drgtRes: drgtRes, + drgtMoRes: drgtRes, + isInit: false, + } } +const initFetch = async (fromDate: string, toDate: string, sido: string[]) => { + const drgtData = await postFectch( + `/il/getDataCount/${fromDate}/${toDate}`, + sido, + fetchType.WR + ); + + //주의, 관심, 정상 추출 + const statusData = Array.from( + new Set(drgtData.data.map((e) => e.lvngMfgUswtrDrgtInfo)) + ); + + const statusMap = drgtData.data.reduce((acc: any, { sggNm, lvngMfgUswtrDrgtInfo: status, countDrgt }: { sggNm: string; lvngMfgUswtrDrgtInfo: string; countDrgt: number }) => { + if (!acc[sggNm]) { + acc[sggNm] = { sggNm, ...Object.fromEntries(statusData.map((e) => [e, 0])) }; + } + acc[sggNm][status] = Number(countDrgt); + return acc; + }, {}); + const data = Object.values(statusMap); + + const formatStatus = data.map((item: any) => { + const attention = item['관심'] || 0; + const caution = item['주의'] || 0; + return { + sggNm: item['sggNm'], + normal: String(item['정상'] || 0), + caution: String(caution), + attention: String(attention), + warning: String(attention + caution), + }; + }); + + const ranking = Array.from( + new Set(formatStatus.map(e => Number(e.warning))) + ).sort((a, b) => b - a); + + const rankData = ranking.reduce((acc, val, idx) => { + acc[val] = idx + 1; + return acc; + }, {}); + + const drgtRes = formatStatus.map(item => ({ + ...item, + warning: String(item.warning), + ranking: rankData[Number(item.warning)], + })); + + return drgtRes; + +} + + export const drgtActionsExport: (set: any) => drgtActions = ( set: any ) => { - return {}; -}; \ No newline at end of file + return { + drgtInit: async () => { + try { + const today = new Date(); + const year = today.getFullYear(); + const month = String(today.getMonth() + 1).padStart(2, '0'); + const formatted = `${year}${month}`; + let fromDate = formatted; + let toDate = formatted; + fromDate='202506' + toDate='202506' + const drgtRes = await initFetch(fromDate, toDate, []); + set((state: drgtState) => { + return { + ...state, + drgt: { + ...state.drgt, + drgtRes: drgtRes, + isInit: false, + }, + } + }); + } catch (e) { + console.log(e) + } + }, + drgtSearch: async (fromDate: string, toDate: string, sido: string[]) => { + try { + const drgtRes = await initFetch(fromDate, toDate, sido); + + set((state: drgtState) => { + return { + ...state, + drgt: { + ...state.drgt, + drgtRes: drgtRes, + }, + } + }); + } catch (e) { + console.log(e) + } + }, + drgtModalSearch: async (fromDate: string, toDate: string, sgg?: string) => { + try { + const drgtMoData = await getFetch( + fetchType.WR, + `/il/getData/${fromDate}/${toDate}/${sgg}/${sgg}` + ); + + const drgtMoRes = drgtMoData.map((item) => { + const year = item.anlsYmd.slice(0, 4); + const month = item.anlsYmd.slice(4, 6); + const drgtState = item.lvngMfgUswtrDrgtInfo; + const sggNm = item.sggNm; + return { + year: year, + month: month, + drgtState: drgtState, + sggNm: sggNm + } + }); + set((state: drgtState) => { + return { + ...state, + drgt: { + ...state.drgt, + drgtMoRes: drgtMoRes, + }, + } + }); + } catch (e) { + console.log(e) + } + }, + } +} \ No newline at end of file diff --git a/apps/wr/app/store/matDmd/index.tsx b/apps/wr/app/store/matDmd/index.tsx index ed8ecd919..cc7094bc7 100644 --- a/apps/wr/app/store/matDmd/index.tsx +++ b/apps/wr/app/store/matDmd/index.tsx @@ -1,55 +1,53 @@ import type { SelectDataType } from "@common/business_components/ui"; - - const selectSido = [ - { text: '서울', val: "1" }, - { text: "인천", val: "2" }, - { text: "부산", val: "3" }, - ]; + { text: "서울", val: "1" }, + { text: "인천", val: "2" }, + { text: "부산", val: "3" }, +]; const selectSgg = [ - { text: '종로구', val: "1" }, - { text: "성북구", val: "2" }, - { text: "강남구", val: "3" }, - ]; - const selectUsg = [ - { text: '민간', val: "1" }, - { text: "공공", val: "2" }, - ]; - const selectYN= [ - { text: 'Y', val: "1" }, - { text: 'N', val: "2" }, - ]; + { text: "종로구", val: "1" }, + { text: "성북구", val: "2" }, + { text: "강남구", val: "3" }, +]; +const selectUsg = [ + { text: "민간", val: "1" }, + { text: "공공", val: "2" }, +]; +const selectYN = [ + { text: "Y", val: "1" }, + { text: "N", val: "2" }, +]; -export type matDmdState ={ dmd:{ +export type matDmdState = { + dmd: { isInit: boolean; - selectSido:SelectDataType[]; - selectSgg:SelectDataType[]; - selectUsg:SelectDataType[]; - selectYN:SelectDataType[]; -}} + selectSido: SelectDataType[]; + selectSgg: SelectDataType[]; + selectUsg: SelectDataType[]; + selectYN: SelectDataType[]; + }; +}; type matDmdActions = { - decrementList: () => void -} + decrementList: () => void; +}; export type matDmdType = matDmdState & matDmdActions; -export const matDmdStateExport:matDmdState= {dmd:{ - isInit:false, - selectSido:selectSido, +export const matDmdStateExport: matDmdState = { + dmd: { + isInit: false, + selectSido: selectSido, selectSgg: selectSgg, selectUsg: selectUsg, - selectYN:selectYN, - - -}} -export const matDmdActionsExport:(set:any)=>matDmdActions=(set: any) => { - return { - decrementList:()=>set( - (state:matDmdState) => { - return ( - {rain:{ - }})} - ) - } -} + selectYN: selectYN, + }, +}; +export const matDmdActionsExport: (set: any) => matDmdActions = (set: any) => { + return { + decrementList: () => + set(() => { + return { rain: {} }; + }), + }; +}; diff --git a/apps/wr/app/store/matDmd/ldrmntCoSttn/excel.tsx b/apps/wr/app/store/matDmd/ldrmntCoSttn/excel.tsx index dc0601127..55af81643 100644 --- a/apps/wr/app/store/matDmd/ldrmntCoSttn/excel.tsx +++ b/apps/wr/app/store/matDmd/ldrmntCoSttn/excel.tsx @@ -1,4 +1,3 @@ -import { TEST_UUID } from "../../ruseFcltMng/fcltReg/rainReg"; type nameMap = { [key: string]: string; @@ -46,29 +45,17 @@ export const names: nameMap = { } -export const excelPbnstSttnParsing = (data: any[]): string[][] => { +export const excelLdrmntParsing = (data: any[]): string[][] => { const excelExportData: string[][] = []; - data.forEach((item) => { excelExportData.push([ - item.rvbsn, - String(item.lgbsn), - item.mdmbsn ?? "", - item.rgnNm ?? "", - String(item.sido ?? ""), - String(item.gugun ?? ""), - String(item.bscFctNm ?? ""), - String(item.efflWqltStndrdNm ?? ""), - String(item.fctCpc ?? ""), - String(item.tel ?? ""), - String(item.ruseUtztnFcltNm ?? ""), - String(item.fcltCpct ?? ""), - String(item.splyExtraCpct ?? ""), - String(item.ruseCpct ?? ""), - String(item.usageTotal ?? ""), - String(item.inUsage ?? ""), - String(item.outUsage ?? ""), + item.fctryAddr?.split(" ")[0] || "", + item.fctryAddr?.split(" ")[1] || "", + item.dmdCoNm, + item.fctryAddr, + item.fctryMngNo, + item.telno, ]) }) return excelExportData; diff --git a/apps/wr/app/store/matDmd/ldrmntCoSttn/index.tsx b/apps/wr/app/store/matDmd/ldrmntCoSttn/index.tsx index d1176528a..fed013417 100644 --- a/apps/wr/app/store/matDmd/ldrmntCoSttn/index.tsx +++ b/apps/wr/app/store/matDmd/ldrmntCoSttn/index.tsx @@ -1,11 +1,9 @@ import type { CommonTableProps } from "@/app/store/common/type/header"; -import { deleteFectch, fetchType, getFetch, postFectch, putFectch } from "@/components/lib/api"; -import { ToastFormUi } from "@/components/ui/toast/toastFromUi"; +import { deleteFectch, fetchType, getFetchWithCount, postFectch, putFectch } from "@/components/lib/api"; +import toast from "react-hot-toast"; import { z } from "zod"; -import { ldrmntHeadListExport, ldrmntNodeListExport, ldrmntZod } from "./headList"; import { TEST_UUID } from "../../ruseFcltMng/fcltReg/rainReg"; - - +import { ldrmntHeadListExport, ldrmntNodeListExport, ldrmntZod } from "./headList"; // 테이블 헤드 리스트 const ldrmntHeadList = ldrmntHeadListExport; @@ -22,6 +20,8 @@ export type ldrmntState = { excelList: ExcelType[]; isInit: boolean; ldrmntRes: customLdrmnt[]; + isLoading: boolean; + ldrmntResCount: number; }; }; @@ -29,14 +29,15 @@ type ExcelType = { [key: string]: string | number | undefined; }; -const excelDownList: ExcelType[] = []; +// const excelDownList: ExcelType[] = []; type ldrmntActions = { ldrmntRegData: (data: any[]) => void, - ldrmntInit: (con: string) => void - ldrmntSearch: (page: number, sggCd: string, coNm: string) => void; + ldrmntInit: (isExcel: boolean, con: string) => void + ldrmntSearch: (isExcel: boolean, page: number, sggCd: string, total: string, coNm: string) => void; ldrmntUpdate: (ldrmntRes: customLdrmnt) => void; ldrmntDelete: () => void; + loadingOpen: (isOpen: boolean) => void }; export type ldrmntType = ldrmntState & ldrmntActions; @@ -48,6 +49,8 @@ export const ldrmntStateExport: ldrmntState = { excelList: [], isInit: false, ldrmntRes: ldrmntRes, + isLoading: false, + ldrmntResCount: 1, }, }; @@ -55,121 +58,172 @@ export const ldrmntActionsExport: (set: any) => ldrmntActions = ( set: any ) => { return { - ldrmntInit: async (con: string) => { + ldrmntInit: async (isExcel: boolean,con: string) => { try { - const ldrmntRes = await getFetch( + const ldrmntRes = await getFetchWithCount( fetchType.WR, `/reuseFc/request/temp/1/20/${con}` ); - set((state: ldrmntState) => { - return { - ...state, - ldrmnt: { - ...state.ldrmnt, - ldrmntRes: ldrmntRes, - isInit: false, - }, - } - }); - } catch (e) { - console.log(e) - } - }, - ldrmntRegData: async (data) => { - try { - const response = await postFectch( - "/reuseFc/request/temp", - data, - fetchType.WR - ); - - console.log('response ', response) - - if (response.status === 200) { - ToastFormUi({ - body: "잠재수요처 등록이 완료되었습니다. ", - title: "잠재수요처 등록", + if (isExcel) { + const excelDownList: ExcelType[] = []; + ldrmntRes.data.forEach((item: customLdrmnt) => { + excelDownList.push({ + ctpvCd: item.ctpvCd, + sggCd: item.sggCd, + dmdCoNm: item.dmdCoNm, + fctryAddr: item.fctryAddr, + fctryMngNo: item.fctryMngNo, + telno: item.telno, + }) + }) + set((state: ldrmntState) => { + return { + ...state, + ldrmnt: { + ...state.ldrmnt, + excelList: excelDownList, + ldrmntResCount: ldrmntRes.count, + }, + }; + }); + } else { + set((state: ldrmntState) => { + return { + ...state, + ldrmnt: { + ...state.ldrmnt, + ldrmntRes: ldrmntRes.data, + ldrmntResCount: ldrmntRes.count, + isInit: false, + }, + } }); - } } catch (e) { console.log(e) - ToastFormUi({ - body: "잠재수요처 등록이 실패하였습니다. ", - title: "잠재수요처 등록", - }); - } }, - ldrmntSearch: async (page: number, sggCd: string, coNm: string) => { + loadingOpen: (isOpen) => { + return set((state: ldrmntState) => { + return { + ...state, + ldrmnt: { + ...state.ldrmnt, + isLoading: isOpen, + }, + }; + }); + }, + ldrmntRegData: async (data) => { + await deleteFectch( + `/reuseFc/request/temp`, + ); + console.log("deleteFectch", deleteFectch) + + const response = await postFectch( + "/reuseFc/request/temp", + data, + fetchType.WR + ); + + console.log('response ', response) + + }, + ldrmntSearch: async (isExcel: boolean, page: number, total: string, sggCd: string, coNm: string) => { let searchUrl = ""; if (sggCd !== "") { if (coNm !== "") { - searchUrl = `/reuseFc/request/temp/${page}/20/${sggCd}/${coNm}`; + searchUrl = `/reuseFc/request/temp/${page}/${total}/${sggCd}/${coNm}`; } else { - searchUrl = `/reuseFc/request/temp/${page}/20/${sggCd}`; + searchUrl = `/reuseFc/request/temp/${page}/${total}/${sggCd}`; } } - try { - const ldrmntRes = await getFetch( + const ldrmntRes = await getFetchWithCount( fetchType.WR, searchUrl ); - - ldrmntRes.forEach((item: customLdrmnt) => { - excelDownList.push({ - ctpvCd: item.ctpvCd, - sggCd: item.sggCd, - dmdCoNm: item.dmdCoNm, - fctryAddr: item.fctryAddr, - fctryMngNo: item.fctryMngNo, - telno: item.telno, + if (isExcel) { + const excelDownList: ExcelType[] = []; + ldrmntRes.data.forEach((item: customLdrmnt) => { + excelDownList.push({ + ctpvCd: item.ctpvCd, + sggCd: item.sggCd, + dmdCoNm: item.dmdCoNm, + fctryAddr: item.fctryAddr, + fctryMngNo: item.fctryMngNo, + telno: item.telno, + }) }) - }) - - set((state: ldrmntState) => { - return { - ...state, - ldrmnt: { - ...state.ldrmnt, - ldrmntRes: ldrmntRes, - excelList: excelDownList, - }, - } - }) + set((state: ldrmntState) => { + return { + ...state, + ldrmnt: { + ...state.ldrmnt, + excelList: excelDownList, + ldrmntResCount: ldrmntRes.count, + }, + }; + }); + } else + set((state: ldrmntState) => { + return { + ...state, + ldrmnt: { + ...state.ldrmnt, + ldrmntRes: ldrmntRes.data, + ldrmntResCount: ldrmntRes.count, + isInit: false, + }, + }; + }); } catch (e) { console.log(e) } }, ldrmntUpdate: async (ldrmntRes: customLdrmnt) => { - try { - ldrmntRes.rgtr = TEST_UUID; - const response = await putFectch( - `/reuseFc/request/temp`, - ldrmntRes - ); + if (ldrmntRes) + try { + ldrmntRes.rgtr = TEST_UUID; + const res = await putFectch( + `/reuseFc/request/temp`, + ldrmntRes + ); - return set((state: ldrmntState) => ({ - ...state, - ldrmnt: { - ...state.ldrmnt, - ldrmntRes: ldrmntRes, - }, - })); - - } catch (error) { - console.error("Error ldrmntUpdate:", error); - } + if (res.ok) { + toast.success("잠재수요처 상세정보가 수정되었습니다.",) + } + } catch (error) { + console.error("Error ldrmntUpdate:", error); + toast.error("잠재수요처 상세정보 수정에 실패하였습니다.") + } }, ldrmntDelete: async () => { try { - const response = await deleteFectch( + set((state: ldrmntState) => { + return { + ...state, + ldrmnt: { + ...state.ldrmnt, + isLoading: true, + }, + }; + }); + await deleteFectch( `/reuseFc/request/temp`, ); - } catch (error) { + console.log("error", error) + set((state: ldrmntState) => { + return { + ...state, + ldrmnt: { + ...state.ldrmnt, + isLoading: false, + }, + }; + }); console.error("Error ldrmntDelete:", error); } }, diff --git a/apps/wr/app/store/matDmd/ruseCaseMng/excel.tsx b/apps/wr/app/store/matDmd/ruseCaseMng/excel.tsx new file mode 100644 index 000000000..bb0c18d6a --- /dev/null +++ b/apps/wr/app/store/matDmd/ruseCaseMng/excel.tsx @@ -0,0 +1,109 @@ +import { z } from "zod"; + +export const reUseCaseExcel = z.object({ + file: z.any() +}); + +type nameMap = { + [key: string]: string; +} + +export const reUseCaseNames: nameMap = { + "사업명": "ruseBizNm", + "사업시작일": "bizBgngDate", + "사업종료일": "bizEndDate", + "공급처_시설종류": "splyFcltKnd", + "공급처명": "splyFcltNm", + "공급처_시설용량(㎥/일)": "splyFcltCpct", + "수요처명": "dmdFcltNm", + "수요처_주소": "dmdFcltAddr", + "재이용시설_시설용량(㎥/일)": "ruseFcltCpct", + "재이용시설_관로(km)": "ruseFcltPplLen", + "재이용시설_용도": "ruseFcltUsg", + "재이용시설_공급량(㎥/일)": "ruseFcltSplyCpct", + "재이용시설_처리공법": "ruseFcltPrcsMsrc", + "보증수질_용도별": "grnteWqUsg", + "보증수질_총대장균군수": "grnteWqGrssClfg", + "보증수질_결합잔류염소": "grnteWqCombResdch", + "보증수질_탁도": "grnteWqTrbt", + "보증수질_생물학적산소요구량(BOD)(mg/L)": "grnteWqBod", + "보증수질_냄새": "grnteWqSmll", + "보증수질_색도(도)": "grnteWqCrmty", + "보증수질_총질소(T-N)(mg/L)": "grnteWqTn", + "보증수질_총인(T-P)(mg/L)": "grnteWqTp", + "보증수질_수소이온농도(pH)": "grnteWqPh", + "보증수질_염화물": "grnteWqClIonQnt", + "보증수질_전기전도도": "grnteWqCndctStat", + "보증수질_비고": "grnteWqRmrk", + "소요사업비_총(백만원)": "reqBizGrssCst", + "소요사업비_토목_재이용시설(백만원)": "reqBizReuseFcltCst", + "소요사업비_토목_관로(백만원)": "reqBizPplCst", + "소요사업비_건축(백만원)": "reqBizArchCst", + "소요사업비_기계(백만원)": "reqBizMchnCst", + "소요사업비_전기.계측(백만원)": "reqBizMtrngCst", + "소요사업비_GIS DB(백만원)": "reqBizGisCst", + "소요사업비_CCTV(백만원)": "reqBizCctvCst", + "소요사업비_시공 및 준공(백만원)": "reqBizCmcnCst", + "소요사업비_폐기물(백만원)": "reqBizWstCst", + "소요사업비_설계비(백만원)": "reqBizDesignCst", + "소요사업비_감리비(백만원)": "reqBizSpvsCst", + "소요사업비_시설부대비(백만원)": "reqBizSbsdCst", + "소요사업비_기타(백만원)": "reqBizEtc", + "재원조달계획_총(백만원)": "prcmPlanGrssCst", + "재원조달계획_국비(백만원)": "prcmPlanNlep", + "재원조달계획_지방비(백만원)": "prcmPlanLcep", + "재원조달계획_원인자부담(백만원)": "prcmPlanBrdnCst", + "재원조달계획_국고비율(백만원)": "prcmPlanEcqrRt", + "사업비 정산_총(백만원)": "clclnGrssCst", + "사업비 정산_국비(백만원)": "clclnNlep", + "사업비 정산_지방비(백만원)": "clclnLcep", + "사업비 정산_원인자부담(백만원)": "clclnBrdnCst", + "사업비 정산_국고비율(백만원)": "clclnEcqrRt" +} + +interface PbnstSttnItem { + rvbsn: string; + lgbsn: number; + mdmbsn?: string; + rgnNm?: string; + sido?: number; + gugun?: number; + bscFctNm?: string; + efflWqltStndrdNm?: string; + fctCpc?: number; + tel?: string; + ruseUtztnFcltNm?: string; + fcltCpct?: number; + splyExtraCpct?: number; + ruseCpct?: number; + usageTotal?: number; + inUsage?: number; + outUsage?: number; +} + +export const excelPbnstSttnParsing = (data: PbnstSttnItem[]): string[][] => { + const excelExportData: string[][] = []; + + data.forEach((item) => { + excelExportData.push([ + item.rvbsn, + String(item.lgbsn), + item.mdmbsn ?? "", + item.rgnNm ?? "", + String(item.sido ?? ""), + String(item.gugun ?? ""), + String(item.bscFctNm ?? ""), + String(item.efflWqltStndrdNm ?? ""), + String(item.fctCpc ?? ""), + String(item.tel ?? ""), + String(item.ruseUtztnFcltNm ?? ""), + String(item.fcltCpct ?? ""), + String(item.splyExtraCpct ?? ""), + String(item.ruseCpct ?? ""), + String(item.usageTotal ?? ""), + String(item.inUsage ?? ""), + String(item.outUsage ?? ""), + ]) + }) + return excelExportData; +}; \ No newline at end of file diff --git a/apps/wr/app/store/matDmd/ruseCaseMng/headList.tsx b/apps/wr/app/store/matDmd/ruseCaseMng/headList.tsx index 125866601..8c3a232e7 100644 --- a/apps/wr/app/store/matDmd/ruseCaseMng/headList.tsx +++ b/apps/wr/app/store/matDmd/ruseCaseMng/headList.tsx @@ -4,15 +4,22 @@ import type { TableUpperProps } from "../../common/type/header"; //★☆★☆ 여기서부터 수정하기 ★☆★☆ export const ruseCaseHeadListExport: TableUpperProps[] = [ { - id: "a", + id: "ruseBizNm", title: "사업명", visible: true, row: 0, rowSpan: 2, }, { - id: "b", - title: "사업기간", + id: "bizBgngDate", + title: "사업시작일", + visible: true, + row: 0, + rowSpan: 2, + }, + { + id: "bizEndDate", + title: "사업종료일", visible: true, row: 0, rowSpan: 2, @@ -25,19 +32,19 @@ export const ruseCaseHeadListExport: TableUpperProps[] = [ colSpan: 3, }, { - id: "d", + id: "splyFcltKnd", title: "시설종류", visible: true, row: 1, }, { - id: "e", + id: "splyFcltNm", title: "공급처명", visible: true, row: 1, }, { - id: "f", + id: "splyFcltCpct", title: "시설용량(m³/일)", visible: true, row: 1, @@ -50,13 +57,13 @@ export const ruseCaseHeadListExport: TableUpperProps[] = [ colSpan: 2, }, { - id: "h", + id: "dmdFcltNm", title: "수요처명", visible: true, row: 1, }, { - id: "i", + id: "dmdFcltAddr", title: "주소", visible: true, row: 1, @@ -69,31 +76,31 @@ export const ruseCaseHeadListExport: TableUpperProps[] = [ colSpan: 5, }, { - id: "q2", + id: "ruseFcltCpct", title: "시설용량(m³/일)", visible: true, row: 1, }, { - id: "q3", + id: "ruseFcltPplLen", title: "관로(km)", visible: true, row: 1, }, { - id: "q4", + id: "ruseFcltUsg", title: "용도", visible: true, row: 1, }, { - id: "q5", + id: "ruseFcltSplyCpct", title: "공급량(m³/일)", visible: true, row: 1, }, { - id: "q6", + id: "ruseFcltPrcsMsrc", title: "처리공법", visible: true, row: 1, @@ -106,79 +113,79 @@ export const ruseCaseHeadListExport: TableUpperProps[] = [ colSpan: 13, }, { - id: "k1", + id: "grnteWqUsg", title: "용도별", visible: true, row: 1, }, { - id: "k2", + id: "grnteWqGrssClfg", title: "총대장균군수​(개/100ml)", visible: true, row: 1, }, { - id: "k3", + id: "grnteWqCombResdch", title: "결합잔류염소(mg/L)", visible: true, row: 1, }, { - id: "k4", + id: "grnteWqTrbt", title: "탁도(NTU)", visible: true, row: 1, }, { - id: "k5", + id: "grnteWqBod", title: "생물학적​산소요구량(BOD)(mg/L)​", visible: true, row: 1, }, { - id: "k6", + id: "grnteWqSmll", title: "냄새​", visible: true, row: 1, }, { - id: "k7", + id: "grnteWqCrmty", title: "색도​", visible: true, row: 1, }, { - id: "k8", + id: "grnteWqTn", title: "총질소(T-N)(mg/L)​", visible: true, row: 1, }, { - id: "k9", + id: "grnteWqTp", title: "총인(T-P)(mg/L)", visible: true, row: 1, }, { - id: "k10", + id: "grnteWqPh", title: "수소이온농도(PH)​", visible: true, row: 1, }, { - id: "k11", - title: "수소이온농도(PH)​", + id: "grnteWqClIonQnt", + title: "염화물", visible: true, row: 1, }, { - id: "k12", - title: "전기전도도​(㎲/cm", + id: "grnteWqCndctStat", + title: "전기전도도​(㎲/cm)", visible: true, row: 1, }, { - id: "k13", + id: "grnteWqRmrk", title: "비고​", visible: true, row: 1, @@ -191,85 +198,85 @@ export const ruseCaseHeadListExport: TableUpperProps[] = [ colSpan: 14, }, { - id: "h1", + id: "reqBizGrssCst", title: "총", visible: true, row: 1, }, { - id: "h2", - title: "토목재이용시설", + id: "reqBizReuseFcltCst", + title: "토목재이용시설", visible: true, row: 1, }, { - id: "h3", + id: "reqBizPplCst", title: "토목관로", visible: true, row: 1, }, { - id: "h4", + id: "reqBizArchCst", title: "건축", visible: true, row: 1, }, { - id: "h5", + id: "reqBizMchnCst", title: "기계", visible: true, row: 1, }, { - id: "h6", + id: "reqBizMtrngCst", title: "전기・계측", visible: true, row: 1, }, { - id: "h7", + id: "reqBizGisCst", title: "GIS・DB", visible: true, row: 1, }, { - id: "h8", + id: "reqBizCctvCst", title: "CCTV", visible: true, row: 1, }, { - id: "h9", + id: "reqBizCmcnCst", title: "시공및준공", visible: true, row: 1, }, { - id: "h10", + id: "reqBizWstCst", title: "페기물", visible: true, row: 1, }, { - id: "h11", - title: "설계비", + id: "reqBizDesignCst", + title: "설계비", visible: true, row: 1, }, { - id: "h12", + id: "reqBizSpvsCst", title: "감리비", visible: true, row: 1, }, { - id: "h13", + id: "reqBizSbsdCst", title: "시설부대비", visible: true, row: 1, }, { - id: "h14", + id: "reqBizEtc", title: "기타", visible: true, row: 1, @@ -279,32 +286,38 @@ export const ruseCaseHeadListExport: TableUpperProps[] = [ title: "자원조달계획(백만원)", visible: true, row: 0, - colSpan: 4, + colSpan: 5, }, { - id: "i1", + id: "prcmPlanGrssCst", title: "총", visible: true, row: 1, }, { - id: "i2", + id: "prcmPlanNlep", title: "국비", visible: true, row: 1, }, { - id: "i3", + id: "prcmPlanLcep", title: "지방비", visible: true, row: 1, }, { - id: "i4", + id: "prcmPlanBrdnCst", title: "원인자부담", visible: true, row: 1, }, + { + id: "prcmPlanEcqrRt", + title: "국고비율", + visible: true, + row: 1, + }, { id: "j", title: "사업비정산(백만원)", @@ -313,31 +326,31 @@ export const ruseCaseHeadListExport: TableUpperProps[] = [ colSpan: 5, }, { - id: "j1", + id: "clclnGrssCst", title: "총", visible: true, row: 1, }, { - id: "j2", + id: "clclnNlep", title: "국비", visible: true, row: 1, }, { - id: "j3", + id: "clclnLcep", title: "지방비", visible: true, row: 1, }, { - id: "j4", + id: "clclnBrdnCst", title: "원인자부담", visible: true, row: 1, }, { - id: "j5", + id: "clclnEcqrRt", title: "국고비율", visible: true, row: 1, diff --git a/apps/wr/app/store/matDmd/ruseCaseMng/index.tsx b/apps/wr/app/store/matDmd/ruseCaseMng/index.tsx index c54b0db1a..5ae2e67b4 100644 --- a/apps/wr/app/store/matDmd/ruseCaseMng/index.tsx +++ b/apps/wr/app/store/matDmd/ruseCaseMng/index.tsx @@ -1,24 +1,124 @@ +import { fetchType, getFetchWithCount, postFectch } from "@/components/lib/api"; +import { toast } from "react-hot-toast"; import type { TableUpperProps } from "../../common/type/header"; import { ruseCaseHeadListExport } from "./headList"; - export type ruseCaseState = { - ruseCase: { - ruseHeadList:TableUpperProps[]; - }; + ruseCaseMng: { + ruseHeadList:TableUpperProps[]; + ruseCaseMngList:ruseCaseMngExcelProps[]; + status: number; + count: number; }; -type ruseCaseActions = { }; + +export interface ruseCaseMngExcelProps { + ruseBizNm: string, + bizBgngDate: string, + bizEndDate: string, + splyFcltKnd: string, + splyFcltNm: string, + splyFcltCpct: number | 0 |'', + dmdFcltNm: string, + dmdFcltAddr: string, + ruseFcltCpct: number | 0 |'', + ruseFcltPplLen: string, + ruseFcltUsg: string, + ruseFcltSplyCpct: number | 0 |'', + ruseFcltPrcsMsrc: string, + grnteWqUsg: string, + grnteWqGrssClfg: number | 0 |'', + grnteWqCombResdch: number | 0 |'', + grnteWqTrbt: string, + grnteWqBod: string, + grnteWqSmll: string, + grnteWqCrmty: string, + grnteWqTn: string, + grnteWqTp: string, + grnteWqPh: string, + grnteWqClIonQnt: string, + grnteWqCndctStat: string, + grnteWqRmrk: string, + reqBizReuseFcltCst: number | 0 |'', + reqBizPplCst: number | 0 |'', + reqBizArchCst: number | 0 |'', + reqBizMchnCst: number | 0 |'', + reqBizMtrngCst: number | 0 |'', + reqBizGisCst: number | 0 |'', + reqBizCctvCst: number | 0 |'', + reqBizCmcnCst: number | 0 |'', + reqBizWstCst: number | 0 |'', + reqBizDesignCst: number | 0 |'', + reqBizSpvsCst: number | 0 |'', + reqBizSbsdCst: number | 0 |'', + reqBizEtc: string, + reqBizGrssCst: number | 0 |'', + prcmPlanNlep: number | 0 |'', + prcmPlanLcep: number | 0 |'', + prcmPlanBrdnCst: number | 0 |'', + prcmPlanEcqrRt: number | 0 |'', + prcmPlanGrssCst: number | 0 |'', + clclnNlep: number | 0 |'', + clclnLcep: number | 0 |'', + clclnBrdnCst: number | 0 |'', + clclnEcqrRt: number | 0 |'', + clclnGrssCst: number | 0 |'', +} + +type ruseCaseActions = { + selectRuseCaseMng: (pageNo: number, pageSize: number, sidoCd: string, sigunguCd: string, usageCd : string, name: string) => Promise<void>; + insertRuseCaseMng: (data: any) => Promise<void>; +}; + +export const fetchSearchRuseCaseMng = async (pageNo: number, pageSize: number, sidoCd: string, sigunguCd: string, usageCd : string, name: string) => { + const tailUrl = '/'+pageNo+'/'+pageSize+ ((sigunguCd !== '전체' && sigunguCd !=='') ? `/${sigunguCd}` : (sidoCd != '전체' && sidoCd != '') ? `/${sidoCd}` : `/0`) + (usageCd? `/${usageCd}` : '/0') + (name? `/${name}` : ''); + const resp = await getFetchWithCount(fetchType.WR,`/reuseFc/pubFcRuseCase${tailUrl}`); + return {data:resp.data, count:resp.count}; +}; + +export const fetchInsertRuseCaseMng = async (data: any) => { + const response = await postFectch('/reuseFc/pubFcRuseCase', data, fetchType.WR); + return {data:response.data, status:response.status}; +}; + export type ruseCaseType = ruseCaseState & ruseCaseActions; export const ruseCaseStateExport: ruseCaseState = { - ruseCase: { - ruseHeadList:ruseCaseHeadListExport, + ruseCaseMng: { + ruseHeadList:ruseCaseHeadListExport, + ruseCaseMngList:[], + status: 0, + count: 0, }, }; -export const ruseCaseActionsExport: (set: any) => ruseCaseActions = ( + +export const ruseCaseActionsExport: (set: any) => ruseCaseActions = ( set: any ) => { return { - + selectRuseCaseMng: async (pageNo: number, pageSize: number, sidoCd: string, sigunguCd: string, usageCd : string, name: string) => { + const res = await fetchSearchRuseCaseMng(pageNo, pageSize, sidoCd, sigunguCd, usageCd, name); + set((state: ruseCaseState) => ({ + ruseCaseMng: { + ...state.ruseCaseMng, + ruseCaseMngList: res.data, + count: res.count, + }, + })); + }, + + insertRuseCaseMng: async (data: any) => { + const response = await fetchInsertRuseCaseMng(data); + if(response.status === 200){ + toast.success("물재이용사례 등록이 완료되었습니다. "); + }else{ + toast.error("물재이용사례 등록에 실패하였습니다. "); + } + set((state: ruseCaseState) => ({ + ruseCaseMng: { + ...state.ruseCaseMng, + status: response.status, + }, + })); + }, }; -}; +}; \ No newline at end of file diff --git a/apps/wr/app/store/matDmd/ruseRvwHstry/headList.tsx b/apps/wr/app/store/matDmd/ruseRvwHstry/headList.tsx index 90c793c2f..e2873d573 100644 --- a/apps/wr/app/store/matDmd/ruseRvwHstry/headList.tsx +++ b/apps/wr/app/store/matDmd/ruseRvwHstry/headList.tsx @@ -6,6 +6,7 @@ export const ruseHstryHeadListExport:TableUpperProps[]= [ id: "cityText", title: "시도", visible: true, + }, { id: "sggText", @@ -19,7 +20,7 @@ export const ruseHstryHeadListExport:TableUpperProps[]= [ }, { id: "daddr", - title: "공장주소", + title: "주소", visible: true, }, { @@ -28,7 +29,7 @@ export const ruseHstryHeadListExport:TableUpperProps[]= [ visible: true, }, { - id: "regDt", //바꾸기 + id: "dmndTmng", //바꾸기 title: "신청일자", visible: true, }, @@ -49,7 +50,7 @@ export const ruseHstryHeadListExport:TableUpperProps[]= [ }, { id: "dmndQty", //바꾸기 - title: "요구수량(M3/d)", + title: "요구수량(m³/일)", visible: true, }, { diff --git a/apps/wr/app/store/matDmd/ruseRvwHstry/index.tsx b/apps/wr/app/store/matDmd/ruseRvwHstry/index.tsx index 3338bc467..c490dc399 100644 --- a/apps/wr/app/store/matDmd/ruseRvwHstry/index.tsx +++ b/apps/wr/app/store/matDmd/ruseRvwHstry/index.tsx @@ -1,14 +1,11 @@ -import type { z } from "zod"; -import type { sttnHeadListType } from "../coSttn/headList"; -import type { - CommonTableProps, - TableUpperProps, -} from "../../common/type/header"; -import { ruseHstryHeadListExport } from "./headList"; -import { fetchType, getFetch, postFectch, putFectch } from "@/components/lib/api"; -import type { DmdCoReqType } from "../coMatRvw/type"; +import { fetchType, getFetchWithCount, postFectch, putFectch } from "@/components/lib/api"; import toast from "react-hot-toast"; +import type { + TableUpperProps +} from "../../common/type/header"; import { TEST_UUID } from "../../ruseFcltMng/fcltReg/rainReg"; +import type { DmdCoReqType } from "../coMatRvw/type"; +import { ruseHstryHeadListExport } from "./headList"; const ruseHistHeadList = ruseHstryHeadListExport; @@ -16,6 +13,7 @@ export type ruseHstryState = { ruseHist: { isInit: boolean; rvwHistRes: DmdCoReqType[]; + rvwHistCount:number; ruseHistHeadList: TableUpperProps[]; }; }; @@ -25,6 +23,7 @@ export const ruseHstryStateExport: ruseHstryState = { isInit: false, rvwHistRes: [], ruseHistHeadList: ruseHistHeadList, + rvwHistCount:1, }, }; @@ -70,7 +69,7 @@ type ruseHstryActions = { endYear: string, sggCd: string ) => void; - ruseRvwComplete:( data: Number[]) => void; + ruseRvwComplete:( data: number[]) => void; }; const irvwHistInit = async ( @@ -79,14 +78,14 @@ const irvwHistInit = async ( startYear: string, endYear: string, ) => { - let url = `/reuseFc/request/infoList/matched/${pageNo}/${pageCnt}/${startYear}/${endYear}`; + const url = `/reuseFc/request/infoList/Matched/${pageNo}/${pageCnt}/${startYear}/${endYear}`; // console.log("sggCd ", sggCd); // if (sggCd) { // url += `/${sggCd}`; // } - const rvwHistRes = await getFetch(fetchType.WR, url); + const rvwHistRes = await getFetchWithCount(fetchType.WR, url); return rvwHistRes; }; @@ -100,7 +99,6 @@ export const ruseHstryActionsExport: (set: any) => ruseHstryActions = ( pageCnt: number, startYear: string, endYear: string, - sggCd?: string ) => { const rvwHistRes = await irvwHistInit( pageNo, @@ -115,7 +113,8 @@ export const ruseHstryActionsExport: (set: any) => ruseHstryActions = ( ruseHist: { ...state.ruseHist, init: false, - rvwHistRes: rvwHistRes, + rvwHistRes: rvwHistRes.data, + rvwHistCount:rvwHistRes.count }, })); }, @@ -131,7 +130,7 @@ export const ruseHstryActionsExport: (set: any) => ruseHstryActions = ( toast.success("매칭 계산 저장이 완료입니다."); const rvwHistRes = await irvwHistInit( 1, - 10, + 13, new Date().getFullYear().toString(), new Date().getFullYear().toString(), ); @@ -140,8 +139,8 @@ export const ruseHstryActionsExport: (set: any) => ruseHstryActions = ( ruseHist: { ...state.ruseHist, init: false, - rvwHistRes: rvwHistRes, - }, + rvwHistRes: rvwHistRes.data, + rvwHistCount:rvwHistRes.count }, })); } } catch (e) { @@ -158,7 +157,7 @@ export const ruseHstryActionsExport: (set: any) => ruseHstryActions = ( dmdCoNm: string, ruseUtztnFcltNm: string ) => { - let url = `/reuseFc/request/infoList/matched/${pageNo}/${pageCnt}/${startYear}/${endYear}`; + let url = `/reuseFc/request/infoList/Matched/${pageNo}/${pageCnt}/${startYear}/${endYear}`; if (sggCd.length > 0) { url += `/${sggCd}`; @@ -175,14 +174,14 @@ export const ruseHstryActionsExport: (set: any) => ruseHstryActions = ( } else { url += `/0`; } - const rvwHistRes = await getFetch(fetchType.WR, url); + const rvwHistRes = await getFetchWithCount(fetchType.WR, url); return set((state: ruseHstryState) => ({ ...state, ruseHist: { ...state.ruseHist, - rvwHistRes: rvwHistRes, - }, + rvwHistRes: rvwHistRes.data, + rvwHistCount:rvwHistRes.count }, })); }, ruseRvwInqua: async ( @@ -193,23 +192,24 @@ export const ruseHstryActionsExport: (set: any) => ruseHstryActions = ( sggCd: string ) => { try { - let url = `/reuseFc/request/infoList/matched/${pageNo}/${pageCnt}/${startYear}/${endYear}`; + let url = `/reuseFc/request/infoList/Matched/${pageNo}/${pageCnt}/${startYear}/${endYear}`; if (sggCd.length > 0) { url += `/${sggCd}/0`; } - const rvwHistRes = await getFetch(fetchType.WR, url); + const rvwHistRes = await getFetchWithCount(fetchType.WR, url); return set((state: ruseHstryState) => ({ ...state, ruseHist: { ...state.ruseHist, - rvwHistRes: rvwHistRes, + rvwHistRes: rvwHistRes.data, + rvwHistCount:rvwHistRes.count }, })); } catch (e) { console.log(" error ", e); } }, - ruseRvwComplete: async (data: Number[]) => { + ruseRvwComplete: async (data: number[]) => { try { const complet = await postFectch( `/reuseFc/request/completeCheck`, @@ -217,17 +217,26 @@ export const ruseHstryActionsExport: (set: any) => ruseHstryActions = ( fetchType.WR ); if (complet.status === 200) { - toast.success("완료 처리가 완료되었습니다."); + toast.success("매칭완료가 완료되었습니다."); const rvwHistRes = await irvwHistInit( 1, - 10, + 13, new Date().getFullYear().toString(), new Date().getFullYear().toString(), ); + + return set((state: ruseHstryState) => ({ + ...state, + ruseHist: { + ...state.ruseHist, + rvwHistRes: rvwHistRes.data, + rvwHistCount:rvwHistRes.count + }, + })); } } catch (e) { console.log(" error ", e); - toast.error("완료 처리 중 오류가 발생했습니다."); + toast.error("매칭 완료 처리 중 오류가 발생했습니다."); } }, }; diff --git a/apps/wr/app/store/prfmncReg/chkgdnReg/rainChkgdn/excel.tsx b/apps/wr/app/store/prfmncReg/chkgdnReg/rainChkgdn/excel.tsx index 5754c02fc..6d6ef18a6 100644 --- a/apps/wr/app/store/prfmncReg/chkgdnReg/rainChkgdn/excel.tsx +++ b/apps/wr/app/store/prfmncReg/chkgdnReg/rainChkgdn/excel.tsx @@ -1,7 +1,11 @@ +import { TEST_UUID } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; import type { SelectDataType } from "@common/business_components"; import type { rainChkZodType } from "."; -import { TEST_UUID } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; -import { buildingType } from "../../../common/code/index"; +import { + isInvalidDate, + isInvalidNumber, + parseRawValue, +} from "@/components/lib/func/excelFunc"; export const useTypes = [ "조경용수", "청소화장실용수", @@ -13,7 +17,7 @@ export const useTypes = [ ]; export type Buildingtype = { - bdstInfoPk: number; + bdstInfoSn: number; ctpvCd: 1; sggCd: 1001; daddr: "string3"; @@ -29,17 +33,17 @@ export type Buildingtype = { export const buildingToExcelType = ( buildings: Buildingtype[], sidoCode: SelectDataType[], - sggCdoe: SelectDataType[], + sggCdoe: SelectDataType[] ) => { const excelData: string[][] = []; buildings.forEach((building) => { useTypes.forEach((useType) => { excelData.push([ - building.bdstInfoPk.toString(), + building.bdstInfoSn.toString(), sidoCode.find((sido) => sido.val === building.ctpvCd.toString()) ?.text || "", - sggCdoe.find((sido) => sido.val === building.sggCd.toString()) - ?.text || "", + sggCdoe.find((sido) => sido.val === building.sggCd.toString())?.text || + "", building.daddr, building.bdstNm, "", @@ -81,57 +85,125 @@ export const buildingToExcelType = ( }); }); return excelData; - }; export const excelToRainChkData = (excelData: any, year: string) => { - const rainChkgdData: rainChkZodType[] = excelData.map((item: any) => { - if (item["점검일"] !== undefined) { - const checkYmd = item["점검일"].replace(/\./g, "") - let vltnDt = item["과태료 부과일(위반일)"] - if (vltnDt !== undefined) { - vltnDt = vltnDt ? vltnDt.replace(/\./g, "") : ""; - } - const makeGuidDetail = Array.from({ length: 12 }, (_, i) => { - let month = `${i + 1}`; - const splyData = item[`보급_${month}월`]; - const ruseData = item[`빗물_${month}월`]; - if (month.length < 2) month = "0" + month; - return { - utztnUsgCd: item["이용용도"], - ym: year + month, - chckYmd: checkYmd, - splyUswtrUtztnQnt: Number(splyData ?? 0), - ruseFcltUtztnQnt: Number(ruseData ?? 0), - flg: "RNB", - }; - }); + // const errors: string[] = []; + const issues: string[] = []; - return { + const rainChkgdData: rainChkZodType[] = excelData + .map((item: any) => { + if (item["점검일"] !== undefined) { + const checkYmd = item["점검일"].replace(/\./g, ""); + let vltnDt = item["과태료 부과일(위반일)"]; + if (vltnDt !== undefined) { + vltnDt = vltnDt ? vltnDt.replace(/\./g, "") : ""; + } + const makeGuidDetail = Array.from({ length: 12 }, (_, i) => { + let month = `${i + 1}`; + const splyData = parseRawValue(item[`대체_${month}월`]); + const ruseData = parseRawValue(item[`빗물_${month}월`]); + if (month.length < 2) month = "0" + month; + return { + utztnUsgCd: item["이용용도"], + ym: year + month, + chckYmd: checkYmd, + splyUswtrUtztnQnt: splyData, + ruseFcltUtztnQnt: ruseData, + flg: "RNB", + }; + }); - bdstRuseFcltFk: Number(item["NO"]), - chckYmd: checkYmd, - spwtPositn: item["유량측정방법"] ?? "", - rgtr: TEST_UUID, - oilqMeasMthd: item["운영관리형태"] ?? "", - anlOperCst: Number(item["연간운영비(원/년)"]) ?? 0, - operMngCd: item["유량측정방법"] ?? "", - guideDetails: makeGuidDetail, - rainyDetails: [ - { - utztnUsgCd: item["이용용도"] ?? "", - ph: Number(item["PH"] ?? 0), - trbt: Number(item["탁도"] ?? 0), - grssClfg: Number(item["총대장균군수"] ?? 0), - etc: item["기타"] ?? "", // 필드 누락 방지 - vltnDt: vltnDt ?? "", - vltnMttr: item["위반사항"] ?? "", - levyAmt: Number(item["과태료부과금액(천원)"] ?? 0), + if (checkYmd.length > 7) { + return { + bdstNm: item["시설명"] ?? "", + bdstRuseFcltSn: Number(item["NO"]), + chckYmd: checkYmd, + spwtPositn: item["유량측정방법"] ?? "", rgtr: TEST_UUID, - }, - ], - }; - } - }).filter((el: any) => el !== undefined); - return rainChkgdData; + oilqMeasMthd: item["운영관리형태"] ?? "", + anlOperCst: parseRawValue(item["연간운영비(원/년)"]), + operMngCd: item["유량측정방법"] ?? "", + guideDetails: makeGuidDetail, + rainyDetails: [ + { + utztnUsgCd: item["이용용도"], + trbt: parseRawValue(item["탁도"]), + grssClfg: parseRawValue(item["총대장균군수"]), + etc: item["기타"] || "", // 필드 누락 방지 + vltnDt: vltnDt || "-", + vltnMttr: item["위반사항"] || "-", + levyAmt: parseRawValue(item["과태료부과금액(천원)"]), + rgtr: TEST_UUID, + }, + ], + }; + } + } + }) + .filter((el: any) => el !== undefined); + + if (rainChkgdData.length > 0) { + rainChkgdData.forEach((rain: any) => { + const defaultMessage = `[시설명: ${rain.bdstNm}, 점검일: ${rain.chckYmd}, 이용용도: ${rain.guideDetails[0]?.utztnUsgCd}] 항목에서 `; + + if (rain.chckYmd && isInvalidDate(rain.chckYmd)) { + issues.push( + defaultMessage + `점검일: 날짜 형식 아님 (${rain.chckYmd})` + ); + } + if (rain.anlOperCst.length > 2 && isInvalidNumber(rain.anlOperCst)) { + issues.push( + defaultMessage + + ` 연간운영비(원/년): 숫자 형식 아님 (${rain.anlOperCst})` + ); + } + rain.rainyDetails?.forEach((key) => { + if (key.ph?.length > 1 && isInvalidNumber(key.ph)) { + issues.push(defaultMessage + `PH: 숫자 형식 아님 (${key.ph})`); + } + if (key.trbt?.length > 1 && isInvalidNumber(key.trbt)) { + issues.push(defaultMessage + `탁도: 숫자 형식 아님 (${key.trbt})`); + } + if (key.grssClfg?.length > 1 && isInvalidNumber(key.grssClfg)) { + issues.push( + defaultMessage + `총대장균군수: 숫자 형식 아님 (${key.grssClfg})` + ); + } + if (key.levyAmt?.length > 1 && isInvalidNumber(key.levyAmt)) { + issues.push( + defaultMessage + + `과태료부과금액(천원): 숫자 형식 아님 (${key.levyAmt})` + ); + } + if (key.vltnDt?.length > 2 && isInvalidDate(key.vltnDt)) { + issues.push( + defaultMessage + + `과태료 부과일(위반일): 날짜 형식 아님 (${key.vltnDt})` + ); + } + }); + rain.guideDetails?.forEach((guide) => { + const mm = guide.ym.substring(4); + if (isInvalidNumber(guide.splyUswtrUtztnQnt)) { + issues.push( + defaultMessage + + `대체_${mm}: 숫자 형식 아님 (${guide.splyUswtrUtztnQnt})` + ); + } + if (isInvalidNumber(guide.ruseFcltUtztnQnt)) { + issues.push( + defaultMessage + + `빗물_${mm}: 숫자 형식 아님 (${guide.ruseFcltUtztnQnt})` + ); + } + }); + }); + } + + if (issues.length > 0) { + return { rainChkgdData: [], issues }; + } + + return { rainChkgdData, issues: [] }; }; diff --git a/apps/wr/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList.tsx b/apps/wr/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList.tsx index 3cc75d70c..d216b88f6 100644 --- a/apps/wr/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList.tsx +++ b/apps/wr/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList.tsx @@ -2,7 +2,7 @@ import type { TableUpperProps } from "@/app/store/common/type/header"; import { z } from "zod"; export const chkgdRegExcel = z.object({ - bdstInfoPk: z.number().default(111111111), + bdstInfoSn: z.number().default(111111111), chckYr: z .string() .default("") @@ -17,11 +17,11 @@ export const chkgdRegExcel = z.object({ }, { message: "현재 년도보다 높은 수를 입력할 수 없습니다." } ), - file: z.instanceof(File).optional(), + file: z.instanceof(File).nullish(), }); export const guideDetail = z.object({ - chkgdnFk: z.number().nullable(), + chkgdnSn: z.number().nullable(), utztnUsgCd: z.string(), chckYmd: z.string(), ym: z.string(), @@ -30,12 +30,13 @@ export const guideDetail = z.object({ flg: z.string(), }); export const rainChkZod = z.object({ - chkgdnPk: z.number().nullable(), - bdstRuseFcltFk: z.number(), + chkgdnSn: z.number().nullable(), + bdstRuseFcltSn: z.number(), chckYmd: z.string(), cityText:z.string().nullish(), sggText:z.string().nullish(), prcsCpct: z.number().nullable(), + anlOperCst:z.number().nullish(), spwtPositn: z.string().nullable(), regDt: z.string().nullable(), rgtr: z.string().nullable(), @@ -45,7 +46,7 @@ export const rainChkZod = z.object({ guideDetails: z.array(guideDetail), building: z .object({ - bdstInfoPk: z.number(), + bdstInfoSn: z.number(), ctpvCd: z.number(), sggCd: z.number(), daddr: z.string().nullable(), @@ -60,7 +61,7 @@ export const rainChkZod = z.object({ .nullable(), rainyDetails: z.array( z.object({ - chkgdnFk: z.number().nullable(), + chkgdnSn: z.number().nullable(), utztnUsgCd: z.string(), ph: z.number().nullable(), trbt: z.number().nullable(), @@ -114,10 +115,22 @@ export type splRuseWaterType = { ruse12: number; }; -export const rainHeadNmExport: chHeadType[] = [ +export const rainMainHeadNmExport: chHeadType[] = [ + { headNm: "useQuilty", text: "이용수질(필요시)", length: 3 }, + { headNm: "feeHist", text: "과태료 부과내역", length: 3 }, +]; + +export const rainRuseHeadNmExport: chHeadType[] = [ + { headNm: "useQuilty", text: "이용수질(필요시)", length: 3 }, + { headNm: "feeHist", text: "과태료 부과내역", length: 3 }, + { headNm: "ruseWater", text: "연간대체수 이용량(m³)", length: 13 }, + { headNm: "detailQulity", text: "분기", length: 11 }, +]; + + +export const rainSplHeadNmExport: chHeadType[] = [ { headNm: "useQuilty", text: "이용수질(필요시)", length: 3 }, { headNm: "feeHist", text: "과태료 부과내역", length: 3 }, - { headNm: "ruseWater", text: "연간보급수 이용량(m³)", length: 13 }, { headNm: "detailQulity", text: "분기", length: 11 }, { headNm: "splWater", text: "연간빗물 이용량(m³)", length: 13 }, ]; @@ -134,11 +147,11 @@ export const rainChkgdnHeadListExport: TableUpperProps[] = [ { id: "dtfcltCpct", title: "빗물저류조용량(m³)", mergeCell: true }, { id: "chckYmd", title: "점검일", mergeCell: true }, { id: "utztnUsgCd", title: "이용용도" }, - { id: "totalSplyUswtrUtztnQnt", title: "연간보급수이용량(m³)" }, + { id: "totalSplyUswtrUtztnQnt", title: "연간대체수이용량(m³)" }, { id: "totalRuseFcltUtztnQnt", title: "연간빗물이용량(m³)" }, { id: "ph", - title: "PH", + title: "수소이온농도(PH)", upName: "useQuilty", upSequnce: 1, type: "number", @@ -146,7 +159,7 @@ export const rainChkgdnHeadListExport: TableUpperProps[] = [ }, { id: "trbt", - title: "탁도", + title: "탁도(NTU)", upName: "useQuilty", upSequnce: 2, type: "number", @@ -154,7 +167,7 @@ export const rainChkgdnHeadListExport: TableUpperProps[] = [ }, { id: "grssClfg", - title: "총대장균군수", + title: "총대장균군수(개/mg)", upName: "useQuilty", upSequnce: 3, type: "number", @@ -182,7 +195,7 @@ export const rainChkgdnHeadListExport: TableUpperProps[] = [ title: "부과금액(천원)", upName: "feeHist", upSequnce: 3, - type: "number", + type: "text", edit: true, }, ]; diff --git a/apps/wr/app/store/prfmncReg/chkgdnReg/rainChkgdn/index.tsx b/apps/wr/app/store/prfmncReg/chkgdnReg/rainChkgdn/index.tsx index ff612a0fb..d2c8e18e1 100644 --- a/apps/wr/app/store/prfmncReg/chkgdnReg/rainChkgdn/index.tsx +++ b/apps/wr/app/store/prfmncReg/chkgdnReg/rainChkgdn/index.tsx @@ -1,27 +1,31 @@ import { buildingType } from "@/app/store/common/code"; -import { - TEST_UUID, -} from "@/app/store/ruseFcltMng/fcltReg/rainReg"; +import type { TableUpperProps } from "@/app/store/common/type/header"; +import { TEST_UUID } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; import { deleteFectch, fetchType, getFetch, + getFetchWithCount, postFectch, putFectch, } from "@/components/lib/api"; -import { ToastFormUi } from "@/components/ui/toast/toastFromUi"; import type { SelectDataType } from "@common/business_components/ui"; +import toast from "react-hot-toast"; import { z } from "zod"; import { buildingToExcelType } from "./excel"; import { rainChkgdnHeadListExport, rainChkZod, - rainHeadNmExport, + rainMainHeadNmExport, + rainRuseHeadNmExport, + rainSplHeadNmExport, ruseHeadListExport, splHeadListExport, type chHeadType, } from "./headList"; -import type { TableUpperProps } from "@/app/store/common/type/header"; + +export const rainPageSize = 5; +export const commonCd = "41"; export type rainChkZodType = z.infer<typeof rainChkZod>; const fcltDiv = [ @@ -29,39 +33,58 @@ const fcltDiv = [ { val: "RNB", text: "빗물이용시설" }, ]; -const rainHeadNm = rainHeadNmExport; +const rainRuseHeadNm = rainRuseHeadNmExport; +const rainSplHeadNm = rainSplHeadNmExport; const rainChkgdnHeadList = rainChkgdnHeadListExport; const ruseHeadList = ruseHeadListExport; const splHeadList = splHeadListExport; +const rainMainHeadNm = rainMainHeadNmExport const rainChk: rainChkZodType[] = []; export type rainChkgdnState = { rainChkgdn: { rainChkgdnHeadList: TableUpperProps[]; + rainSplHeadNm: chHeadType[] + rainRuseHeadNm: chHeadType[] rainChk: rainChkZodType[]; - rainHeadNm: chHeadType[]; ruseHeadList: TableUpperProps[]; splHeadList: TableUpperProps[]; isInit: boolean; isModalClose: boolean; fcltDiv: SelectDataType[]; + rainMainHeadNm: chHeadType[] excelList: []; isSttn: boolean; + rainChkCount: number; + isLoading: boolean; }; }; type rainChkgdnActions = { - rainChkInit: (sggCd: string, searchTxt: string) => void; - rainChkDetilUpdate(rainChk: rainChkZodType[]): Promise<void>; + rainChkInit: ( + pageNo: number, + cnt: number, + sggCd: string, + searchTxt: string + ) => void; + rainChkDetilUpdate(rainChk: rainChkZodType[] ,year:string) : Promise<void>; rainChkGuidUpdate(rainChk: rainChkZodType): Promise<void>; rainChkExcelDownload( sidoCode: SelectDataType[], sggCode: SelectDataType[] ): Promise<void>; - rainChkExcelDataUPload(excelData: rainChkZodType[]): Promise<void>; - rainChkDelete(chkgdnPks: number[]): Promise<void>; - rainChkHeadChange: (rainChkgdn: rainChkgdnState, isEdit :boolean) => void; + rainChkExcelDataUPload(excelData: rainChkZodType[] ,year :string) : Promise<void>; + rainChkDelete(chkgdnSns: number[] ,year :string): Promise<void>; + rainChkHeadChange: (rainChkgdn: rainChkgdnState, isEdit: boolean) => void; + rainChkNmSearch: ( + pageNo: number, + cnt: number, + year: string, + sggCd: string, + txt: string + ) => void; + setRainChkLoading:(status:boolean) =>void }; export type rainChkgdnType = rainChkgdnState & rainChkgdnActions; @@ -69,7 +92,9 @@ export type rainChkgdnType = rainChkgdnState & rainChkgdnActions; export const rainChkgdnStateExport: rainChkgdnState = { rainChkgdn: { rainChkgdnHeadList: rainChkgdnHeadList, - rainHeadNm: rainHeadNm, + rainRuseHeadNm: rainRuseHeadNm, + rainSplHeadNm: rainSplHeadNm, + rainMainHeadNm: rainMainHeadNm, rainChk: rainChk, splHeadList: splHeadList, ruseHeadList: ruseHeadList, @@ -78,10 +103,12 @@ export const rainChkgdnStateExport: rainChkgdnState = { excelList: [], fcltDiv: fcltDiv, isSttn: false, + rainChkCount: 1, + isLoading: false, }, }; -const makeMdfrChk = (data: rainChkZodType[], type: string, toDo: string) => { +const makeMdfrChk = (data: rainChkZodType[], type: string) => { const chData = data.map((item) => { item.mdfr = TEST_UUID; item.rgtr = TEST_UUID; @@ -98,11 +125,23 @@ const makeMdfrChk = (data: rainChkZodType[], type: string, toDo: string) => { return chData; }; -const initFetch = async (txt: string | undefined, sgg: string | undefined) => { - const rainRes = await getFetch( +const initFetch = async ( + pageNo: number, + cnt: number, + year: string | undefined, + sgg: string | undefined +) => { + const currentYear = new Date().getFullYear().toString(); + + const rainRes = await getFetchWithCount( "wr", - `/reuseChk/rainy` + (txt ? `/${txt}` : "2024") + (sgg ? `/${sgg}` : "/0") + `/reuseChk/rainy` + + (pageNo ? `/${pageNo}` : "/1") + + (cnt ? `/${cnt}` : "/10") + + (year ? `/${year}` : `/${currentYear}`) + + (sgg ? `/${sgg}` : "/0") ); + return rainRes; }; @@ -127,131 +166,202 @@ export const rainChkgdnActionsExport: (set: any) => rainChkgdnActions = ( }, })); }, - rainChkExcelDataUPload: async (excelData: rainChkZodType[]) => { + rainChkExcelDataUPload: async (excelData: rainChkZodType[] ,year:string) => { + set((state: rainChkgdnType) => ( + { rainChkgdn: { ...state.rainChkgdn, isLoading: true } })); try { const res = await postFectch("/reuseChk/rainy", excelData); - if (res.ok) { - ToastFormUi({ - body: "지도점검 등록이 완료되었습니다. ", - title: "지도점검 등록", - }); + if (res.status === 200) { + const rainChkRes = await initFetch( + 1, + rainPageSize, + year, + commonCd + ); + set((state: rainChkgdnState) => ({ + ...state, + rainChkgdn: { + ...state.rainChkgdn, + rainChk: rainChkRes.data, + rainChkCount: rainChkRes.count, + }, + })); + toast.success("지도점검 등록이 완료되었습니다."); + } else { + toast.error("지도점검 등록이 실패되었습니다."); } - const rainChkRes = await initFetch("2024", "0"); - return set((state: rainChkgdnState) => ({ + } catch (error) { + console.log(error); + toast.error("지도점검 등록이 실패되었습니다."); + } finally { + set((state: rainChkgdnType) => ({ rainChkgdn: { ...state.rainChkgdn, isLoading: false } })); + } + }, + rainChkInit: async ( + pageNo: number, + cnt: number, + year: string, + sggCd: string + ) => { + // set((state: rainChkgdnType) => ({ rainChkgdn: { ...state.rainChkgdn, isLoading: true } })); + try { + const rainChkRes = await initFetch(pageNo, cnt, year, sggCd); + set((state: rainChkgdnState) => ({ ...state, rainChkgdn: { ...state.rainChkgdn, - rainChk: rainChkRes, + rainChk: rainChkRes.data, + rainChkCount: rainChkRes.count, + isInit: false, // 무한 루프 방지를 위해 isInit을 true로 설정 }, })); } catch (error) { console.log(error); - ToastFormUi({ - body: "지도점검 등록이 실패되었습니다.", - title: "지도점검 등록", - }); + // 에러가 발생하더라도 무한 루프에 빠지지 않도록 isInit을 true로 설정합니다. + // set((state: rainChkgdnType) => ({ + // rainChkgdn: { ...state.rainChkgdn, isInit: true }, + // })); + } finally { + // set((state: rainChkgdnType) => ({ rainChkgdn: { ...state.rainChkgdn, isLoading: false } })); } }, - rainChkInit: async (searchTxt: string, sggCd: string) => { + rainChkNmSearch: async ( + pageNo: number, + cnt: number, + year: string, + sggCd: string, + txt: string + ) => { try { - const rainChkRes = await initFetch(searchTxt, sggCd); - + const currentYear = new Date().getFullYear(); + const rainChkRes = await getFetchWithCount( + "wr", + `/reuseChk/rainy` + + (pageNo ? `/${pageNo}` : "/1") + + (cnt ? `/${cnt}` : "/10") + + (year ? `/${year}` : `/${currentYear}`) + + (sggCd ? `/${sggCd}` : "/0") + + (txt ? `/${txt}` : "") + ); return set((state: rainChkgdnState) => ({ ...state, rainChkgdn: { ...state.rainChkgdn, - rainChk: rainChkRes, + rainChk: rainChkRes.data, + rainChkCount: rainChkRes.count, }, })); } catch (error) { console.log(error); } }, - rainChkDetilUpdate: async (rainChk: rainChkZodType[]) => { + rainChkDetilUpdate: async (rainChk: rainChkZodType[] ,year:string) => { try { - const chData = await makeMdfrChk(rainChk, "rain", "rainDetil"); + const chData = await makeMdfrChk(rainChk, "rain"); - const res = await postFectch("/reuseChk/rainy", chData); - if (res.ok) { - ToastFormUi({ - body: "지도점검 보급수 및 밋물이용량 수정이 완료되었습니다. ", - title: "지도점검 수정", - }); + const res = await putFectch("/reuseChk/rainy/list", chData); + if (res) { + toast.success( + "지도점검 대체수 및 밋물이용량 수정이 완료되었습니다. " + ); + const rainChkRes = await initFetch( + 1, + rainPageSize, + year, + "41" + ); + return set((state: rainChkgdnState) => ({ + ...state, + rainChkgdn: { + ...state.rainChkgdn, + rainChk: rainChkRes.data, + rainChkCount: rainChkRes.count, + }, + })); + } else { + toast.error("지도점검 대체수 및 밋물이용량 수정 오류입니다."); } - const rainChkRes = await initFetch("2024", "0"); - return set((state: rainChkgdnState) => ({ - ...state, - rainChkgdn: { - ...state.rainChkgdn, - rainChk: rainChkRes, - }, - })); } catch (error) { console.log(error); - alert("수정에 실패하였습니다."); + toast.error("지도점검 대체수 및 밋물이용량 수정 오류입니다."); } }, rainChkGuidUpdate: async (rainChk: rainChkZodType) => { try { //const chData = await makeMdfrChk(rainChk, "guideDetails", "rainDetil"); const res = await putFectch("/reuseChk/rainy", rainChk); - if (res.ok) { - ToastFormUi({ - body: "지도점검 수질 및 과태료 수정이 완료되었습니다. ", - title: "지도점검 수정", - }); + if (res.status === 200) { + toast.success("지도점검 수질 및 과태료 수정이 완료되었습니다. "); + + const currentYear = new Date().getFullYear().toString(); + const rainChkRes = await initFetch( + 1, + rainPageSize, + currentYear, + "41" + ); + return set((state: rainChkgdnState) => ({ + ...state, + rainChkgdn: { + ...state.rainChkgdn, + rainChk: rainChkRes.data, + rainChkCount: rainChkRes.count, + }, + })); + } else { + toast.error("지도점검 수질 및 과태료 수정 오류입니다."); + } - const rainChkRes = await initFetch("2024", "0"); - return set((state: rainChkgdnState) => ({ - ...state, - rainChkgdn: { - ...state.rainChkgdn, - rainChk: rainChkRes, - }, - })); } catch (error) { - console.log(error); + console.log(' 지도점검 수질 및 과태료 수정 오류', error) + toast.error("지도점검 수질 및 과태료 수정 오류입니다."); } }, - rainChkDelete: async (chkgdnPks: number[]) => { + rainChkDelete: async (chkgdnSns: number[] ,year :string) => { try { - const res = await deleteFectch(`/reuseChk/rainy`, chkgdnPks); + const res = await deleteFectch(`/reuseChk/rainy`, chkgdnSns); if (res) { - ToastFormUi({ - body: "삭제가 완료되었습니다. ", - title: "지도점검 삭제", - }); + toast.success("삭제가 완료되었습니다. "); + + const rainChkRes = await initFetch( + 1, + rainPageSize, + year, + "41" + ); + return set((state: rainChkgdnState) => ({ + ...state, + rainChkgdn: { + ...state.rainChkgdn, + rainChk: rainChkRes.data, + rainChkCount: rainChkRes.count, + }, + })); + } else { + toast.error("삭제시 오류가 발생하였습니다.. "); + } - const rainChkRes = await initFetch("2024", "0"); - return set((state: rainChkgdnState) => ({ - ...state, - rainChkgdn: { - ...state.rainChkgdn, - rainChk: rainChkRes, - }, - })); } catch (error) { - ToastFormUi({ body: "삭제를 실패했습니다.. ", title: "지도점검 삭제" }); + console.log(' 삭제시 오류가 발생하였습니다', error) + + toast.error("삭제시 오류가 발생하였습니다.. "); } }, - rainChkHeadChange: async (rainChkgdn: rainChkgdnState , isEdit :boolean) => { + rainChkHeadChange: async (rainChkgdn: rainChkgdnState, isEdit: boolean) => { try { const rainChkgdnHeadList = rainChkgdn.rainChkgdn.rainChkgdnHeadList; const splHeadList = rainChkgdn.rainChkgdn.splHeadList; const ruseHeadList = rainChkgdn.rainChkgdn.ruseHeadList; - rainChkgdnHeadList.forEach(( head) => { - if(head.edit!== undefined) - head.edit=isEdit; + rainChkgdnHeadList.forEach((head) => { + if (head.edit !== undefined) head.edit = isEdit; }); splHeadList.forEach((head) => { - if(head.edit!== undefined) - head.edit=isEdit; + if (head.edit !== undefined) head.edit = isEdit; }); ruseHeadList.forEach((head) => { - if(head.edit!== undefined) - head.edit=isEdit; + if (head.edit !== undefined) head.edit = isEdit; }); return set((state: rainChkgdnState) => ({ @@ -259,13 +369,24 @@ export const rainChkgdnActionsExport: (set: any) => rainChkgdnActions = ( rainChkgdn: { ...state.rainChkgdn, rainChkgdnHeadList: rainChkgdnHeadList, - splHeadList:splHeadList, - ruseHeadList:ruseHeadList + splHeadList: splHeadList, + ruseHeadList: ruseHeadList, }, })); } catch (error) { console.log(error); } - }, + },setRainChkLoading:(status:boolean) =>{ + set((state: rainChkgdnState) => ({ + ...state, + rainChkgdn: { + ...state.rainChkgdn, + isLoading: status, + }, + })); + } + + + }; }; diff --git a/apps/wr/app/store/prfmncReg/chkgdnReg/rswrtChkgdn/excel.tsx b/apps/wr/app/store/prfmncReg/chkgdnReg/rswrtChkgdn/excel.tsx index 1364d6660..262b1cb30 100644 --- a/apps/wr/app/store/prfmncReg/chkgdnReg/rswrtChkgdn/excel.tsx +++ b/apps/wr/app/store/prfmncReg/chkgdnReg/rswrtChkgdn/excel.tsx @@ -2,6 +2,11 @@ import type { SelectDataType } from "@common/business_components/ui"; import { useTypes, type Buildingtype } from "../rainChkgdn/excel"; import { TEST_UUID } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; import type { rswtChkZodType } from "."; +import { + isInvalidDate, + isInvalidNumber, + parseRawValue, +} from "@/components/lib/func/excelFunc"; export const rswtMakeExcelSample = ( buildings: Buildingtype[], @@ -16,7 +21,7 @@ export const rswtMakeExcelSample = ( buildings.forEach((building) => { useTypes.forEach((useType) => { excelSite.guideDetail.push([ - building.bdstInfoPk.toString(), + building.bdstInfoSn.toString(), sidoCode.find((sido) => sido.val === building.ctpvCd.toString()) ?.text || "", sggCdoe.find((sido) => sido.val === building.sggCd.toString())?.text || @@ -62,7 +67,7 @@ export const rswtMakeExcelSample = ( buildings.forEach((building) => { useTypes.forEach((useType) => { excelSite.polutions.push([ - building.bdstInfoPk.toString(), + building.bdstInfoSn.toString(), sidoCode.find((sido) => sido.val === building.ctpvCd.toString()) ?.text || "", sggCdoe.find((sido) => sido.val === building.sggCd.toString())?.text || @@ -125,17 +130,20 @@ export const rswtMakeExcelSample = ( return excelSite; }; -export const excelToRswtGuideData = (excelData: any, year: string) => { - const rainChkgdData: rswtChkZodType[] = excelData +export const excelToRswtGuideData = ( + excelData: any, + year: string, + issues: string[] +) => { + const rswtChkgdData: rswtChkZodType[] = excelData .map((item: any) => { if (item["점검일"] !== undefined) { + const checkYmd = item["점검일"].replace(/\./g, ""); - const checkYmd = item["점검일"].replace(/\./g, "") - const makeGuidDetail = Array.from({ length: 12 }, (_, i) => { let month = `${i + 1}`; - const splyData = item[`대체수_${month}월`]; - const rswtData = item[`중수_${month}월`]; + const splyData = parseRawValue(item[`대체수_${month}월`]); + const rswtData = parseRawValue(item[`중수_${month}월`]); if (month.length < 2) month = "0" + month; return { ym: year + month, @@ -144,85 +152,91 @@ export const excelToRswtGuideData = (excelData: any, year: string) => { ruseFcltUtztnQnt: rswtData !== "" ? rswtData : 0, }; }); - - return { - bdstRuseFcltFk: Number(item["NO"]), - operMngCd: item["운영관리형태"], - anlOperCst: Number(item["연간운영비(원/년)"]) ?? 0, - prcsCpct: item["처리용량(m³)"], - spwtPositn: item["채수지점"], - chckYmd:checkYmd, - utztnUsgCd: item["이용용도"], - guideDetails: makeGuidDetail, - rgtr: TEST_UUID, - mdfr: TEST_UUID, - - }; + if (checkYmd.length > 7) { + return { + bdstRuseFcltSn: Number(item["NO"]), + operMngCd: item["운영관리형태"], + anlOperCst: parseRawValue(item[`연간운영비(원/년)`]), + prcsCpct: parseRawValue(item[`처리용량(m³)`]), + spwtPositn: item["채수지점"], + chckYmd: checkYmd, + utztnUsgCd: item["이용용도"], + guideDetails: makeGuidDetail, + rgtr: TEST_UUID, + mdfr: TEST_UUID, + }; + } } }) .filter((el: any) => el !== undefined); - return rainChkgdData; + + if (rswtChkgdData.length > 0) { + rswtExcelDataCheck(rswtChkgdData,issues) + } + + if (issues.length > 0) { + return { data: [], issues }; + } + + return { data:rswtChkgdData, issues: [] }; }; -export const excelToRswtPolution = (excelData: any, year: string) => { - const rainChkgdData: rswtChkZodType[] = excelData +export const excelToRswtPolution = (excelData: any, issues: string[]) => { + const rswtChkgdData: rswtChkZodType[] = excelData .map((item: any) => { if (item["점검일"] !== undefined) { - - const checkYmd = item["점검일"].replace(/\./g, "") - let vltnDt = item["과태료 부과일(위반일)"] - if (vltnDt !== undefined) { - vltnDt = vltnDt .replace(/\./g, "") + const checkYmd = item["점검일"].replace(/\./g, ""); + let vltnDt = item["과태료 부과일(위반일)"]; + if (vltnDt !== undefined) { + vltnDt = vltnDt.replace(/\./g, ""); } - const makePoluttion = Array.from({ length: 4 }, (_, i) => { - let quarter = `${i+1}`; - const grssClfgUswtr = item[`${quarter}분기_총대장균군수`]; - const combResdch = item[`${quarter}분기_결합잔류염소`]; - const trbt = item[`${quarter}분기_탁도`]; - const bod = item[`${quarter}분기_BOD`]; - const smll = item[`${quarter}분기_냄새`]; - const crmty = item[`${quarter}분기_색도`]; - const tph = item[`${quarter}분기_T-P`]; - const tn = item[`${quarter}분기_T-N`]; - const ph = item[`${quarter}분기_PH`]; - const clIonQnt = item[`${quarter}분기_염화물`]; - + const quarter = `${i + 1}`; + const grssClfgUswtr = parseRawValue( + item[`${quarter}분기_총대장균군수`] + ); + const combResdch = parseRawValue(item[`${quarter}분기_결합잔류염소`]); + const trbt = parseRawValue(item[`${quarter}분기_탁도`]); + const bod = parseRawValue(item[`${quarter}분기_BOD`]); + const smll = parseRawValue(item[`${quarter}분기_냄새`]); + const crmty = parseRawValue(item[`${quarter}분기_색도`]); + const tph = parseRawValue(item[`${quarter}분기_T-P`]); + const tn = parseRawValue(item[`${quarter}분기_T-N`]); + const ph = parseRawValue(item[`${quarter}분기_PH`]); + const clIonQnt = parseRawValue(item[`${quarter}분기_염화물`]); return { - node: quarter, chckYmd: checkYmd, - grssClfgUswtr: grssClfgUswtr !== "" ? grssClfgUswtr : 0, - combResdch: combResdch !== "" ? combResdch : 0, - trbt: trbt !== "" ? trbt : 0, - bod: bod !== "" ? bod : 0, - smll: smll !== "" ? smll : 0, - crmty: crmty !== "" ? crmty : 0, - tph: tph !== "" ? tph : 0, - tn: tn !== "" ? tn : 0, - ph: ph !== "" ? ph : 0, - clIonQnt: clIonQnt !== "" ? clIonQnt : 0, + grssClfgUswtr: grssClfgUswtr, + combResdch: combResdch, + trbt: trbt, + bod: bod, + smll: smll, + crmty: crmty, + tph: tph, + tn: tn, + ph: ph, + clIonQnt: clIonQnt, rgtr: TEST_UUID, mdfr: TEST_UUID, }; }); - return { - bdstRuseFcltFk: Number(item["NO"]), + bdstRuseFcltSn: Number(item["NO"]), chckYmd: checkYmd, utztnUsgCd: item["이용용도"], operMngCd: item["운영관리형태"], - anlOperCst: Number(item["연간운영비(원/년)"]) ?? 0, - prcsCpct: item["처리용량(m³)"], - spwtPositn: item["채수지점"], - rswtrPollutions: makePoluttion, + anlOperCst: parseRawValue(item["연간운영비(원/년)"]), + prcsCpct: parseRawValue(item["처리용량(m³)"]), + spwtPositn: item["채수지점"], + rswtrPollutions: makePoluttion, rgtr: TEST_UUID, mdfr: TEST_UUID, rswtrDetails: [ - { + { utztnUsgCd: item["이용용도"], - vltnDt:vltnDt, + vltnDt: vltnDt, vltnMttr: item["위반사항"], levyAmt: Number(item["과태료 부과금액(천원)"]) ?? 0, rgtr: TEST_UUID, @@ -233,5 +247,118 @@ export const excelToRswtPolution = (excelData: any, year: string) => { } }) .filter((el: any) => el !== undefined); - return rainChkgdData; + if (rswtChkgdData.length > 0) { + rswtExcelDataCheck(rswtChkgdData,issues) + } + + if (issues.length > 0) { + return { data: [], issues }; + } + + return { data:rswtChkgdData, issues: [] }; }; + + +const rswtExcelDataCheck=(rswtChkgdData:rswtChkZodType[], issues:string[]) =>{ + rswtChkgdData.forEach((rswt: any) => { + const defaultMessage = `*중수도 수질 [NO: ${rswt.bdstRuseFcltSn}, 점검일: ${rswt.chckYmd}, 이용용도: ${rswt.utztnUsgCd}] 항목에서 `; + + if (rswt.chckYmd && isInvalidDate(rswt.chckYmd)) { + issues.push( + defaultMessage + `점검일: 날짜 형식 아님 (${rswt.chckYmd})` + ); + } + if (rswt.anlOperCst.length > 2 && isInvalidNumber(rswt.anlOperCst)) { + issues.push( + defaultMessage + + ` 연간운영비(원/년): 숫자 형식 아님 (${rswt.anlOperCst})` + ); + } + if (rswt.prcsCpct.length > 2 && isInvalidNumber(rswt.prcsCpct)) { + issues.push( + defaultMessage + ` 처리용량(m³): 숫자 형식 아님 (${rswt.prcsCpct})` + ); + } + if (rswt?.rswtrPollutions && rswt?.rswtrPollutions.length > 0) { + rswt.rswtrPollutions.forEach((pol) => { + if (isInvalidNumber(pol.grssClfgUswtr)) { + issues.push( + defaultMessage + + `${pol.node}분기_총대장균군수: 숫자 형식 아님 (${pol.grssClfgUswtr})` + ); + } + if (isInvalidNumber(pol.combResdch)) { + issues.push( + defaultMessage + + `${pol.node}분기_결합잔류염소: 숫자 형식 아님 (${pol.combResdch})` + ); + } + if (isInvalidNumber(pol.trbt)) { + issues.push( + defaultMessage + + `${pol.node}분기_탁도: 숫자 형식 아님 (${pol.trbt})` + ); + } + if (isInvalidNumber(pol.bod)) { + issues.push( + defaultMessage + + `${pol.node}분기_BOD: 숫자 형식 아님 (${pol.bod})` + ); + } + if (isInvalidNumber(pol.smll)) { + issues.push( + defaultMessage + + `${pol.node}분기_냄새: 숫자 형식 아님 (${pol.smll})` + ); + } + if (isInvalidNumber(pol.crmty)) { + issues.push( + defaultMessage + + `${pol.node}분기_색도: 숫자 형식 아님 (${pol.crmty})` + ); + } + if (isInvalidNumber(pol.tph)) { + issues.push( + defaultMessage + + `${pol.node}분기_T-P: 숫자 형식 아님 (${pol.tph})` + ); + } + if (isInvalidNumber(pol.tn)) { + issues.push( + defaultMessage + `${pol.node}분기_T-N: 숫자 형식 아님 (${pol.tn})` + ); + } + if (isInvalidNumber(pol.ph)) { + issues.push( + defaultMessage + `${pol.node}분기_PH: 숫자 형식 아님 (${pol.ph})` + ); + + if (isInvalidNumber(pol.clIonQnt)) { + issues.push( + defaultMessage + + `${pol.node}분기_PH: 숫자 형식 아님 (${pol.clIonQnt})` + ); + } + } + }); + } + if (rswt?.rswtrDetails && rswt?.rswtrDetails.length > 0) { + rswt?.rswtrDetails.forEach((key) => { + if (key.levyAmt?.length > 1 && isInvalidNumber(key.levyAmt)) { + issues.push( + defaultMessage + + `과태료부과금액(천원): 숫자 형식 아님 (${key.levyAmt})` + ); + } + if (key.vltnDt?.length > 2 && isInvalidDate(key.vltnDt)) { + issues.push( + defaultMessage + + `과태료 부과일(위반일): 날짜 형식 아님 (${key.vltnDt})` + ); + } + }); + } + }) + + +} \ No newline at end of file diff --git a/apps/wr/app/store/prfmncReg/chkgdnReg/rswrtChkgdn/headList.tsx b/apps/wr/app/store/prfmncReg/chkgdnReg/rswrtChkgdn/headList.tsx index 0e690dbb9..04914b236 100644 --- a/apps/wr/app/store/prfmncReg/chkgdnReg/rswrtChkgdn/headList.tsx +++ b/apps/wr/app/store/prfmncReg/chkgdnReg/rswrtChkgdn/headList.tsx @@ -1,6 +1,6 @@ +import type { TableUpperProps } from "@/app/store/common/type/header"; import { z } from "zod"; import { guideDetail, type chHeadType } from "../rainChkgdn/headList"; -import type { TableUpperProps } from "@/app/store/common/type/header"; export type polutionType = { chkgdnDtlFk: number; @@ -19,8 +19,8 @@ export type polutionType = { edit?:boolean }; const reuseBuildingZod = z.object({ - bdstRuseFcltPk: z.number().nullish(), - bdstInfoFk: z.number().nullish(), + bdstRuseFcltSn: z.number().nullish(), + bdstInfoSn: z.number().nullish(), instlCmptnDt: z.string().nullable(), // Assuming it's a date string or null instlCst: z.number(), operMngCd: z.string().nullable(), @@ -41,7 +41,7 @@ const reuseBuildingZod = z.object({ }); const buildingZod = z.object({ - bdstInfoPk: z.number().nullable(), + bdstInfoSn: z.number().nullable(), ctpvCd: z.number(), sggCd: z.number(), daddr: z.string().nullable(), @@ -75,7 +75,7 @@ export const rswtrPolutionsZode = z.object({ export const rswtrDetailsZod = z.object({ chkgdnDtlPk: z.string().nullish(), - chkgdnFk: z.string().nullish(), + chkgdnSn: z.string().nullish(), utztnUsgCd: z.string(), vltnDt: z.string(), vltnMttr: z.string(), @@ -87,8 +87,8 @@ export const rswtrDetailsZod = z.object({ }) export const rswtChkgdZod = z.object({ - chkgdnPk: z.number().nullish(), - bdstRuseFcltFk: z.number().nullish(), + chkgdnSn: z.number().nullish(), + bdstRuseFcltSn: z.number().nullish(), chckYmd: z.string(), prcsCpct: z.number(), dtfcltCpct: z.number(), @@ -109,6 +109,24 @@ export const rswtChkgdZod = z.object({ rswtrPollutions: z.array(rswtrPolutionsZode), }); + + +export const rswtRuseHeadNmExport: chHeadType[] = [ + { headNm: "useQuilty", text: "이용수질(필요시)", length: 3 }, + { headNm: "feeHist", text: "과태료 부과내역", length: 3 }, + { headNm: "ruseWater", text: "연간중수 이용량(m³)", length: 13 }, + { headNm: "detailQulity", text: "분기", length: 11 }, +]; + +export const rswtSplHeadNmExport: chHeadType[] = [ + { headNm: "useQuilty", text: "이용수질(필요시)", length: 3 }, + { headNm: "feeHist", text: "과태료 부과내역", length: 3 }, + { headNm: "detailQulity", text: "분기", length: 11 }, + { headNm: "splWater", text: "연간대체수 이용량(m³)", length: 13 }, +]; + + + export const rswtHeadNmExport: chHeadType[] = [ { headNm: "useQuilty", text: "연간 평균 이용수질", length: 10 }, { headNm: "feeHist", text: "과태료 부과내역", length: 3 }, @@ -126,31 +144,31 @@ export const rswtChkHeadListExport: TableUpperProps[] = [ { id: "spwtPositn", title: "채수지점" ,mergeCell: true}, { id: "chckYmd", title: "점검일" ,mergeCell: true}, { id: "utztnUsgCd", title: "이용용도" }, - { id: "splyUswtrUtztnQnt", title: "연간보급수이용량(m³)" ,type: "number"}, + { id: "splyUswtrUtztnQnt", title: "연간대체수이용량(m³)" ,type: "number"}, { id: "ruseFcltUtztnQnt", title: "연간중수도이용량(m³)",type: "number"}, { id: "grssClfgUswtr", - title: "총대장균군수", + title: "총대장균군수(개/mg)", upName: "useQuilty", upSequnce: 1, type: "number" }, { id: "combResdch", - title: "결합잔류염소", + title: "결합잔류염소(mh/L)", upName: "useQuilty", upSequnce: 2, type: "number" }, - { id: "trbt", title: "탁도", upName: "useQuilty", upSequnce: 3 ,type: "number"}, - { id: "bod", title: "BOD", upName: "useQuilty", upSequnce: 4 ,type: "number"}, + { id: "trbt", title: "탁도(NTU)", upName: "useQuilty", upSequnce: 3 ,type: "number"}, + { id: "bod", title: "BOD(mg/L)", upName: "useQuilty", upSequnce: 4 ,type: "number"}, { id: "smll", title: "냄새", upName: "useQuilty", upSequnce: 5 ,type: "number"}, - { id: "crmty", title: "색도", upName: "useQuilty", upSequnce: 6 ,type: "number"}, - { id: "tn", title: "T-N", upName: "useQuilty", upSequnce: 7 ,type: "number"}, - { id: "tph", title: "T-P", upName: "useQuilty", upSequnce: 8 ,type: "number"}, - { id: "ph", title: "PH", upName: "useQuilty", upSequnce: 9,type: "number" }, - { id: "clIonQnt", title: "염화물", upName: "useQuilty", upSequnce: 10 ,type: "number"}, + { id: "crmty", title: "색도(도)", upName: "useQuilty", upSequnce: 6 ,type: "number"}, + { id: "tn", title: "총질소(T-N)(mg/L)", upName: "useQuilty", upSequnce: 7 ,type: "number"}, + { id: "tph", title: "총인(T-P)(mg/L)", upName: "useQuilty", upSequnce: 8 ,type: "number"}, + { id: "ph", title: "수소이온농도(PH)", upName: "useQuilty", upSequnce: 9,type: "number" }, + { id: "clIonQnt", title: "염화물(mg/L)", upName: "useQuilty", upSequnce: 10 ,type: "number"}, { id: "vltnDt", diff --git a/apps/wr/app/store/prfmncReg/chkgdnReg/rswrtChkgdn/index.tsx b/apps/wr/app/store/prfmncReg/chkgdnReg/rswrtChkgdn/index.tsx index eb0010047..5f72998d1 100644 --- a/apps/wr/app/store/prfmncReg/chkgdnReg/rswrtChkgdn/index.tsx +++ b/apps/wr/app/store/prfmncReg/chkgdnReg/rswrtChkgdn/index.tsx @@ -1,18 +1,24 @@ +import { buildingType } from "@/app/store/common/code"; +import type { TableUpperProps } from "@/app/store/common/type/header"; import { TEST_UUID, } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; -import type { - SelectDataType, - SheetParameter, -} from "@common/business_components"; import { deleteFectch, fetchType, getFetch, + getFetchWithCount, postFectch, putFectch, } from "@/components/lib/api"; -import { buildingType } from "@/app/store/common/code"; +import { ToastFormUi } from "@/components/ui/toast/toastFromUi"; +import type { + SelectDataType, + SheetParameter, +} from "@common/business_components"; +import toast from "react-hot-toast"; +import { z } from "zod"; +import { type chHeadType } from "../rainChkgdn/headList"; import { rswtMakeExcelSample } from "./excel"; import { rswtChkgdZod, @@ -20,15 +26,19 @@ import { rswtDetailHeadListExport, rswtHeadNmExport, rswtrDetailsZod, + rswtRuseHeadNmExport, + rswtSplHeadNmExport, } from "./headList"; -import { z } from "zod"; -import { ToastFormUi } from "@/components/ui/toast/toastFromUi"; -import { type chHeadType } from "../rainChkgdn/headList"; -import type { TableUpperProps } from "@/app/store/common/type/header"; + +export const rswtPageSize = 5; const rswtChkHeadList = rswtChkHeadListExport; const rswtDetailHeadList = rswtDetailHeadListExport; const rswtHeadNm = rswtHeadNmExport; +const rswtRuseHeadNm =rswtRuseHeadNmExport +const rswtSplHeadNm =rswtSplHeadNmExport + + export type rswtChkZodType = z.infer<typeof rswtChkgdZod>; export type rswtrChkDetailsZodType = z.infer<typeof rswtrDetailsZod>; export type rswtChkgdnState = { @@ -40,24 +50,40 @@ export type rswtChkgdnState = { excelList: SheetParameter[]; rswtChkRes: rswtChkZodType[]; rswtChkPolGuide: rswtChkZodType[]; + rswtSplHeadNm:chHeadType[] + rswtRuseHeadNm:chHeadType[] isInit: boolean; - isSttn:boolean + isSttn: boolean; + isLoading:boolean; + rswtChkCount: number; }; }; type rswtChkgdnActions = { - rswtChkgdnInit: (year: string, sggCd: string) => void; + rswtChkgdnInit: ( + pageNo: number, + cnt: number, + year: string, + sggCd: string + ) => void; + rswtChkNmSearch: ( + pageNo: number, + cnt: number, + year: string, + sggCd: string, + txt: string + ) => void; rswtrChkExcelDownload( sidoCode: SelectDataType[], sggCode: SelectDataType[] ): Promise<void>; rswtChkExceDataUpload: (excelData: rswtChkZodType[]) => void; - rswtChkDelete: (chkgdnPks: number[]) => void; - rswtChkDetilsUpdate: (rswtChkDetails: rswtrChkDetailsZodType[]) => void; - rswtChkPolAndGuide: (chkGdnPk: number) => void; + rswtChkDelete: (chkgdnSns: number[] ,year:string) => void; + rswtChkDetilsUpdate: (rswtChkDetails: rswtrChkDetailsZodType[] ,year:string) => void; + rswtChkPolAndGuide: (chkgdnSn: number) => void; rswtChkPolInit: () => void; rwstPolAndGuideUpdate: (rswtChk: rswtChkZodType[]) => void; - rswtChkHeadChange: (rswtChkgdn: rswtChkgdnState, isEdit: boolean) => void; + rswtChkHeadChange: (rswtChkgdn: rswtChkgdnState, isEdit: boolean) => void; }; export type rswtChkgdnType = rswtChkgdnState & rswtChkgdnActions; @@ -67,24 +93,41 @@ export const rswtChkgdnStateExport: rswtChkgdnState = { rswtChkHeadList: rswtChkHeadList, rswtDetailHeadList: rswtDetailHeadList, rswtHeadNm: rswtHeadNm, - excelList: [{ - data: [[]], headerNum: 0, headerRange: "A1:V3", + rswtRuseHeadNm:rswtRuseHeadNm, + rswtSplHeadNm:rswtSplHeadNm, + + excelList: [ + { + data: [[]], + headerNum: 0, + headerRange: "A1:V3", sheetName: "", - startCol: '1' - }], + startCol: "1", + }, + ], rswtChkRes: [], rswtChkPolGuide: [], isInit: false, - isSttn:false, + isSttn: false, + isLoading:false, + rswtChkCount: 1, }, }; -const initFetch = async (year: string | undefined, sgg: string | undefined) => { - const rswtChkRes = await getFetch( +const initFetch = async ( + pageNo: number, + cnt: number, + year: string | undefined, + sgg: string | undefined +) => { + const currentYear = new Date().getFullYear().toString(); + const rswtChkRes = await getFetchWithCount( "wr", - `/reuseChk/reuseWater/` + - (year ? `${year}` : "2024") + - (sgg ? `/${sgg}` : "/0") + `/reuseChk/reuseWater` + + (pageNo ? `/${pageNo}` : "/1") + + (cnt ? `/${cnt}` : "/20") + + (year ? `/${year}` : `/${currentYear}`) + + (sgg ? `/${sgg}` : "/0") ); return rswtChkRes; }; @@ -121,12 +164,12 @@ export const rswtChkgdnActionsExport: (set: any) => rswtChkgdnActions = ( excelList.push({ data: exceldata.guideDetail, headerNum: 0, - headerRange: "A1:AI1", + headerRange: "A1:AJ1", }); excelList.push({ data: exceldata.polutions, headerNum: 0, - headerRange: "A1:AZ1", + headerRange: "A1:BB1", }); return set((state: rswtChkgdnState) => ({ @@ -136,16 +179,62 @@ export const rswtChkgdnActionsExport: (set: any) => rswtChkgdnActions = ( }, })); }, - rswtChkgdnInit: async (year: string, sggCd: string) => { + rswtChkgdnInit: async ( + pageNo: number, + cnt: number, + year: string, + sggCd: string + ) => { + // set((state: rswtChkgdnType) => ({ rswtChkgdn: { ...state.rswtChkgdn, isLoading: true } })); try { - const rswtChkRes = await initFetch("2024", sggCd); + const rswtChkRes = await initFetch(pageNo, cnt, year, sggCd); + + + set((state: rswtChkgdnState) => ({ + ...state, + rswtChkgdn: { + ...state.rswtChkgdn, + rswtChkRes: rswtChkRes.data, + rswtChkCount: rswtChkRes.count, + isInit: true, // 무한 루프를 방지하기 위해 isInit을 true로 설정 + }, + })); + } catch (error) { + console.error(error); + // set((state: rswtChkgdnType) => ({ + // rswtChkgdn: { ...state.rswtChkgdn, isInit: true }, + // })); + } finally { + // set((state: rswtChkgdnType) => ({ rswtChkgdn: { ...state.rswtChkgdn, isLoading: false } })); + } + }, + rswtChkNmSearch: async ( + pageNo: number, + cnt: number, + year: string, + sggCd: string, + txt: string + ) => { + try { + console.log("여기") + const currentYear = new Date().getFullYear().toString(); + const rswtChkRes = await getFetchWithCount( + "wr", + `/reuseChk/reuseWater` + + (pageNo ? `/${pageNo}` : "/1") + + (cnt ? `/${cnt}` : "/20") + + (year ? `/${year}` : `/${currentYear}`) + + (sggCd ? `/${sggCd}` : "/0") + + (txt ? `/${txt}` : "") + ); return set((state: rswtChkgdnState) => ({ ...state, rswtChkgdn: { ...state.rswtChkgdn, - rswtChkRes: rswtChkRes, - isInit: false, + rswtChkRes: rswtChkRes.data, + rswtChkCount: rswtChkRes.count, + // isInit: false, }, })); } catch (error) { @@ -154,6 +243,8 @@ export const rswtChkgdnActionsExport: (set: any) => rswtChkgdnActions = ( }, rswtChkExceDataUpload: async (excelData: rswtChkZodType[]) => { try { + set((state: rswtChkgdnType) => ( + { rswtChkgdn: { ...state.rswtChkgdn, isLoading: true } })); const res = await postFectch("/reuseChk/reuseWater", excelData); if (res.ok) { ToastFormUi({ @@ -161,69 +252,73 @@ export const rswtChkgdnActionsExport: (set: any) => rswtChkgdnActions = ( title: "지도점검 등록", }); } - // const rainChkRes = await initFetch('2024', '0'); + const currentYear = new Date().getFullYear().toString(); + const rswtChkRes = await initFetch(1, rswtPageSize, currentYear, "0"); return set((state: rswtChkgdnState) => ({ ...state, rswtChkgdn: { ...state.rswtChkgdn, + rswtChkRes: rswtChkRes.data, + rswtChkCount: rswtChkRes.count, }, })); } catch (error) { console.log(error); + }finally{ + set((state: rswtChkgdnType) => ( + { rswtChkgdn: { ...state.rswtChkgdn, isLoading: false } })); } + }, - rswtChkDetilsUpdate: async (rswtChk: rswtrChkDetailsZodType[]) => { + rswtChkDetilsUpdate: async (rswtChk: rswtrChkDetailsZodType[] ,year:string) => { try { const chData = await makeMdfrChk(rswtChk); - const res = await putFectch("/reuseChk/reuseWater", chData); if (res.ok) { - ToastFormUi({ - body: "지도점검 과태료 수정 수정이 완료되었습니다. ", - title: "지도점검 수정", - }); + toast.success("지도점검 과태료 수정이 완료되었습니다. ",) } - const rswtChkRes = await initFetch("2024", "0"); + const rswtChkRes = await initFetch(1, rswtPageSize, year, "0"); return set((state: rswtChkgdnState) => ({ ...state, rswtChkgdn: { ...state.rswtChkgdn, - rswtChkRes: rswtChkRes, + rswtChkRes: rswtChkRes.data, + rswtChkCount: rswtChkRes.count, }, })); } catch (error) { console.log(error); - alert("수정에 실패하였습니다."); + toast.error("지도점검 과태료 수정 오류입니다. ",) + } }, - rswtChkDelete: async (chkgdnPks: number[]) => { + rswtChkDelete: async (chkgdnSns: number[] ,year:string) => { try { - const res = await deleteFectch(`/reuseChk/reuseWater`, chkgdnPks); + const res = await deleteFectch(`/reuseChk/reuseWater`, chkgdnSns); - if (res.ok) { - ToastFormUi({ - body: "삭제가 완료되었습니다. ", - title: "지도점검 삭제", - }); + if (res) { + toast.success("삭제가 완료되었습니다.") } - const rswtChkRes = await initFetch("2024", "0"); + const rswtChkRes = await initFetch(1, rswtPageSize, year, "0"); return set((state: rswtChkgdnState) => ({ ...state, rswtChkgdn: { ...state.rswtChkgdn, - rswtChkRes: rswtChkRes, + rswtChkRes: rswtChkRes.data, + rswtChkCount: rswtChkRes.count, }, })); } catch (error) { - ToastFormUi({ body: "삭제를 실패했습니다.. ", title: "지도점검 삭제" }); + toast.error("삭제 오류입니다.") + console.error(error); } }, - rswtChkPolAndGuide: async (chkGdnPk: number) => { + rswtChkPolAndGuide: async (chkgdnSn: number) => { try { const rswtChkPolGuide = await getFetch( "wr", - `/reuseChk/reuseWater/${chkGdnPk}` + `/reuseChk/reuseWater/${chkgdnSn}` ); return set((state: rswtChkgdnState) => ({ @@ -233,7 +328,9 @@ export const rswtChkgdnActionsExport: (set: any) => rswtChkgdnActions = ( rswtChkPolGuide: rswtChkPolGuide, }, })); - } catch (error) {} + } catch (error) { + console.error(error); + } }, rswtChkPolInit: () => { try { @@ -244,22 +341,22 @@ export const rswtChkgdnActionsExport: (set: any) => rswtChkgdnActions = ( rswtChkPolGuide: undefined, }, })); - } catch (error) {} + } catch (error) { + console.error(error); + } }, rwstPolAndGuideUpdate: async (rswtChk: rswtChkZodType[]) => { try { const chData = await makeMdfrChk(rswtChk); const res = await putFectch( - `/reuseChk/reuseWater/${rswtChk[0]?.chkgdnPk}`, + `/reuseChk/reuseWater/${rswtChk[0]?.chkgdnSn}`, chData[0] ); if (res.ok) { - ToastFormUi({ - body: "지도점검 연간 수질 및 보급수/중수도 수정이 완료되었습니다. ", - title: "지도점검 수정", - }); + toast.success("지도점검 연간 수질 및 대체수/중수도 수정이 완료되었습니다.") } - const rswtChkRes = await initFetch("2024", "0"); + const currentYear = new Date().getFullYear().toString(); + const rswtChkRes = await initFetch(1, rswtPageSize, currentYear, "0"); return set((state: rswtChkgdnState) => ({ ...state, rswtChkgdn: { @@ -269,10 +366,9 @@ export const rswtChkgdnActionsExport: (set: any) => rswtChkgdnActions = ( })); } catch (error) { console.log(error); - ToastFormUi({ - body: "지도점검 연간 수질 및 보급수/중수도 수정이 실패하였습니다.. ", - title: "지도점검 수정", - }); + + toast.error("지도점검 연간 수질 및 대체수/중수도 수정 오류입니다.") + } }, rswtChkHeadChange: async (rswtChkgdn: rswtChkgdnState, isEdit: boolean) => { @@ -293,7 +389,9 @@ export const rswtChkgdnActionsExport: (set: any) => rswtChkgdnActions = ( rswtDetailHeadList: rswtDetailHeadList, }, })); - } catch (e) {} + } catch (e) { + console.error(e); + } }, }; }; diff --git a/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/excel.tsx b/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/excel.tsx index d8160cab9..c75011361 100644 --- a/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/excel.tsx +++ b/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/excel.tsx @@ -23,7 +23,7 @@ type NameMap = { }; -export const excelToPbnPrfData = (excelData: JSON[], year: string,pbnstRuseFcltFk:string) => { +export const excelToPbnPrfData = (excelData: JSON[], year: string,pbnstRuseFcltId:string) => { // 월별 데이터 하나로 병합 const merged = {} as any; excelData.forEach((item: any) => { @@ -46,7 +46,7 @@ export const excelToPbnPrfData = (excelData: JSON[], year: string,pbnstRuseFcltF return({ ...(item as object), - pbnstRuseFcltFk:pbnstRuseFcltFk, + pbnstRuseFcltId:pbnstRuseFcltId, rgtr: TEST_UUID, mdfr: TEST_UUID, }) diff --git a/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/headList.tsx b/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/headList.tsx index 544f07516..0e19fe52b 100644 --- a/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/headList.tsx +++ b/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/headList.tsx @@ -4,7 +4,7 @@ import type { TableUpperProps } from "../../common/type/header"; export const pbnPrfRegExcelZod = z.object({ ym: z.string().default(""), bscFctNm: z.string().default(""), - pbnstRuseFcltFk:z.string(), + pbnstRuseFcltId: z.string(), pbnstRuseFcltNm: z.string().default(""), file: z.instanceof(File).optional(), }); @@ -14,24 +14,25 @@ export const pbnstPrfmncHeadListExport: TableUpperProps[] = [ { id: "gugun", title: "시군구" }, { id: "bscFctNm", title: "시설명" }, { id: "ruseUtztnFcltNm", title: "재이용시설명" }, - { id: "pbnstRuseFcltPk", title: "재이용시설코드" }, + { id: "pbnstRuseFcltId", title: "재이용시설코드" }, ]; export const monthHeadListExport: TableUpperProps[] = [ + { id: "구분", title: "구분", mergeCell: true }, { id: "용수명", title: "용수명" }, { id: "total", title: "계" }, - { id: "1월", title: "1월" ,edit: true }, - { id: "2월", title: "2월" ,edit: true }, - { id: "3월", title: "3월" ,edit: true }, - { id: "4월", title: "4월" ,edit: true }, - { id: "5월", title: "5월" ,edit: true }, - { id: "6월", title: "6월" ,edit: true }, - { id: "7월", title: "7월" ,edit: true }, - { id: "8월", title: "8월" ,edit: true }, - { id: "9월", title: "9월" ,edit: true }, - { id: "10월", title: "10월" ,edit: true }, - { id: "11월", title: "11월" ,edit: true }, - { id: "12월", title: "12월" ,edit: true }, + { id: "1월", title: "1월", edit: true }, + { id: "2월", title: "2월", edit: true }, + { id: "3월", title: "3월", edit: true }, + { id: "4월", title: "4월", edit: true }, + { id: "5월", title: "5월", edit: true }, + { id: "6월", title: "6월", edit: true }, + { id: "7월", title: "7월", edit: true }, + { id: "8월", title: "8월", edit: true }, + { id: "9월", title: "9월", edit: true }, + { id: "10월", title: "10월", edit: true }, + { id: "11월", title: "11월", edit: true }, + { id: "12월", title: "12월", edit: true }, ]; diff --git a/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx b/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx index 87e827f60..fbdd4fba6 100644 --- a/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx +++ b/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx @@ -3,7 +3,6 @@ import { pbnstPrfmncHeadListExport, monthHeadListExport } from "./headList"; import type { pubHasuType } from "../../ruseFcltMng/fcltReg/pbnstReg/type"; import type { FcPrfType, PrfType } from "./type"; import type { TableUpperProps } from "../../common/type/header"; -import { ToastFormUi } from "@/components/ui/toast/toastFromUi"; import toast from "react-hot-toast"; const pbnstPrfmncHeadList = pbnstPrfmncHeadListExport; @@ -15,7 +14,9 @@ export type pbnstPrfmncState = { pbnstPrfmncHeadList: TableUpperProps[]; monthHeadList: TableUpperProps[]; prfmncRes: FcPrfType[]; - excelList: Number[]; + prfmncResCount:number; + excelList: number[]; + pageKey:number }; }; @@ -25,34 +26,39 @@ export const pbnstPrfmncStateExport: pbnstPrfmncState = { pbnstPrfmncHeadList: pbnstPrfmncHeadList, monthHeadList: monthHeadList, prfmncRes: [], + prfmncResCount:1, excelList: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + pageKey:1 }, }; export type pbnstPrfmncType = pbnstPrfmncState & pbnstPrfmncActions; type pbnstPrfmncActions = { - prfmncHasuInit: (page: string, sggOrCity: string) => void; - prfmnRegData: (data: PrfType) => void; + prfmncHasuInit: (page: number, sggOrCity: string ,year:string) => void; + prfmncHasuNmSearch: (page: number, sggOrCity: string , txt:string ,year:string) => void; + prfmnRegData: (page:number,sggOrCity:string, data: PrfType,year:string) => void; }; -const checkWrRegList = async () => { - const wrRegList = await getFetch("wr", `/reuseFc/pubDetailList`); +const checkWrRegList = async (year:string) => { + const url =`/reuseFc/pubDetailList` + if(year&& year.length>0){ + // url+=`/${year}` + } + const wrRegList = await getFetch("wr", url); return wrRegList; }; -const hasuListFetch = async (page: string, sggOrCity: string) => { - const checkWrRegListRes = await checkWrRegList(); +const hasuListFetch = async (page: number, sggOrCity: string , year:string,searchNm?:string ) => { + const checkWrRegListRes = await checkWrRegList(year); const rainRes = await postFectch( `/prcsFclt/ruseInfo/${page ? page : 1}/10/A/${sggOrCity ? sggOrCity : "11"}/0/1/SW`, checkWrRegListRes?.fcltCds, fetchType.COMMON ); - - - const findData = rainRes.data.map((item: pubHasuType) => { + let findData = rainRes.data.map((item: pubHasuType) => { const data = checkWrRegListRes?.fcPrfList.find( (fclt: FcPrfType) => - fclt.pbnstRuseFcltPk.substring(0, 11) === item?.bscFctCd + fclt.pbnstRuseFcltId.substring(0, 11) === item?.bscFctCd ); if(data !==undefined){ return( { @@ -62,27 +68,32 @@ const hasuListFetch = async (page: string, sggOrCity: string) => { bscFctNm: item.bscFctNm, bscFctCd: item.bscFctCd, ruseUtztnFcltNm: data.ruseUtztnFcltNm, - pbnstRuseFcltPk: data.pbnstRuseFcltPk, + pbnstRuseFcltId: data.pbnstRuseFcltId, prfList: data.prfList, }); } - }); + }).filter(Boolean); // undefined 값을 제거합니다. - return findData; + if(searchNm){ + + findData= findData.filter((ruse:any) => ruse.ruseUtztnFcltNm.includes(searchNm)) + } + + return {data:findData, count:findData.length}; }; export const pbnstPrfmncActionsExport: (set: any) => pbnstPrfmncActions = ( set: any ) => { return { - prfmncHasuInit: async (page: string, sggOrCity: string) => { + prfmncHasuInit: async (page: number, sggOrCity: string,year:string) => { try { - const prfmncRes = await hasuListFetch(page, sggOrCity); - console.log("prfmncRes", prfmncRes); + const prfmncRes = await hasuListFetch(page, sggOrCity, year); return set((state: pbnstPrfmncState) => ({ ...state, prfmnc: { ...state.prfmnc, - prfmncRes: prfmncRes, + prfmncRes: prfmncRes.data, + prfmncResCount: prfmncRes.count, isInit: false, }, })); @@ -90,7 +101,23 @@ export const pbnstPrfmncActionsExport: (set: any) => pbnstPrfmncActions = ( console.log(errorHasu); } }, - prfmnRegData: async (data: PrfType) => { + prfmncHasuNmSearch: async (page: number, sggOrCity: string,fcltNm:string ,year:string)=> { + try { + const prfmncRes = await hasuListFetch(page, sggOrCity,fcltNm ,year); + return set((state: pbnstPrfmncState) => ({ + ...state, + prfmnc: { + ...state.prfmnc, + prfmncRes: prfmncRes.data, + prfmncResCount: prfmncRes.count, + isInit: false, + }, + })); + } catch (errorHasu) { + console.log(errorHasu); + } + }, + prfmnRegData: async (page:number,sggOrCity:string,data: PrfType,year:string) => { try { const ressponse = await postFectch( "/reuseFc/pubFcPrf", @@ -101,6 +128,20 @@ export const pbnstPrfmncActionsExport: (set: any) => pbnstPrfmncActions = ( if (ressponse.status === 200) { toast.success("하수재이용 시설 실적 등록이 완료되었습니다."); + const prfmncRes = await hasuListFetch(page, sggOrCity,year) ; + + + return set((state: pbnstPrfmncState) => ({ + ...state, + prfmnc: { + ...state.prfmnc, + prfmncRes: prfmncRes.data, + prfmncResCount: prfmncRes.count, + isInit: false, + pageKey: state.prfmnc.pageKey + 1 + }, + })); + } } catch (e) { console.log(e); diff --git a/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/type.tsx b/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/type.tsx index f6d5ff43f..6723f3922 100644 --- a/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/type.tsx +++ b/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/type.tsx @@ -1,9 +1,22 @@ import { z } from "zod"; -import type { TableUpperProps } from "../../common/type/header"; import { fcOperSchema } from "../../ruseFcltMng/fcltReg/pbnstReg/type"; + + +type Month = `${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12}월`; + +export type PrfUsageSummary = { + 구분?: string; + usage: string; + 용수명?: string; + total: number; + edit?:boolean +} & Record<Month, number>; + + + export const PrfSchema = z.object({ - pbnstRuseFcltFk: z.string(), + pbnstRuseFcltId: z.string(), ym: z.string(), // 예: "202310" wshwtr: z.number(), coolUswtr: z.number(), @@ -27,7 +40,7 @@ export const PrfSchema = z.object({ }); export const FcPrfSchema = z.object({ - pbnstRuseFcltPk: z.string(), + pbnstRuseFcltId: z.string(), ruseUtztnFcltNm: z.string(), fcltCpct: z.number(), ruseCpct: z.number(), @@ -44,8 +57,9 @@ export const FcPrfSchema = z.object({ export type FcPrfType = z.infer<typeof FcPrfSchema>; export type PrfType = z.infer<typeof PrfSchema>; -export const initValInUsage = [ +export const initValInUsage:PrfUsageSummary[] = [ { + 구분: "장내용수", usage: "wshwtr", 용수명: "세척수", total: 0, @@ -63,6 +77,7 @@ export const initValInUsage = [ "12월": 0, }, { + 구분: "장내용수", usage: "coolUswtr", 용수명: "냉각수", total: 0, @@ -80,6 +95,7 @@ export const initValInUsage = [ "12월": 0, }, { + 구분: "장내용수", usage: "cleanUswtr", 용수명: "청소수", total: 0, @@ -97,6 +113,7 @@ export const initValInUsage = [ "12월": 0, }, { + 구분: "장내용수", usage: "drwtUswtr", 용수명: "식수대", total: 0, @@ -114,6 +131,7 @@ export const initValInUsage = [ "12월": 0, }, { + 구분: "장내용수", usage: "dltnUswtr", 용수명: "희석용수", total: 0, @@ -131,6 +149,7 @@ export const initValInUsage = [ "12월": 0, }, { + 구분: "장내용수", usage: "ithlRswtr", 용수명: "중수도", total: 0, @@ -148,6 +167,7 @@ export const initValInUsage = [ "12월": 0, }, { + 구분: "장내용수", usage: "ithlEtcUswtr", 용수명: "기타", total: 0, @@ -166,8 +186,9 @@ export const initValInUsage = [ }, ]; -export const initValOutUsage = [ +export const initValOutUsage: PrfUsageSummary[] = [ { + 구분: "장외용수", usage: "resn", 용수명: "세척살수용수", total: 0, @@ -185,6 +206,7 @@ export const initValOutUsage = [ "12월": 0, }, { + 구분: "장외용수", usage: "cleanToilUswtr", 용수명: "청소화장실용수", total: 0, @@ -202,6 +224,7 @@ export const initValOutUsage = [ "12월": 0, }, { + 구분: "장외용수", usage: "lndwtr", 용수명: "조경용수", total: 0, @@ -219,6 +242,7 @@ export const initValOutUsage = [ "12월": 0, }, { + 구분: "장외용수", usage: "hydUswtr", 용수명: "친수용수", total: 0, @@ -236,6 +260,7 @@ export const initValOutUsage = [ "12월": 0, }, { + 구분: "장외용수", usage: "ugwtChr", 용수명: "지하수충전", total: 0, @@ -253,6 +278,7 @@ export const initValOutUsage = [ "12월": 0, }, { + 구분: "장외용수", usage: "farmUswtr", 용수명: "농업용수", total: 0, @@ -271,6 +297,7 @@ export const initValOutUsage = [ }, { + 구분: "장외용수", usage: "rvrKeepUswtr", 용수명: "하천등유지용수", total: 0, @@ -289,6 +316,7 @@ export const initValOutUsage = [ }, { + 구분: "장외용수", usage: "indstUswtr", 용수명: "공업용수", total: 0, @@ -307,7 +335,7 @@ export const initValOutUsage = [ }, ]; -export const subTotalObj = { +export const subTotalObj:PrfUsageSummary = { usage: "subtotal", 용수명: "소계", edit: false, @@ -326,7 +354,7 @@ export const subTotalObj = { "12월": 0, }; -export const totalObj = { +export const totalObj:PrfUsageSummary = { usage: "total", 용수명: "총합계", edit: false, diff --git a/apps/wr/app/store/prfmncSttn/chkgdnSttn/rainChkgdnSttn/excel.tsx b/apps/wr/app/store/prfmncSttn/chkgdnSttn/rainChkgdnSttn/excel.tsx index 78e0b878f..cf1cd93a5 100644 --- a/apps/wr/app/store/prfmncSttn/chkgdnSttn/rainChkgdnSttn/excel.tsx +++ b/apps/wr/app/store/prfmncSttn/chkgdnSttn/rainChkgdnSttn/excel.tsx @@ -1,21 +1,7 @@ -import type { rainChkZodType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn"; -type rainSttnExcelType = { - bdstInfoPk: number; - ctpvCd: 1; - sggCd: 1001; - daddr: "string3"; - bdstNm: "string"; - frstArchPrmsnDt: "2025-03-25"; - archArea: 0; - regDt: "2025-04-16"; - rgtr: null; - mdfcnDt: "2025-04-16"; - mdfr: null; -}; export const buildingExcelRainSttn = (excelList: any[]) => { const excelData: string[][] = []; - excelList.forEach((item, index) => { + excelList.forEach((item) => { excelData.push([ String(item.cityText) ?? "", String(item.sggText) ?? "", diff --git a/apps/wr/app/store/prfmncSttn/chkgdnSttn/rainChkgdnSttn/headList.tsx b/apps/wr/app/store/prfmncSttn/chkgdnSttn/rainChkgdnSttn/headList.tsx index f112db0bd..0f9d802ff 100644 --- a/apps/wr/app/store/prfmncSttn/chkgdnSttn/rainChkgdnSttn/headList.tsx +++ b/apps/wr/app/store/prfmncSttn/chkgdnSttn/rainChkgdnSttn/headList.tsx @@ -11,25 +11,25 @@ export const rainChkgdnHeadSttnExport: TableUpperProps[] = [ { id: "dtfcltCpct", title: "빗물저류조용량(m³)", mergeCell: true }, { id: "chckYmd", title: "점검일", mergeCell: true }, { id: "utztnUsgCd", title: "이용용도" }, - { id: "totalSplyUswtrUtztnQnt", title: "연간보급수이용량(m³)" }, + { id: "totalSplyUswtrUtztnQnt", title: "연간대체수이용량(m³)" }, { id: "totalRuseFcltUtztnQnt", title: "연간빗물이용량(m³)" }, { id: "ph", - title: "PH", + title: "수소이온농도(PH)", upName: "useQuilty", upSequnce: 1, type: "number", }, { id: "trbt", - title: "탁도", + title: "탁도(NTU)", upName: "useQuilty", upSequnce: 2, type: "number", }, { id: "grssClfg", - title: "총대장균군수", + title: "총대장균군수(개/mg)", upName: "useQuilty", upSequnce: 3, type: "number", diff --git a/apps/wr/app/store/prfmncSttn/chkgdnSttn/rainChkgdnSttn/index.tsx b/apps/wr/app/store/prfmncSttn/chkgdnSttn/rainChkgdnSttn/index.tsx index 9fd948856..e99bdb7a8 100644 --- a/apps/wr/app/store/prfmncSttn/chkgdnSttn/rainChkgdnSttn/index.tsx +++ b/apps/wr/app/store/prfmncSttn/chkgdnSttn/rainChkgdnSttn/index.tsx @@ -1,11 +1,10 @@ +import type { TableUpperProps } from "@/app/store/common/type/header"; import type { rainChkZodType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn"; -import { type TableUpperProps } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; -import { getFetch } from "@/components/lib/api"; -import { ToastFormUi } from "@/components/ui/toast/toastFromUi"; +import { rainMainHeadNmExport, type chHeadType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList"; +import { getFetchWithCount } from "@/components/lib/api"; import type { SelectDataType } from "@common/business_components/ui"; -import { rainChkgdnHeadSttnExport, ruseSttnHeadListExport, splSttnHeadListExport } from "./headList"; -import { rainHeadNmExport, type chHeadType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList"; import { buildingExcelRainSttn } from "./excel"; +import { rainChkgdnHeadSttnExport, ruseSttnHeadListExport, splSttnHeadListExport } from "./headList"; const fcltDiv = [ { val: "RST", text: "중수도시설" }, @@ -16,29 +15,28 @@ const fcltDiv = [ const rainChkgdnHeadList = rainChkgdnHeadSttnExport; const ruseHeadList = ruseSttnHeadListExport; const splHeadList = splSttnHeadListExport; -const rainHeadNm = rainHeadNmExport - -const rainChk: rainChkZodType[] = []; +const rainHeadNm = rainMainHeadNmExport export type rainChkSttnState = { rainChkSttn: { rainChkRes: rainChkZodType[]; fcltDiv: SelectDataType[]; rainChkgdnHeadList: TableUpperProps[]; - rainHeadNm:chHeadType[]; + rainHeadNm: chHeadType[]; ruseHeadList: TableUpperProps[]; splHeadList: TableUpperProps[]; - sttnExcelList:[] + sttnExcelList: [] isInit: boolean; - isSttn:boolean; + isSttn: boolean; + rainChkCount: number; }; }; type rainChkSttnActions = { - rainChkSttnInit: ( sggCd: string) => void; - rainChkSttnSearch: (searchTxt: string, sggCd: string) => void; - isRainSttn:(isSttn:boolean) => void; - getRainSttnExcel:(excelList:rainChkZodType[]) => void; + rainChkSttnInit: (pageNo: number, cnt: number, searchTxt: string, sggCd: string) => void; + isRainSttn: (isSttn: boolean) => void; + getRainSttnExcel: (excelList: rainChkZodType[]) => void; + rainChkSttnNmSearch: (pageNo: number, cnt: number, year: string, sggCd: string, txt: string) => void }; export type rainChkSttnType = rainChkSttnState & rainChkSttnActions; @@ -50,17 +48,23 @@ export const rainChkSttnStateExport: rainChkSttnState = { rainChkgdnHeadList: rainChkgdnHeadList, ruseHeadList: ruseHeadList, splHeadList: splHeadList, - rainHeadNm:rainHeadNm, + rainHeadNm: rainHeadNm, isInit: false, - sttnExcelList:[], - isSttn:false, + sttnExcelList: [], + isSttn: false, + rainChkCount: 1, }, }; -const initFetch = async (txt: string | undefined, sgg: string | undefined) => { - const rainRes = await getFetch( +const initFetch = async (pageNo: number, cnt: number, txt: string | undefined, sgg: string | undefined) => { + const currentYear = new Date().getFullYear().toString(); + const rainRes = await getFetchWithCount( "wr", - `/reuseChk/rainy/` + (txt ? `${txt}` : "2024") + (sgg ? `/${sgg}` : "/0") + `/reuseChk/rainy` + + (pageNo ? `/${pageNo}` : "/1") + + (cnt ? `/${cnt}` : "/5") + + (txt ? `/${txt}` : `/${currentYear}`) + + (sgg ? `/${sgg}` : "/0") ); return rainRes; }; @@ -68,48 +72,66 @@ export const rainChkSttnActionsExport: (set: any) => rainChkSttnActions = ( set: any ) => { return { - rainChkSttnInit: async ( sggCd: string) => { + rainChkSttnInit: async (pageNo: number, cnt: number, searchTxt: string, sggCd: string) => { try { - const rainChkRes = await initFetch(undefined, sggCd); + const rainChkRes = await initFetch(pageNo, cnt, searchTxt, sggCd); return set((state: rainChkSttnState) => ({ ...state, rainChkSttn: { ...state.rainChkSttn, - rainChkRes:rainChkRes + rainChkRes: rainChkRes.data, + rainChkCount: rainChkRes.count, + isSttn: true }, })); } catch (error) { console.log(error); } }, - rainChkSttnSearch: async (searchTxt: string, sggCd: string) => { + rainChkSttnNmSearch: async ( + pageNo: number, + cnt: number, + year: string, + sggCd: string, + txt: string + ) => { try { - const rainChkRes = await initFetch(searchTxt, sggCd); - + const currentYear = new Date().getFullYear(); + const rainChkRes = await getFetchWithCount( + "wr", + `/reuseChk/rainy` + + (pageNo ? `/${pageNo}` : "/1") + + (cnt ? `/${cnt}` : "/5") + + (year ? `/${year}` : `/${currentYear}`) + + (sggCd ? `/${sggCd}` : "/0") + + (txt ? `/${txt}` : "") + ); return set((state: rainChkSttnState) => ({ ...state, rainChkSttn: { ...state.rainChkSttn, - rainChkRes:rainChkRes + rainChkRes: rainChkRes.data, + rainChkCount: rainChkRes.count, }, })); } catch (error) { console.log(error); } - },isRainSttn :(isSttn:boolean) =>{ + }, + isRainSttn: (isSttn: boolean) => { try { return set((state: rainChkSttnState) => ({ ...state, rainChkSttn: { ...state.rainChkSttn, - isSttn:isSttn + isSttn: isSttn }, })); } catch (error) { console.log(error); } - },getRainSttnExcel: async (excelList:rainChkZodType[]) => { + }, getRainSttnExcel: async (excelList: rainChkZodType[]) => { try { const excelData = buildingExcelRainSttn(excelList) @@ -118,7 +140,7 @@ export const rainChkSttnActionsExport: (set: any) => rainChkSttnActions = ( ...state, rainChkSttn: { ...state.rainChkSttn, - sttnExcelList:excelData + sttnExcelList: excelData }, })); } catch (error) { diff --git a/apps/wr/app/store/prfmncSttn/chkgdnSttn/rswtChkgdnSttn/excel.tsx b/apps/wr/app/store/prfmncSttn/chkgdnSttn/rswtChkgdnSttn/excel.tsx index ff104bc91..55eaf2df1 100644 --- a/apps/wr/app/store/prfmncSttn/chkgdnSttn/rswtChkgdnSttn/excel.tsx +++ b/apps/wr/app/store/prfmncSttn/chkgdnSttn/rswtChkgdnSttn/excel.tsx @@ -1,7 +1,7 @@ export const buildingExcelRswtSttn = (excelList: any[]) => { const excelData: string[][] = []; - excelList.forEach((item, index) => { + excelList.forEach((item) => { excelData.push([ String(item.cityText) ?? "", String(item.sggText) ?? "", @@ -19,6 +19,7 @@ export const buildingExcelRswtSttn = (excelList: any[]) => { String(item.trbt) ?? "", String(item.bod) ?? "", String(item.smll) ?? "", + String(item.crmty) ?? "", String(item.tn) ?? "", String(item.tph) ?? "", String(item.ph) ?? "", diff --git a/apps/wr/app/store/prfmncSttn/chkgdnSttn/rswtChkgdnSttn/headList.tsx b/apps/wr/app/store/prfmncSttn/chkgdnSttn/rswtChkgdnSttn/headList.tsx index f67e98101..e000c550a 100644 --- a/apps/wr/app/store/prfmncSttn/chkgdnSttn/rswtChkgdnSttn/headList.tsx +++ b/apps/wr/app/store/prfmncSttn/chkgdnSttn/rswtChkgdnSttn/headList.tsx @@ -1,43 +1,43 @@ import type { TableUpperProps } from "@/app/store/common/type/header"; export const rswtChkSttnHeadListExport: TableUpperProps[] = [ - { id: "order", title: "순번", rowSpan: true }, - { id: "cityText", title: "시도", rowSpan: true }, - { id: "sggText", title: "시군구", rowSpan: true }, - { id: "bdstNm", title: "시설명", rowSpan: true }, - { id: "operMngCd", title: "운영관리형태", rowSpan: true }, - { id: "anlOperCst", title: "연간운영비(원/년)", rowSpan: true }, - { id: "prcsCpct", title: "처리용량(m³)", rowSpan: true }, - { id: "spwtPositn", title: "채수지점", rowSpan: true }, - { id: "chckYmd", title: "점검일", rowSpan: true }, + { id: "order", title: "순번", mergeCell: true }, + { id: "cityText", title: "시도", mergeCell: true }, + { id: "sggText", title: "시군구", mergeCell: true }, + { id: "bdstNm", title: "시설명", mergeCell: true }, + { id: "operMngCd", title: "운영관리형태", mergeCell: true }, + { id: "anlOperCst", title: "연간운영비(원/년)", mergeCell: true }, + { id: "prcsCpct", title: "처리용량(m³)", mergeCell: true }, + { id: "spwtPositn", title: "채수지점", mergeCell: true }, + { id: "chckYmd", title: "점검일", mergeCell: true }, { id: "utztnUsgCd", title: "이용용도" }, { id: "splyUswtrUtztnQnt", title: "연간보급수이용량(m³)", type: "number" }, { id: "ruseFcltUtztnQnt", title: "연간중수도이용량(m³)", type: "number" }, { id: "grssClfgUswtr", - title: "총대장균군수", + title: "총대장균군수(개/mg)", upName: "useQuilty", upSequnce: 1, type: "number", }, { id: "combResdch", - title: "결합잔류염소", + title: "결합잔류염소(mh/L)", upName: "useQuilty", upSequnce: 2, type: "number", }, { id: "trbt", - title: "탁도", + title: "탁도(NTU)", upName: "useQuilty", upSequnce: 3, type: "number", }, { id: "bod", - title: "BOD", + title: "BOD(mg/L)", upName: "useQuilty", upSequnce: 4, type: "number", @@ -51,23 +51,23 @@ export const rswtChkSttnHeadListExport: TableUpperProps[] = [ }, { id: "crmty", - title: "색도", + title: "색도(도)", upName: "useQuilty", upSequnce: 6, type: "number", }, - { id: "tn", title: "T-N", upName: "useQuilty", upSequnce: 7, type: "number" }, + { id: "tn", title: "총질소(T-N)(mg/L)", upName: "useQuilty", upSequnce: 7, type: "number" }, { id: "tph", - title: "T-P", + title: "총인(T-P)(mg/L)", upName: "useQuilty", upSequnce: 8, type: "number", }, - { id: "ph", title: "PH", upName: "useQuilty", upSequnce: 9, type: "number" }, + { id: "ph", title: "수소이온농도(PH)", upName: "useQuilty", upSequnce: 9, type: "number" }, { id: "clIonQnt", - title: "염화물", + title: "염화물(mg/L)", upName: "useQuilty", upSequnce: 10, type: "number", diff --git a/apps/wr/app/store/prfmncSttn/chkgdnSttn/rswtChkgdnSttn/index.tsx b/apps/wr/app/store/prfmncSttn/chkgdnSttn/rswtChkgdnSttn/index.tsx index b985cd944..0426ed9f5 100644 --- a/apps/wr/app/store/prfmncSttn/chkgdnSttn/rswtChkgdnSttn/index.tsx +++ b/apps/wr/app/store/prfmncSttn/chkgdnSttn/rswtChkgdnSttn/index.tsx @@ -1,10 +1,10 @@ -import { rswtHeadNmExport } from "@/app/store/prfmncReg/chkgdnReg/rswrtChkgdn/headList"; -import { rswtChkSttnHeadListExport, rswtDetailSttnHeadListExport } from "./headList"; import type { TableUpperProps } from "@/app/store/common/type/header"; import type { chHeadType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList"; import type { rswtChkZodType } from "@/app/store/prfmncReg/chkgdnReg/rswrtChkgdn"; -import { getFetch } from "@/components/lib/api"; +import { rswtHeadNmExport } from "@/app/store/prfmncReg/chkgdnReg/rswrtChkgdn/headList"; +import { getFetchWithCount } from "@/components/lib/api"; import { buildingExcelRswtSttn } from "./excel"; +import { rswtChkSttnHeadListExport, rswtDetailSttnHeadListExport } from "./headList"; const rswtChkHeadList = rswtChkSttnHeadListExport; const rswtDetailHeadList = rswtDetailSttnHeadListExport; @@ -20,14 +20,15 @@ export type rswtChkSttnState = { rswtChkRes: rswtChkZodType[]; isInit: boolean; isSttn: boolean; + rswtChkCount: number; }; }; type rswtChkSttnActions = { - rswtChkSttnInit: ( sggCd: string) => void; - isRswtSttn: (isSttn:boolean) => void; - getRswtSttnExcel:(excelList:rswtChkZodType[]) => void; - + rswtChkSttnInit: (pageNo: number, cnt: number, searchTxt: string, sggCd: string) => void; + isRswtSttn: (isSttn: boolean) => void; + getRswtSttnExcel: (excelList: rswtChkZodType[]) => void; + rswtChkNmSttnSearch: (pageNo: number, cnt: number, year: string, sggCd: string, txt: string) => void; }; export type rswtChkSttnType = rswtChkSttnState & rswtChkSttnActions; @@ -40,68 +41,104 @@ export const rswtChkStateExport: rswtChkSttnState = { sttnExcelList: [], rswtChkRes: [], isInit: false, - isSttn:false, + isSttn: false, + rswtChkCount: 1, }, }; -const initFetch = async (year: string | undefined, sgg: string | undefined) => { - const rswtChkRes = await getFetch( +const initFetch = async (pageNo: number, cnt: number, year: string | undefined, sgg: string | undefined) => { + const currentYear = new Date().getFullYear().toString(); + const rswtChkRes = await getFetchWithCount( "wr", - `/reuseChk/reuseWater/` + - (year ? `${year}` : "2024") + - (sgg ? `/${sgg}` : "/0") + `/reuseChk/reuseWater` + + (pageNo ? `/${pageNo}` : "/1") + + (cnt ? `/${cnt}` : "/20") + + (year ? `/${year}` : `/${currentYear}`) + + (sgg ? `/${sgg}` : "/0") ); return rswtChkRes; }; - export const rswtChkSttnActionsExport: (set: any) => rswtChkSttnActions = ( set: any ) => { return { - rswtChkSttnInit: async ( sggCd: string) => { + rswtChkSttnInit: async (pageNo: number, cnt: number, searchTxt: string, sggCd: string) => { try { - const rswtChkRes = await initFetch("2024", sggCd); + const rswtChkRes = await initFetch(pageNo, cnt, searchTxt, sggCd); return set((state: rswtChkSttnState) => ({ ...state, rswtChkSttn: { ...state.rswtChkSttn, - rswtChkRes: rswtChkRes, + rswtChkRes: rswtChkRes.data, + rswtChkCount: rswtChkRes.count, isInit: false, }, })); } catch (error) { console.log(error); } - },isRswtSttn :(isSttn:boolean) =>{ - try { - return set((state: rswtChkSttnState) => ({ - ...state, - rswtChkSttn: { - ...state.rswtChkSttn, - isSttn:isSttn - }, - })); - } catch (error) { - console.log(error); - } - },getRswtSttnExcel: async (excelList:rswtChkZodType []) => { - try { - - const excelData = buildingExcelRswtSttn(excelList) - - return set((state: rswtChkSttnState) => ({ - ...state, - rswtChkSttn: { - ...state.rswtChkSttn, - sttnExcelList:excelData - }, - })); - } catch (error) { - console.log(error); - } - } + }, + isRswtSttn: (isSttn: boolean) => { + try { + return set((state: rswtChkSttnState) => ({ + ...state, + rswtChkSttn: { + ...state.rswtChkSttn, + isSttn: isSttn + }, + })); + } catch (error) { + console.log(error); + } + }, + getRswtSttnExcel: async (excelList: rswtChkZodType[]) => { + try { - + const excelData = buildingExcelRswtSttn(excelList) + + return set((state: rswtChkSttnState) => ({ + ...state, + rswtChkSttn: { + ...state.rswtChkSttn, + sttnExcelList: excelData + }, + })); + } catch (error) { + console.log(error); + } + }, + rswtChkNmSttnSearch: async ( + pageNo: number, + cnt: number, + year: string, + sggCd: string, + txt: string + ) => { + try { + const currentYear = new Date().getFullYear().toString(); + const rswtChkRes = await getFetchWithCount( + "wr", + `/reuseChk/reuseWater` + + (pageNo ? `/${pageNo}` : "/1") + + (cnt ? `/${cnt}` : "/20") + + (year ? `/${year}` : `/${currentYear}`) + + (sggCd ? `/${sggCd}` : "/0") + + (txt ? `/${txt}` : "") + ); + + return set((state: rswtChkSttnState) => ({ + ...state, + rswtChkSttn: { + ...state.rswtChkSttn, + rswtChkRes: rswtChkRes.data, + rswtChkCount: rswtChkRes.count, + isInit: false, + }, + })); + } catch (error) { + console.log(error); + } + }, }; }; diff --git a/apps/wr/app/store/prfmncSttn/pbnstPrfmncSttn/headList.tsx b/apps/wr/app/store/prfmncSttn/pbnstPrfmncSttn/headList.tsx index d04ef7b20..b6641a796 100644 --- a/apps/wr/app/store/prfmncSttn/pbnstPrfmncSttn/headList.tsx +++ b/apps/wr/app/store/prfmncSttn/pbnstPrfmncSttn/headList.tsx @@ -1,5 +1,4 @@ -import { z } from "zod"; -import type { TableUpperProps } from "../../ruseFcltMng/fcltReg/rainReg"; +import type { TableUpperProps } from "../../common/type/header"; export const pbnstPrfmncSttnHeadListExport: TableUpperProps[] = [ { @@ -37,23 +36,46 @@ export const pbnstPrfmncSttnHeadListExport: TableUpperProps[] = [ export const pbnstYearHeadListExport: TableUpperProps[] = [ { + id: "year", title: "년도", colSpan: 3, row: 0 }, { id: "total", - title: "계", + title: "계(m³)", row: 1 }, { id: "inUsage", - title: "장내용수", + title: "장내용수(m³)", row: 1 }, { id: "outUsage", - title: "장외용수", + title: "장외용수(m³)", row: 1 }, ] + + + + + +export const monthSttnHeadListExport: TableUpperProps[] = [ + { id: "구분", title: "구분", mergeCell: true }, + { id: "용수명", title: "용수명" }, + { id: "total", title: "계" }, + { id: "1월", title: "1월" }, + { id: "2월", title: "2월" }, + { id: "3월", title: "3월" }, + { id: "4월", title: "4월" }, + { id: "5월", title: "5월" }, + { id: "6월", title: "6월" }, + { id: "7월", title: "7월" }, + { id: "8월", title: "8월" }, + { id: "9월", title: "9월" }, + { id: "10월", title: "10" }, + { id: "11월", title: "11월" }, + { id: "12월", title: "12월", }, +]; \ No newline at end of file diff --git a/apps/wr/app/store/prfmncSttn/pbnstPrfmncSttn/index.tsx b/apps/wr/app/store/prfmncSttn/pbnstPrfmncSttn/index.tsx index b513085ae..13093f98f 100644 --- a/apps/wr/app/store/prfmncSttn/pbnstPrfmncSttn/index.tsx +++ b/apps/wr/app/store/prfmncSttn/pbnstPrfmncSttn/index.tsx @@ -1,14 +1,16 @@ import { fetchType, getFetch, postFectch } from "@/components/lib/api"; import type { TableUpperProps } from "../../common/type/header"; -import { pbnstPrfmncSttnHeadListExport, pbnstYearHeadListExport } from "./headList"; +import { monthSttnHeadListExport, pbnstPrfmncSttnHeadListExport, pbnstYearHeadListExport } from "./headList"; import type { FcPrfType, PrfType } from "../../prfmncReg/pbnstPrfmncReg/type"; import type { pubHasuType, selectLabel } from "../../ruseFcltMng/fcltReg/pbnstReg/type"; import type { ReturnType } from "@common/business_components"; +import type { WaterReuseFacility } from "../../ruseFcltMng/fcltSttn/pbnstSttn/type"; type pbnstPrfmncSttnActions = { tableHeadMaker: (startYear: number, endYear: number) => void; - pbnstPrfmncInit: (page: string, sggOrCity: string) => void; - pbnstPrfmncSearch: (returnType: ReturnType, swtrmtFcltNm: string, fromDate: string, toDate: string) => void; + // pbnstPrfmncInit: (page: string, sggOrCity: string) => void; + pbnstPrfmncSearch: (page: number, returnType: ReturnType, fcltNm: string, fromDate: string, toDate: string, fclts: string[]) => void; + getFlctsSttn: () => void; } const pbnstPrfmncRes: PrfType[] = []; @@ -22,6 +24,9 @@ export type pbnstPrfmncSttnState = { isInit: boolean; rawData: any[]; selectYearList: selectLabel[] + monthSttnHeadList: TableUpperProps[]; + fclts: string[]; + prfmncResCount: number; } } @@ -34,34 +39,76 @@ export const pbnstPrfmncSttnExport: pbnstPrfmncSttnState = { prfmncRes: prfmncRes, isInit: false, rawData: [], - selectYearList: [] + selectYearList: [], + monthSttnHeadList: monthSttnHeadListExport, + fclts: [], + prfmncResCount: 1, } } -const checkWrRegList = async () => { - const wrRegList = await getFetch("wr", `/reuseFc/pubDetailList`); - return wrRegList; -}; -const hasuListFetch = async (page: string, sggOrCity: string) => { - const checkWrRegListRes = await checkWrRegList(); - const rainRes = await postFectch( - `/prcsFclt/ruseInfo/${page ? page : 1}/10/A/${sggOrCity ? sggOrCity : "11"}/0/1/SW`, - checkWrRegListRes?.fcltCds, - fetchType.COMMON +const hasuListFetch = async ( + page: number, + returnType: ReturnType, + searchNm: string | undefined, + year: string, + flcts: string[] +) => { + let url = `/prcsFclt/ruseInfo/`; + + if (returnType.partValue === "A") { + url += `${page}/5/${returnType.partValue}/${ + // returnType.sidoValue ? returnType.sidoValue : returnType.sigunValue + returnType.sigunValue !== '0' ? returnType.sigunValue : returnType.sidoValue + }/0/1/SW` + } else if (returnType.partValue === "R") { + url += `${page}/5/${returnType.partValue}/${returnType.regionValue + ? returnType.regionValue + : returnType.unitWaterShedValue + ? returnType.unitWaterShedValue + : returnType.middleRegionValue + }/0/1/SW` + } else if (returnType.partValue === 'G') { + url += `${page}/5/${returnType.envPartValue}/${returnType.sigunValue !== '0' ? returnType.sigunValue : returnType.sidoValue}/0/1/SW` + + } else { + url += `1/2/A/0/0/1/SW` + } + + if (searchNm && searchNm?.length > 0) { + url += "/" + searchNm + } + + const pbnList = await postFectch(url, flcts, fetchType.COMMON); + const getRuseList = await postFectch( + `/reuseFc/pubDetailList/${year}`, + flcts, + fetchType.WR ); - const data = checkWrRegListRes.fcPrfList.map((item: FcPrfType) => { - const findData = rainRes.data.find( - (fclt: pubHasuType) => - fclt.bscFctCd === item.pbnstRuseFcltPk.substring(0, 11) - ); - return { - sido: findData?.sido, - gugun: findData?.gugun, - bscFctNm: findData?.bscFctNm, - ruseUtztnFcltNm: item.ruseUtztnFcltNm, - pbnstRuseFcltPk: item.pbnstRuseFcltPk, - }; - }); + let count = 0; + const checkWrRegListRes = getRuseList.data; + const data: WaterReuseFacility[] = checkWrRegListRes.map( + (item: FcPrfType) => { + if (!item.prfList || item.prfList.length === 0) { + return null; + } + const findData = pbnList.data.find( + (fclt: pubHasuType) => + fclt.bscFctCd === item.pbnstRuseFcltId.substring(0, 11) + ); + if (findData) { + count++; + return { + sido: findData?.sido || '-', + gugun: findData?.gugun || '-', + bscFctNm: findData?.bscFctNm || '-', + ruseUtztnFcltNm: item?.ruseUtztnFcltNm || '-', + pbnstRuseFcltId: item?.pbnstRuseFcltId || '-', + bscFctCd: findData?.bscFctCd || '-', + count + } + } + } + ).filter(Boolean); return data; } @@ -69,6 +116,15 @@ export const pbnstPrfmncSttnActionsExport: (set: any) => pbnstPrfmncSttnActions set: any ) => { return { + getFlctsSttn: async () => { + const fclts = await getFetch(fetchType.WR, '/reuseFc/pubFc') + return set((state: pbnstPrfmncSttnState) => ({ + pbnstPrfmncSttn: { + ...state.pbnstPrfmncSttn, + fclts: fclts + }, + })); + }, tableHeadMaker: (startYear: number, endYear: number) => { let yearHeader: TableUpperProps[] = [...pbnstPrfmncSttnHeadListExport]; const selectList = [] as unknown as selectLabel[] @@ -77,10 +133,9 @@ export const pbnstPrfmncSttnActionsExport: (set: any) => pbnstPrfmncSttnActions if (yearHead) { yearHead.forEach((item: { id?: string; }) => { if (item.id) { + if (item.id === "year") { item.id = ""; return; } item.id = i + item.id; - } - }) if (yearHead[0]) yearHead[0].title = i + "년도" @@ -99,118 +154,144 @@ export const pbnstPrfmncSttnActionsExport: (set: any) => pbnstPrfmncSttnActions }; }); }, - pbnstPrfmncInit: async (page: string, sggOrCity: string) => { + // pbnstPrfmncInit: async (page: string, sggOrCity: string) => { + // try { + // const prfmncRes = await hasuListFetch(page, sggOrCity); + // return set((state: pbnstPrfmncSttnState) => ({ + // ...state, + // pbnstPrfmncSttn: { + // ...state.pbnstPrfmncSttn, + // prfmncRes: prfmncRes, + // isInit: true, + // }, + // })); + // } catch (e) { + // console.log(e) + // } + // }, + pbnstPrfmncSearch: async (page: number, returnType: ReturnType, fcltNm: string, fromDate: string, toDate: string, fclts: string[]) => { try { - const prfmncRes = await hasuListFetch(page, sggOrCity); + const prfmncRes = await hasuListFetch( + page, + returnType, + fcltNm, + toDate, + fclts); + // const bscFctCdList = prfmncRes.map((item: any) => { + // return item.bscFctCd; + // }) + const bscFctCdList = prfmncRes + .map((item: any) => item.bscFctCd) + .filter((value, index, self) => self.indexOf(value) === index); + // const bscFctCdList = [...new Set(prfmncRes.map((item: any) => item.bscFctCd))]; + + + const res = await postFectch( + `/reuseFc/pubDetailList/${fromDate}/${toDate}`, + bscFctCdList, + `wr`, + ) + let inUsageSum = 0; + let outUsageSum = 0; + let year = ""; + const yearsData = [] as any + + res.data.forEach((item: any) => { + if (item.prfList.length === 0) { + const year: string = ""; // 연도 정보가 없으면 빈 문자열, 필요시 item에서 추출. + const yearVal: { [key: string]: any } = { year: year, pbnstRuseFcltId: item.pbnstRuseFcltId ?? "", ruseUtztnFcltNm: item.ruseUtztnFcltNm ?? "" }; + yearVal[`${year}inUsage`] = 0; + yearVal[`${year}outUsage`] = 0; + yearVal[`${year}total`] = 0; + yearsData.push(yearVal); + } + item.prfList.forEach((acc) => { + year = acc.ym?.substring(0, 4) ?? ""; + inUsageSum = + (acc.wshwtr ?? 0) + //조경용수 + (acc.coolUswtr ?? 0) + //냉각수 + (acc.cleanUswtr ?? 0) + //청소수 + (acc.drwtUswtr ?? 0) + //식수대 + (acc.dltnUswtr ?? 0) + //희석용수 + (acc.ithlRswtr ?? 0) + //중수도 + (acc.ithlEtcUswtr ?? 0) //기타 + outUsageSum = + (acc.cleanToilUswtr ?? 0) + //청소화장실용수 + (acc.resn ?? 0) + //세척살수용도 + (acc.lndwtr ?? 0) + //조경용수 + (acc.hydUswtr ?? 0) + //친수용수 + (acc.ugwtChr ?? 0) + //지하수충전 + (acc.farmUswtr ?? 0) + //농업용수 + (acc.rvrKeepUswtr ?? 0) + //하천등유지용수 + (acc.indstUswtr ?? 0); //공업용수 + const yearData = yearsData.find( + (yearItem) => + yearItem.year === year && + yearItem.pbnstRuseFcltId == acc.pbnstRuseFcltId + ); + + if (yearData) { + yearData[`${year}inUsage`] += inUsageSum; + yearData[`${year}outUsage`] += outUsageSum; + yearData[`${year}total`] += (inUsageSum + outUsageSum); + } else { + const yearVal: { [key: string]: any } = { year: year, pbnstRuseFcltId: acc.pbnstRuseFcltId, ruseUtztnFcltNm: item.ruseUtztnFcltNm ?? "" }; + yearVal[`${year}inUsage`] = inUsageSum; + yearVal[`${year}outUsage`] = outUsageSum; + yearVal[`${year}total`] = inUsageSum + outUsageSum; + yearsData.push(yearVal); + } + }); + }); + + // 연도 필터링 + const from = Number(fromDate); + const to = Number(toDate); + + // 연도별 실적을 시설별로 병합 + const mergedMap: { [key: string]: any } = {}; + + yearsData.forEach((acc) => { + const facilityId = acc.pbnstRuseFcltId; + const year = acc.year; + + // 연도 필터 범위 안에만 포함 + if (!year || Number(year) < from || Number(year) > to) return; + + if (!mergedMap[facilityId]) { + mergedMap[facilityId] = { + pbnstRuseFcltId: facilityId, + ruseUtztnFcltNm: acc.ruseUtztnFcltNm, + }; + } + + // 해당 연도 데이터 추가 + mergedMap[facilityId][`${year}inUsage`] = acc[`${year}inUsage`] ?? 0; + mergedMap[facilityId][`${year}outUsage`] = acc[`${year}outUsage`] ?? 0; + mergedMap[facilityId][`${year}total`] = acc[`${year}total`] ?? 0; + }); + + // prfmncRes 정보 merge + const resData = Object.values(mergedMap).map((item: any) => { + const match = prfmncRes.find((res) => res.pbnstRuseFcltId === item.pbnstRuseFcltId); + return match ? { ...item, ...match } : item; + }); + return set((state: pbnstPrfmncSttnState) => ({ ...state, pbnstPrfmncSttn: { ...state.pbnstPrfmncSttn, - prfmncRes: prfmncRes, - isInit: true, + // prfmncRes: prfmncRes, + prfmncResCount: fclts.length - 5, + isInit: false, + prfmncRes: resData, + rawData: res.data }, })); - } catch (e) { - console.log(e) + } catch (errorHasu) { + console.log(errorHasu); } }, - pbnstPrfmncSearch: async (returnType: ReturnType, swtrmtFcltNm: string, fromDate: string, toDate: string) => { - let searchUrl = ""; - if (returnType.sigunValue !== "0") { - if (swtrmtFcltNm !== "") { - searchUrl = `/prcsFclt/info/1/20/${returnType.partValue}/${returnType.sigunValue}/0/1/SW${'/' + swtrmtFcltNm}`; - } else { - searchUrl = `/prcsFclt/info/1/20/${returnType.partValue}/${returnType.sigunValue}/0/1/SW`; - } - } else { - if (swtrmtFcltNm !== "") { - searchUrl = `/prcsFclt/info/1/20/${returnType.partValue}/${returnType.sidoValue}/0/1/SW${'/' + swtrmtFcltNm}`; - } else { - searchUrl = `/prcsFclt/info/1/20/${returnType.partValue}/${returnType.sidoValue}/0/1/SW`; - } - } - const pbnstRes = await getFetch( - fetchType.COMMON, - searchUrl - ); - - const bscFctCdList = pbnstRes.map((item: any) => { - return item.bscFctCd; - }) - const res = await postFectch( - `/reuseFc/pubDetailList/${fromDate}/${toDate}`, - bscFctCdList, - `wr`, - ) - let inUsageSum = 0; - let outUsageSum = 0; - let usageSum = 0; - let year = ""; - const yearsData = [] - - res.data.forEach((item) => { - if (item.prfList.length === 0) { - const year = ""; // 연도 정보가 없으면 빈 문자열, 필요시 item에서 추출 - const yearVal = { year: year, pbnstRuseFcltPk: item.pbnstRuseFcltPk ?? "", ruseUtztnFcltNm : item.ruseUtztnFcltNm ?? "" }; - yearVal[year + "inUsage"] = 0; - yearVal[year + "outUsage"] = 0; - yearVal[year + "total"] = 0; - yearsData.push(yearVal); - } - item.prfList.forEach((acc) => { - year = acc.ym?.substring(0, 4) ?? ""; - inUsageSum = - (acc.wshwtr ?? 0) + - (acc.coolUswtr ?? 0) + - (acc.cleanUswtr ?? 0) + - (acc.drwtUswtr ?? 0) + - (acc.dltnUswtr ?? 0) + - (acc.ithlRswtr ?? 0) + - (acc.ithlEtcUswtr ?? 0) + - (acc.cleanToilUswtr ?? 0) + - (acc.resn ?? 0); - outUsageSum = - (acc.lndwtr ?? 0) + - (acc.hydUswtr ?? 0) + - (acc.ugwtChr ?? 0) + - (acc.farmUswtr ?? 0) + - (acc.rvrKeepUswtr ?? 0) + - (acc.indstUswtr ?? 0); - const yearData = yearsData.find( - (yearItem) => - yearItem.year === year && - yearItem.pbnstRuseFcltPk == acc.pbnstRuseFcltPk - ); - - if (yearData) { - yearData[year + "inUsage"] += inUsageSum; - yearData[year + "outUsage"] += outUsageSum; - yearData[year + "total"] += (inUsageSum + outUsageSum); - } else { - const yearVal = { year: year, pbnstRuseFcltPk: acc.pbnstRuseFcltFk, ruseUtztnFcltNm : item.ruseUtztnFcltNm ?? "" }; - yearVal[year + "inUsage"] = inUsageSum; - yearVal[year + "outUsage"] = outUsageSum; - yearVal[year + "total"] = inUsageSum + outUsageSum; - yearsData.push(yearVal); - } - }); - }); - const resData: any[] = [] - pbnstRes.forEach((acc) => { - let data = yearsData.find((item) => item.pbnstRuseFcltPk.substring(0, 11) === acc.bscFctCd) - if (data) { - resData.push({ ...acc, ...data }) - } - }) - return set((state: pbnstPrfmncSttnState) => ({ - ...state, - pbnstPrfmncSttn: { - ...state.pbnstPrfmncSttn, - prfmncRes: resData, - rawData: res.data - }, - })); - } } } \ No newline at end of file diff --git a/apps/wr/app/store/ruseFcltMng/fcltReg/bdstReg/index.tsx b/apps/wr/app/store/ruseFcltMng/fcltReg/bdstReg/index.tsx index 931cd1305..1d9a0f6af 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltReg/bdstReg/index.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltReg/bdstReg/index.tsx @@ -11,14 +11,14 @@ import type { TableUpperProps } from "@/app/store/common/type/header"; { id: "bdstNm", title: "시설명" }, { id: "daddr", title: "위치" }, { id: "frstArchPrmsnDt", title: "최초건축허가일" }, - { id: "archArea", title: "건축면적", }, + { id: "archArea", title: "건축면적(㎡)", }, ]; const bdstChgHeadList: TableUpperProps[] = [ { id: "bdstChgNm", title: "구분" }, { id: "archChgPrmsnDt", title: "허가일" }, - { id: "archChgArea", title: "변경면적" }, + { id: "archChgArea", title: "변경면적(㎡)" }, ]; @@ -79,8 +79,9 @@ const isRswtBdst = (data: any): data is customRswtBdst =>{ data.reuseBuilding=chData; } - if(data?.chgBuilding.length>0){ - const chRgtr = data.chgBuilding.map((item) =>({...item, rgtr:TEST_UUID, mdfr: item.mdfr ?? null })) + + if('chgBuilding' in data && Array.isArray(data.chgBuilding) && data.chgBuilding.length > 0){ + const chRgtr = data.chgBuilding.map((item) =>({...item, rgtr:TEST_UUID, mdfr: item.mdfr ?? null })) data.chgBuilding =chRgtr; } return data; @@ -127,7 +128,7 @@ const isRswtBdst = (data: any): data is customRswtBdst =>{ data.reuseBuilding=chData; } - if(data?.chgBuilding.length>0){ + if('chgBuilding' in data && Array.isArray(data.chgBuilding) && data.chgBuilding.length > 0){ const chMdfr = data.chgBuilding.map((item) =>({...item, mdfr:TEST_UUID, archChgArea: item.archChgArea ?? '' })) data.chgBuilding =chMdfr; } diff --git a/apps/wr/app/store/ruseFcltMng/fcltReg/htdflReg/headList.tsx b/apps/wr/app/store/ruseFcltMng/fcltReg/htdflReg/headList.tsx new file mode 100644 index 000000000..4d0c228be --- /dev/null +++ b/apps/wr/app/store/ruseFcltMng/fcltReg/htdflReg/headList.tsx @@ -0,0 +1,10 @@ +import type { TableUpperProps } from "@/app/store/common/type/header"; + +export const htdTableHeadList: TableUpperProps[] = [ + { id: "cityText", title: "시도" }, + { id: "sggText", title: "시군구" }, + { id: "bdstNm", title: "시설명" }, + { id: "daddr", title: "위치" }, + { id: "frstArchPrmsnDt", title: "준공년도" }, + { id: "archArea", title: "건축면적(㎡)" }, +]; diff --git a/apps/wr/app/store/ruseFcltMng/fcltReg/htdflReg/index.tsx b/apps/wr/app/store/ruseFcltMng/fcltReg/htdflReg/index.tsx index 0a51ebb5d..add7030fa 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltReg/htdflReg/index.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltReg/htdflReg/index.tsx @@ -1,12 +1,67 @@ -import { getFetch, postFectch, putFectch } from "@/components/lib/api"; -import { upHeadList, type chgBuilding} from "../rainReg"; +import { getFetchWithCount, postFectch, putFectch } from "@/components/lib/api"; +import type { TableUpperProps } from "@/app/store/common/type/header"; +import toast from "react-hot-toast"; import { z } from "zod"; import { makeMdfr, makeRstr } from "../bdstReg"; -import type { TableUpperProps } from "@/app/store/common/type/header"; +import { + htdflReuseBuildingSchema, + validationMessages +} from "../rainReg/type"; // Import baseReuseBuildingSchema, chgBuildingSchema, validationMessages +import { htdTableHeadList } from "./headList"; + +const hdtflObjSchema = z.object({ + htdflFcltPk: z.number().nullish(), + bdstRuseFcltSn: z.number().nullish(), + etgtType: z + .string() + .min(1, { message: validationMessages.required("에너지원 종류") }), + fclCpct: z + .number() + .positive({ message: validationMessages.required("시설 용량") }), + plwtYn: z.enum(["Y", "N"]).default("Y"), + orgwtWtritSpt: z + .string() + .min(1, { message: validationMessages.required("원수 취수 지점") }), + orgwtOcrnProcs: z + .string() + .min(1, { message: validationMessages.required("원수 발생 공정") }), + wtritEntrcTp: z + .number() + .max(100, { message: "온도는 100이상이 될 수 없습니다." }) + .default(0), // Changed max to 100 based on message + amtgnr: z.number().default(0), + prcsProcsYn: z.enum(["Y", "N"]).default("Y"), + prcsFcltCpct: z + .number() + .positive({ message: validationMessages.required("처리시설 용량") }), + fmYn: z.string().default(""), + utztnCpct: z + .number() + .positive({ message: validationMessages.required("이용 용량") }), + utztnUsg: z + .string() + .min(1, { message: validationMessages.required("이용 용도") }), + grssClfg: z.number().default(0).nullish(), // Measurements, 0 is fine as default + combResdch: z.number().default(0).nullish(), // Measurements, 0 is fine as default + trbt: z.number().default(0).nullish(), // Measurements, 0 is fine as default + bod: z.number().default(0).nullish(), // Measurements, 0 is fine as default + smll:z.string().default('').nullish(), // Measurements, 0 is fine as default + crmty: z.number().default(0).nullish(), // Measurements, 0 is fine as default + tn: z.number().default(0).nullish(), // Measurements, 0 is fine as default + tph:z.number().default(0).nullish(),// Measurements, 0 is fine as default + ph: z.string().default('').nullish(), // Measurements, 0 is fine as default + clIonQnt: z.number().default(0).nullish(), // Measurements, 0 is fine as default + cdtnv: z.number().default(0).nullish(), // Measurements, 0 is fine as default + emsnExitTp: z.number() + .max(100, { message: "온도는 100이상이 될 수 없습니다." }) + .default(0), // Changed max to 100 based on message, // Measurements, 0 is fine as default + rgtr: z.string().nullish().default(null), + mdfr: z.string().nullish().default(null), +}); export const hdtflZod = z.object({ - bdstInfoPk: z.number().nullish(), + bdstInfoSn: z.number().nullish(), ctpvCd: z.number().default(0), sggCd: z.number().default(0), cityText: z.string().nullish(), @@ -14,138 +69,79 @@ export const hdtflZod = z.object({ daddr: z.string().default(" "), bdstNm: z.string().default(" "), frstArchPrmsnDt: z.date().default(() => new Date()), - archChgPrmsnDt: z.date().default(() => new Date()).nullish() , - archArea: z.number().default(0), + archChgPrmsnDt: z + .date() + .default(() => new Date()) + .nullish(), + archArea: z.string().default(''), archChgArea: z.number().nullish().default(0), - rgtr:z.string().nullish().default(null), - mdfr:z.string().nullish().default(null), + rgtr: z.string().nullish().default(null), + mdfr: z.string().nullish().default(null), reuseBuilding: z .array( - z.object({ - bdstRuseFcltPk: z.number().nullish(), - bdstInfoFk: z.number().nullish(), - instlCmptnDt: z.coerce.date().default(() => new Date()), - instlCst: z.number().nullish().default(0), - operMngCd: z.string().nullish().default(""), - anlOperCst: z.number().default(0), - instlCstSprtYn: z.enum(["Y", "N"]).nullish(), - instlCstSprtAmt: z.number().nullish().default(0), - grntchBdstCertNo: z.string().nullish().default(""), - grntchBdstCertGrd: z.string().nullish().default(""), - ruseFcltFlg: z.string().nullish().default(""), - bdstUsgCd: z.number().default(0), - lawTrgtYn: z.enum(["Y", "N"]).nullish(), - rgtr:z.string().nullish().default(null), - mdfr:z.string().nullish().default(null), - // htdflObj 매핑 - htdflObj: z - .object({ - htdflFcltPk: z.number().nullish(), - bdstRuseFcltFk: z.number().nullish(), - etgtType: z.string().default(""), - fclCpct: z.number().default(0), - plwtYn: z.enum(["Y", "N"]), - orgwtWtritSpt: z.number().default(0), - orgwtOcrnProcs: z.string().default(""), - wtritEntrcTp: z.number().max(4, "온도는 100이상이 될 수 없습니다.") - .default(0), - amtgnr: z.number().default(0), - prcsProcsYn: z.enum(["Y", "N"]), - prcsFcltCpct: z.number().default(0), - fmYn: z.string().default(""), - utztnCpct: z.number().default(0), - utztnUsg: z.string().default(""), - grssClfg: z.number().default(0), - combResdch: z.number().default(0), - trbt: z.number().default(0), - bod: z.number().default(0), - smll: z.number().default(0), - crmty: z.number().default(0), - tn: z.number().default(0), - tph: z.number().default(0), - ph: z.number().default(0), - clIonQnt: z.number().default(0), - cdtnv: z.number().default(0), - emsnExitTp: z.number().default(0), - rgtr:z.string().nullish().default(null), - mdfr:z.string().nullish().default(null), - }) - .default({ - plwtYn: "N", - prcsProcsYn: "N", - }), - }) - ) - .default([]), - chgBuilding: z - .array( - z.object({ - bdstInfoFk: z.number().nullish(), - archChgPrmsnDt: z.coerce.date() , - archChgArea: z.string().default("-").nullish(), - bdstChgNm: z.string().default(""), - rgtr:z.string().nullish().default(null), - mdfr:z.string().nullish().default(null), + htdflReuseBuildingSchema.extend({ + htdflObj: hdtflObjSchema, }) ) .default([]), + //온배수는 건축물은 등록 하는것이 다름 + // chgBuilding: z.array(chgBuildingSchema).nullish(), // Use imported chgBuildingSchema }); - - export type customHtdflBdst = z.infer<typeof hdtflZod> - - +export type customHtdflBdst = z.infer<typeof hdtflZod>; export type htdflFacState = { htdfl: { isInit: boolean; upHeadList: TableUpperProps[]; htdflRes: customHtdflBdst[]; + htdflResCount:number }; }; type htdflFacSActions = { - initHtdfl: (code: string ) => void; - htdflBdstUpdate:(data: customHtdflBdst, code:string|number) => void; - htdflBdstReg:(data: customHtdflBdst, code:string|number) => void; - getHtdflBdstData:(pageNation:number, code:string|number) =>void; - + initHtdflReg: (code: string) => void; + htdflNmSearch: (code: string | number, fcltNm:string) => void; + htdflBdstUpdate: (data: customHtdflBdst, code: string | number) => void; + htdflBdstReg: (data: customHtdflBdst, code: string | number) => void; + getHtdflBdstData: (pageNation: number, code: string | number) => void; }; const htdflRes: customHtdflBdst[] = []; export type htdflFacType = htdflFacState & htdflFacSActions; - export const htdflFacStateExport: htdflFacState = { htdfl: { isInit: false, - upHeadList: upHeadList, + upHeadList: htdTableHeadList, htdflRes: htdflRes, + htdflResCount:1, }, }; +const initFetch = async (code: string) => { -const initFetch = async (code:string|number) => { - const rswtRes = await getFetch( - "wr", - `/facilityList/1/20/${code}`) + let url = `/drain/facilityList/1/11` + url+=`/${code}` + + + const rswtRes = await getFetchWithCount("wr", url); return rswtRes; }; export const htdflFacActionsExport: (set: any) => htdflFacSActions = ( set: any ) => { return { - getHtdflBdstData: async (pageNation: number, code: string | number) => { + getHtdflBdstData: async (pageNation: number, code: string | number) => { try { - const rswtRes = await getFetch( + const htdflRes = await getFetchWithCount( "wr", - `/drain/facilityList/${pageNation}/20/${code}` + `/drain/facilityList/${pageNation}/11/${code}` ); - console.log(pageNation) - // 여기서 상태 업데이트 return set((state: htdflFacState) => ({ ...state, htdfl: { ...state.htdfl, - htdflRes: rswtRes, + htdflRes: htdflRes.data, + htdflResCount:htdflRes.count, isInit: false, }, })); @@ -153,17 +149,16 @@ export const htdflFacActionsExport: (set: any) => htdflFacSActions = ( console.log(e); } }, - - - initHtdfl: async (code: string ) => { + initHtdflReg: async (code: string) => { try { - const htdflRes = await initFetch(code); + const htdflRes = await initFetch( String(code)); set((state: htdflFacState) => { return { ...state, htdfl: { ...state.htdfl, - htdflRes: htdflRes, + htdflRes: htdflRes.data, + htdflResCount:htdflRes.count, isInit: false, }, }; @@ -172,73 +167,73 @@ export const htdflFacActionsExport: (set: any) => htdflFacSActions = ( console.log(error); } }, - htdflBdstUpdate: async (data: customHtdflBdst, code:string|number) => { + htdflBdstUpdate: async (data: customHtdflBdst, code: string | number) => { try { - const chData=await makeMdfr(data) - const res = await putFectch("/drain/facility", data); - if (res.ok) { - alert(" 수정이 완료되었습니다."); + const chData = await makeMdfr(data); + const res = await putFectch("/drain/facility", chData); + if (res.status) { + toast.success("온배수 시설물 수정이 완료되었습니다."); } - - const htdflRes = await initFetch(code); + const htdflRes = await getFetchWithCount( + "wr", + `/drain/facilityList/1/11/${code}` + ); set((state: htdflFacState) => ({ ...state, htdfl: { ...state.htdfl, - htdflRes: htdflRes, + htdflRes: htdflRes.data, + htdflResCount:htdflRes.count, isInit: false, }, })); } catch (errorUpdate) { - alert( - "업데이트 오류입니다. " + - (errorUpdate instanceof Error ? errorUpdate.message : "") - ); + toast.error("온배수 시설물 수정 오류입니다.."); + console.log(errorUpdate); } }, - htdflBdstReg: async (data: customHtdflBdst , code:string|number) => { + htdflBdstReg: async (data: customHtdflBdst, code: string | number) => { try { - const chData=await makeRstr(data) - const res = await postFectch("/drain/facility", data); - if (res.ok) { - - if(data.chgBuilding.length > 0){ - await res - .json() - .then((resData: any) => { - const chPK = resData?.data?.bdstInfoPk; - - const makeCh = resData.data.chgBuilding.map( - (item: chgBuilding) => ({ - ...item, - bdstInfoFk: chPK, - }) - ); - const chch = postFectch("/reuseFc/chgBuilding", makeCh); - return chch; - }) - .then((chch: any) => { - if (chch.ok) { - alert("등록이 완료되었습니다."); - } - }); - }else{ - alert("등록이 완료되었습니다."); - - } + // data는 단일 객체이므로 .map을 사용할 수 없습니다. + // 등록 시에는 건축물 변경 이력이 없으므로 chgBuilding을 빈 배열로 설정하고, + // API가 배열을 기대하므로 단일 객체를 배열로 감싸줍니다. + const chData = makeRstr(data); + const res = await postFectch("/drain/facility", chData); + if (res.status ===200) { + toast.success("온배수 시설 등록이 완료되었습니다."); } - const htdflRes = await initFetch(code); + const htdflRes = await initFetch(String(code)); set((state: htdflFacState) => ({ ...state, htdfl: { ...state.htdfl, - htdflRes: htdflRes, + htdflRes: htdflRes.data, + htdflResCount:htdflRes.count, isInit: false, }, })); } catch (errorReg) { - alert("등록 오류입니다."); - } + toast.error("온배수 시설물 등록 오류입니다.."); + console.log(errorReg); + + } + },htdflNmSearch :async(code:string | number, fcltNm:string) =>{ + try { + const htdflRes = await getFetchWithCount("wr", `/drain/facilityList/1/11/${code}/${fcltNm}`); + set((state: htdflFacState) => { + return { + ...state, + htdfl: { + ...state.htdfl, + htdflRes: htdflRes.data, + htdflResCount:htdflRes.count, + isInit: false, + }, + }; + }); + } catch (error) { + console.log(error); + } }, }; }; diff --git a/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/headList.tsx b/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/headList.tsx index 536114f3f..5d77c64ca 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/headList.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/headList.tsx @@ -1,3 +1,4 @@ +import { numSize, txtSize } from "@/app/store/common/code/basicCode"; import type { TableUpperProps } from "@/app/store/common/type/header"; import type { chHeadType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList"; @@ -7,15 +8,20 @@ export const pubFacHeadListExport: TableUpperProps[] = [ { id: "bscFctNm", title: "시설명" }, { id: "addr", title: "위치" }, { id: "efflWqltStndrdNm", title: "지역구분" }, - { id: "fctCpc", title: "시설용량" }, + { id: "fctCpc", title: "시설용량(m³/일)" }, { id: "ruse", title: "재이용여부" }, { id: "rswtrYn", title: "등록여부" }, ]; export const pbnstMngUpHeadListExport: TableUpperProps[] = [ { id: "choose", title: "선택", type: "checkbox" }, - { id: "operMthd", title: "운영방식",type: "select", selectData: "ruseMngMtd" }, - { id: "rprtvBzenty", title: "대행업체", edit: true }, + { + id: "operMthd", + title: "운영방식", + type: "select", + selectData: "ruseMngMtd", + }, + { id: "rprtvBzenty", title: "대행업체", edit: true, maxLength: 10 }, { id: "bgngDt", title: "시작일", @@ -31,8 +37,8 @@ export const pbnstMngUpHeadListExport: TableUpperProps[] = [ export const pplInstlsHeadListExport: TableUpperProps[] = [ { id: "choose", title: "선택", type: "checkbox" }, { id: "ppMqlt", title: "관재질", type: "select", selectData: "pplQulity" }, - { id: "pdmt", title: "관경", type: "select", selectData: "pplLenght" }, - { id: "cpct", title: "연장", edit: true, type: "number" }, + { id: "pdmt", title: "관경(mm)", type: "select", selectData: "pplLenght" }, + { id: "cpct", title: "연장(m)", edit: true, type: "number" }, ]; export const pbnstDemandHeadListExport: TableUpperProps[] = [ @@ -47,10 +53,10 @@ export const pbnstDemandHeadListExport: TableUpperProps[] = [ export const pbnstOrgwtHeadListRxport: TableUpperProps[] = [ { id: "ruseUtztnFcltNm", title: "하수처리장명" }, - { id: "orgwtWtritQnt", title: "원수취수량" }, + { id: "orgwtWtritQnt", title: "원수취수량(m³/일)", type: "number" }, { id: "tmsYn", title: "하수처리량TMS 전후" }, { id: "dschgFmYn", title: "하수처리장 방류 유량계 전·후​" }, - { id: "dtlPstnExpln", title: "상세위치 설명" }, + { id: "dtlPstnExpln", title: "상세위치", edit: true }, ]; export const fcOrgwtsHeadListExport: TableUpperProps[] = [ { id: "choose", title: "선택", type: "checkbox" }, @@ -60,7 +66,6 @@ export const fcOrgwtsHeadListExport: TableUpperProps[] = [ { id: "addr", title: "위치" }, ]; - export const pbnstMngNmExport: chHeadType[] = [ { headNm: "upChangeRe", text: "운영대행기간", length: 2 }, ]; @@ -69,64 +74,91 @@ export type formDetailType = { id: string; lable: string; type?: string; - option?:string + maxLenght?: number; + selectData?: string; // select box일 경우 + option?: string; }; export const wasteWaterFcltINfo: formDetailType[] = [ - { id: "wwtEmsnFcltPrmsnYn", lable: "폐수배출물신고 허가여부", type: "selectYn" }, - { id: "tmsInstlYn", lable: "Tms설치여부", type: "selectYn" }, - { id: "pbnstDrngInflwExclYn", lable: "공공하수도유입제외 허가여부", type: "selectYn" }, + { + id: "wwtEmsnFcltPrmsnYn", + lable: "폐수배출물신고 허가여부", + type: "select", + selectData: "yn", + }, + { id: "tmsInstlYn", lable: "Tms설치여부", type: "select", selectData: "yn" }, + { + id: "pbnstDrngInflwExclYn", + lable: "공공하수도유입제외 허가여부", + type: "select", + selectData: "yn", + }, ]; export const concentratedWaterInfo: formDetailType[] = [ - { id: "erctWtrOcrnCpct", lable: "눙축수 발생량(m³/일)", type: "number" }, - { id: "erctUswtrLinkSpt", lable: "눙축수연계지점" }, - { id: "erctUswtrFcltCpct", lable: "시설용량(m³/일)", type: "number" }, - { id: "erctUswtrPrcsMeth", lable: "눙축수처리방식" }, - { id: "erctUswtrPrcsMsrcCd", lable: "눙축수처리공법" }, + { id: "erctWtrOcrnCpct", lable: "눙축수 발생량(m³/일)", type: "number",maxLenght:numSize }, + { id: "erctUswtrLinkSpt", lable: "눙축수연계지점", maxLenght:txtSize }, + { id: "erctUswtrFcltCpct", lable: "시설용량(m³/일)", type: "number" ,maxLenght:numSize }, + { + id: "erctUswtrPrcsMeth", + lable: "눙축수처리방식", + type: "select", + selectData: "erctUswtrPrcsMeth", + }, + { id: "erctUswtrPrcsMsrcCd", lable: "눙축수처리공법" ,maxLenght:txtSize}, ]; export const ruseFcltinfo: formDetailType[] = [ - { id: "rprcsFcltCpct", lable: "시설용량(m³/일)", type: "number" }, - { id: "majorPrcsMsrcCd", lable: "주처리공법" }, - { id: "addr", lable: "위치(주소)" }, - { id: "rprcsMthdCd", lable: "재처리방식" }, - { id: "inoutYn", lable: "위치(처리장 내외)" ,type: "selectYn" }, // 머가 내? 외?, - { id: "mainSplyPumpCpct", lable: "메인공급펌프 용량", type: "number" }, + { id: "rprcsFcltCpct", lable: "시설용량(m³/일)", type: "number", maxLenght:numSize }, + { id: "majorPrcsMsrcCd", lable: "주처리공법", maxLenght:txtSize }, + { id: "addr", lable: "위치(주소)", maxLenght:txtSize}, + { + id: "rprcsMthdCd", + lable: "재처리방식", + type: "select", + selectData: "rusePrcsMthdCd", + + }, + { + id: "inoutYn", + lable: "위치(처리장 내외)", + type: "select", + selectData: "yn", + }, // 머가 내? 외?, + { id: "mainSplyPumpCpct", lable: "메인공급펌프 용량(m³/일)", type: "number" , maxLenght:numSize}, ]; export const businessMoneyInfo: formDetailType[] = [ - { id: "gvsbs", lable: "국고", type: "number" }, - { id: "lcep", lable: "지방비", type: "number" }, - { id: "cusrAlotm", lable: "민간투자비", type: "number" }, - { id: "etc", lable: "기타" }, - { id: "totalMoney", lable: "합계", type: "number" ,option:"readOnly" }, + { id: "gvsbs", lable: "국고", type: "number" , maxLenght:numSize}, + { id: "lcep", lable: "지방비", type: "number" , maxLenght:numSize}, + { id: "cusrAlotm", lable: "민간투자비", type: "number" , maxLenght:numSize}, + { id: "etc", lable: "기타" , maxLenght:numSize}, + { id: "totalMoney", lable: "합계", type: "number", option: "disabled" }, ]; export const businessSttnInfo: formDetailType[] = [ - { id: "seCd", lable: "구분" }, - { id: "bizMthdCd", lable: "사업방식" }, + { id: "seCd", lable: "구분", type: "select", selectData: "ruseSeCd" }, + { + id: "bizMthdCd", + lable: "사업방식", + type: "select", + selectData: "ruseBizMthdCd", + }, { id: "bizCmcnDay", lable: "사업준공일", type: "date" }, { id: "instlAprvSncnDt", lable: "설치승인인가일", type: "date" }, ]; -export const ruseFcltIntakeList: formDetailType[] = [ - { id: "seCd", lable: "구분" }, - { id: "bizMthdCd", lable: "사업방식" }, - { id: "bizCmcnDay", lable: "사업준공일", type: "date" }, - { id: "instlAprvSncnDt", lable: "설치승인인가일", type: "date" }, -]; export const ruseWaterQulityList: formDetailType[] = [ - { id: "bod", lable: "BOD(mg/L)", type: "number" }, - { id: "grssClfg", lable: "총대장균군수(개/mg)", type: "number" }, - { id: "combResdch", lable: "결합잔류염소(mh/L)", type: "number" }, - { id: "trbt", lable: "탁도(NTU)", type: "number" }, - { id: "smll", lable: "냄새", type: "number" }, - { id: "crmty", lable: "색도", type: "number" }, - { id: "tn", lable: "총질소(mg/L)", type: "number" }, - { id: "tph", lable: "총인(mg/L)", type: "number" }, - { id: "ph", lable: "수소이온농도", type: "number" }, - { id: "clIonQnt", lable: "염화물(mg/L)", type: "number" }, - { id: "cdtnv", lable: "전기전도도(μs/cm)​", type: "number" }, + { id: "bod", lable: "BOD(mg/L)", type: "text", maxLenght:numSize }, + { id: "grssClfg", lable: "총대장균군수(개/mg)", type: "text" , maxLenght:numSize}, + { id: "combResdch", lable: "결합잔류염소(mh/L)", type: "text" , maxLenght:numSize}, + { id: "trbt", lable: "탁도(NTU)", type: "text" , maxLenght:numSize}, + { id: "smll", lable: "냄새", type: "text", maxLenght:numSize }, + { id: "crmty", lable: "색도", type: "text", maxLenght:numSize }, + { id: "tn", lable: "총질소(mg/L)", type: "text", maxLenght:numSize }, + { id: "tph", lable: "총인(mg/L)", type: "text" , maxLenght:numSize}, + { id: "ph", lable: "수소이온농도", type: "text", maxLenght:numSize}, + { id: "clIonQnt", lable: "염화물(mg/L)", type: "text", maxLenght:numSize }, + { id: "cdtnv", lable: "전기전도도(μs/cm)​", type: "text", maxLenght:numSize }, ]; diff --git a/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/index.tsx b/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/index.tsx index fb799c26c..d138a8dfe 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/index.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/index.tsx @@ -52,24 +52,24 @@ export type pubFacState = { fcOrgwtsHeadList: TableUpperProps[]; pbnstMngRegData: customPbnst[]; pbnsUpdateIndex: pbnstInsertIndex; + fcOrgwtsNmList:pubHasuType[]; dmdComList: any[]; + apiMessage:string; + pbnstHasuResCount:number }; }; type pubFacActions = { pubHasuInit: (page: string, sggOrCity: string) => void; pbnstRegClick: (pubHasuRes: pubHasuType) => void; - pbnstReg: (txt: string | undefined) => void; + //pbnstReg: (txt: string | undefined) => void; pbnstInsert(pbnstRes: customPbnst): Promise<void>; appendRuseFclt(array: customPbnst[]): void; fileDelete: (atflCd: string) => void; fileDownLoad: (atflCd: string) => void; - //pbnstMngData: (data: customPbnst[]) => void; - //pbnstBizData(data: fcBizType): void; - // getBizCycle: (pbnstObj: customPbnst) => void; - getpbnstInsertIndex: (data: pbnstInsertIndex) => void; pbnstRuseFcltUpdate: (ruseRes: customPbnst[]) => void; pbnstRuseBizUpdate: (ruseRes: fcBizType[]) => void; pubHasuSearch :(page: string, sggOrCity: string , searchTxt:string) => void + getFcOrgwts: (fcltPkList: string[], size:number) => void; }; export type pubFacType = pubFacState & pubFacActions; @@ -90,7 +90,10 @@ export const pubFacStateExport: pubFacState = { pbnstBizCycle: [{ text: "없음", val: "1" }], pbnstMngRegData: [], pbnsUpdateIndex: {} as pbnstInsertIndex, + fcOrgwtsNmList:[], dmdComList: [], + apiMessage:'', + pbnstHasuResCount:1 }, }; @@ -102,12 +105,10 @@ const checkWrRegList = async () => { const hasuListFetch = async (page: string, sggOrCity: string, searchTxt?:string) => { const checkWrRegListRes = await checkWrRegList(); - console.log(' checkWrRegListRes ', checkWrRegListRes) -let url = `/prcsFclt/ruseInfo/${page ? page : 1}/10/A/${sggOrCity? sggOrCity : "11"}/0/1/SW`; +let url = `/prcsFclt/ruseInfo/${page ? page : 1}/11/A/${sggOrCity? sggOrCity : "11"}/0/1/SW`; const data = checkWrRegListRes.filter((item:string) => item !== "") - console.log(' checkWrRegListRes ' ) if(searchTxt){ url= url +`/${searchTxt}` @@ -121,11 +122,12 @@ if(searchTxt){ const hasuList = rainRes.data.map((item: any) => { return { ...item, + fctCpc: Number(item.fctCpc) || 0, ruse: item.ruse === false ? "N" : "Y", }; }); - return hasuList; + return {data: hasuList, count:rainRes.count}; }; const initFetch = async (ruseFcltPk: string | undefined) => { @@ -176,12 +178,12 @@ export const pubFacActionsExport: (set: any) => pubFacActions = (set: any) => { pubHasuInit: async (page: string, sggOrCity: string) => { try { const pbnstHasuRes = await hasuListFetch(page, sggOrCity); - return set((state: pubFacState) => ({ ...state, pbnst: { ...state.pbnst, - pbnstHasuRes: pbnstHasuRes, + pbnstHasuRes: pbnstHasuRes.data, + pbnstHasuResCount:pbnstHasuRes.count, isInit: false, }, })); @@ -196,7 +198,8 @@ export const pubFacActionsExport: (set: any) => pubFacActions = (set: any) => { ...state, pbnst: { ...state.pbnst, - pbnstHasuRes: pbnstHasuRes, + pbnstHasuRes: pbnstHasuRes.data, + pbnstHasuResCount:pbnstHasuRes.count, isInit: false, }, })); @@ -219,22 +222,6 @@ export const pubFacActionsExport: (set: any) => pubFacActions = (set: any) => { console.log(errorHasu); } }, - - pbnstReg: async (txt: string | undefined) => { - try { - const pbnstRes = await hasuListFetch("11", "11"); - return set((state: pubFacState) => ({ - ...state, - pbnst: { - ...state.pbnst, - pbnstRes: pbnstRes, - isInit: false, - }, - })); - } catch (errorHasu) { - console.log(errorHasu); - } - }, pbnstInsert: async (pbnstRes: customPbnst) => { try { @@ -246,13 +233,12 @@ export const pubFacActionsExport: (set: any) => pubFacActions = (set: any) => { toast.success(`${pbnstRes.ruseUtztnFcltNm} 하수재이용 시설이 등록되었습니다.`); } const pbnstHasuRes = await hasuListFetch('1', '0'); - - return set((state: pubFacState) => ({ ...state, pbnst: { ...state.pbnst, - pbnstRes: pbnstHasuRes, + pbnstHasuRes: pbnstHasuRes.data, + pbnstHasuResCount:pbnstHasuRes.count, isInit: false, }, })); @@ -262,32 +248,22 @@ export const pubFacActionsExport: (set: any) => pubFacActions = (set: any) => { },pbnstRuseFcltUpdate : async(ruseRes:customPbnst[])=>{ try { - const response = await putFectch( + await putFectch( `/reuseFc/pubFc`, ruseRes ); - + } catch (error) { console.error("Error pbnstRuseFcltUpdate:", error); } } ,pbnstRuseBizUpdate : async(ruseRes:fcBizType[])=>{ try { - console.log('ruseRes ', ruseRes) - const response = await putFectch( + await putFectch( `/reuseFc/pubFcBiz`, ruseRes ); - const pbnstRes = await hasuListFetch("11", "11"); - return set((state: pubFacState) => ({ - ...state, - pbnst: { - ...state.pbnst, - pbnstRes: pbnstRes, - isInit: false, - }, - })); } catch (error) { console.error("Error pbnstRuseFcltUpdate:", error); } @@ -332,60 +308,17 @@ export const pubFacActionsExport: (set: any) => pubFacActions = (set: any) => { }, fileDownLoad: async (atflCd: string) => { try { - const response = await getFetch( + await getFetch( fetchType.CS, `/attach-service/api/v1/atflcd/download/${atflCd}` ); - - if (!response.ok) { - throw new Error(`Error downloading file: ${response.statusText}`); - } } catch (error) { console.error("Error downloading file:", error); + throw new Error(`Error downloading file: ${error}`); + } }, - - // pbnstMngData: (data: customPbnst[]) => { - // if (data.length > 0) { - // } - // return set((state: pubFacState) => { - // state.pbnst.pbnstMngRegData = data; - // state.pbnst.pbnstBizCycle = - // data.length <= 0 - // ? [{ text: "없음", val: "1" }] - // : state.pbnst.pbnstBizCycle; - // return state; - // }); - // }, - - // pbnstBizData: (data: fcBizType) => { - // return set((state: pubFacState) => { - // if (state.pbnst.pbnstMngRegData) { - // const regData = state.pbnst.pbnstMngRegData.find( - // (item) => data.pbnstRuseFcltFk === item.pbnstRuseFcltPk - // ); - // if (regData) { - // const bizIdx = regData.fcBizs.findIndex( - // (item) => item.pbnstRuseFcltBizPk == data.pbnstRuseFcltBizPk - // ); - - // if (bizIdx >= 0) { - // regData.fcBizs[bizIdx] = data; - // const pbnstData = state.pbnst.pbnstMngRegData - // state.pbnst.pbnstMngRegData= [...pbnstData] - - // } - // } - // } - // return state; - // }); - // }, - getpbnstInsertIndex: async (data: pbnstInsertIndex) => { - return set((state: pubFacState) => { - state.pbnst.pbnsUpdateIndex = data; - return state; - }); - }, + appendRuseFclt: async (array: customPbnst[]) => { try { return set((state: pubFacState) => ({ @@ -399,6 +332,22 @@ export const pubFacActionsExport: (set: any) => pubFacActions = (set: any) => { } catch (e) { console.log(e); } + },getFcOrgwts : async (fcltPkList: string[] ,size:number) => { + + const fcOrgwtsNmList = await postFectch( + `/prcsFclt/ruseInfo/1/${size}/A/0/0/1/SW`, + fcltPkList, + fetchType.COMMON + ); + + return set((state: pubFacState) => ({ + ...state, + pbnst: { + ...state.pbnst, + fcOrgwtsNmList:fcOrgwtsNmList.data??[] + }, + })); } + }; }; diff --git a/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/type.tsx b/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/type.tsx index a34985810..f8bb81b88 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/type.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/type.tsx @@ -1,5 +1,4 @@ import { z } from "zod"; -import { TEST_UUID } from "../rainReg"; export type customPbnst = z.infer<typeof pbnstRegZod>; @@ -14,10 +13,10 @@ export type selectLabel = { val: string; }; -export type pbnstInsertIndex ={ - mng:number, - biz:number -} +export type pbnstInsertIndex = { + mng: number; + biz: number; +}; export type publicReuseFacilityType = { sido: string; @@ -31,12 +30,12 @@ export type publicReuseFacilityType = { area?: string; reuse?: string; register?: string; - swtrmtFcltFk: string; + swtrmtFcltId: string; }; export type ruseFcltType = { - swtrmtFcltFk: string; - pbnstRuseFcltPk: string; + swtrmtFcltId: string; + pbnstRuseFcltId: string; fcltNm: string; fcltCpct: number; swtrmtCpct: number; @@ -45,22 +44,22 @@ export type ruseFcltType = { }; export type PbnstDemandComType = { - dmdInfoPk : string | ''; - bzmnNo : string | ''; - dmdCoNm : string | ''; - daddr : string | ''; - telno : string | ''; - rgtr : string | ''; - useUsgCd : string | ''; - ctpvCd : string | ''; - sggCd : string | ''; - dmdAplyPswd: string | ''; + dmdInfoId: string | ""; + bzmnNo: string | ""; + dmdCoNm: string | ""; + daddr: string | ""; + telno: string | ""; + rgtr: string | ""; + useUsgCd: string | ""; + ctpvCd: string | ""; + sggCd: string | ""; + dmdAplyPswd: string | ""; splyCpct: number | 0; - splyFmYn:string|'Y' -// dmdInfoFk:string|'' -} + splyFmYn: string | "Y"; + // dmdInfoId:string|'' +}; -const pubHasuZod = z.object({ +export const pubHasuZod = z.object({ choose: z.boolean().default(false), bscFctCd: z.string(), sido: z.string().default(""), @@ -68,29 +67,33 @@ const pubHasuZod = z.object({ bscFctNm: z.string().default(""), efflWqltStndrdNm: z.string().default(""), fctCpc: z.number().default(0), - addr:z.string().nullish() + addr: z.string().nullish(), }); - export type pubHasuType = z.infer<typeof pubHasuZod>; -const zDate = z.preprocess((v) => (v ? new Date(v as string) : undefined), z.date()).nullish() ; +const zDate = z + .preprocess((v) => (v ? new Date(v as string) : undefined), z.date()) + .nullish(); const zDateNullish = zDate.nullish(); const zDateDefaultNow = zDate.default(new Date()); -const zNum = z.preprocess((v) => (v !== null && v !== undefined ? Number(v) : v), z.number()); +const zNum = z.preprocess( + (v) => (v !== null && v !== undefined ? Number(v) : v), + z.number() +); const zNumNullish = zNum.nullish(); const zNumDefaultZero = zNum.default(0).nullish(); const pplInstlSchema = z.object({ - ruseFcltBizFk: zNumNullish, + pbnstRuseFcltBizSn: zNumNullish, choose: z.boolean().default(false), - pdmt: z.string().default(''), + pdmt: z.string().default(""), ppMqlt: z.string().default(""), cpct: zNumDefaultZero, }); const fcFileSchema = z.object({ - ruseFcltFk: z.string().nullish(), + pbnstRuseFcltId: z.string().nullish(), fileTkn: z.string(), filePath: z.string(), fileKnd: z.string(), @@ -101,8 +104,8 @@ const fcFileSchema = z.object({ }); export const fcOperSchema = z.object({ - pbnstRuseFcltOperInfoPk: zNumNullish, - pbnstRuseFcltFk: z.string().nullish(), + pbnstRuseFcltOperInfoSn: zNumNullish, + pbnstRuseFcltId: z.string().nullish(), operMthd: z.string(), rprtvBzenty: z.string(), bgngDt: zDateDefaultNow, @@ -115,22 +118,23 @@ export const fcOperSchema = z.object({ }); const fcOrgwtSchema = z.object({ - pbnstRuseFcltBizFk:zNumNullish, - swtrmtFcltFk: z.string().nullish(), + pbnstRuseFcltBizSn: zNumNullish, + swtrmtFcltId: z.string().nullish(), orgwtWtritQnt: zNum, tmsYn: z.enum(["Y", "N"]).default("N"), dschgFmYn: z.enum(["Y", "N"]).default("N"), - dtlPstnExpln: z.string().default(''), + dtlPstnExpln: z.string().default(""), regDt: zDateNullish, - rgtr: z.string().default(''), + rgtr: z.string().default(""), mdfcnDt: zDateNullish, - mdfr: z.string().default(''), + mdfr: z.string().default(""), choose: z.boolean().default(false), + ruseUtztnFcltNm:z.string().nullish() }); const dmdColist = z.object({ - dmdInfoPk: z.string().default(''), - dmdCoNm: z.string().default(''), + dmdInfoId: z.string().default(""), + dmdCoNm: z.string().default(""), daddr: z.string(), useUsgCd: z.string().nullish(), splyBgngDay: zDateDefaultNow, @@ -139,27 +143,26 @@ const dmdColist = z.object({ splyFmYn: z.string().nullish(), rgtr: z.string().nullish(), mdfr: z.string().nullish(), - pbnstRuseFcltBizPk: zNumDefaultZero, - pbnstRuseFcltFk: z.string().nullish(), + pbnstRuseFcltBizSn: zNumDefaultZero, + pbnstRuseFcltId: z.string().nullish(), }); export const fcBizSchema = z.object({ - pbnstRuseFcltBizPk: zNumNullish, - pbnstRuseFcltFk: z.string().nullish(), - superkey: z.string().default("").nullish(), + pbnstRuseFcltBizSn: zNumNullish, + pbnstRuseFcltId: z.string().nullish(), cycl: zNum.default(1), - seCd: z.string().default(""), + seCd: z.string().max(10, "구분은 10자를 초과할 수 없습니다.").default(""), bizMthdCd: z.string().default(""), instlAprvSncnDt: zDateDefaultNow, bizCmcnDay: zDateDefaultNow, gvsbs: zNumDefaultZero, lcep: zNumDefaultZero, cusrAlotm: zNumDefaultZero, - etc: z.string().default(''), + etc: z.string().default(""), totalMoney: zNumNullish.default(0), rprcsFcltCpct: zNumDefaultZero, - majorPrcsMsrcCd: z.string().default(""), - addr: z.string().default(''), + majorPrcsMsrcCd: z.string().nullish(), + addr: z.string().default(""), rprcsMthdCd: z.string().default(""), inoutYn: z.enum(["Y", "N"]).default("Y"), mainSplyPumpCpct: zNumNullish, @@ -171,17 +174,29 @@ export const fcBizSchema = z.object({ wwtEmsnFcltPrmsnYn: z.string().default("Y"), tmsInstlYn: z.string().default("Y"), pbnstDrngInflwExclYn: z.string().default("Y"), - bod: zNumDefaultZero, - grssClfg: zNumDefaultZero, - combResdch: zNumDefaultZero, - trbt: zNumDefaultZero, - clIonQnt: zNumDefaultZero, - smll: zNumDefaultZero, - crmty: zNumDefaultZero, - tph: zNumDefaultZero, - tn: zNumDefaultZero, - ph: zNumDefaultZero, - cdtnv: zNumDefaultZero, + // bod: z.preprocess((v) => (v !== null && v !== undefined ? Number(v) : v), z.number().max(9999, "BOD 값은 5자리 미만이어야 합니다.")).nullish(), + // grssClfg: z.preprocess((v) => (v !== null && v !== undefined ? Number(v) : v), z.number().max(9999, "총대장균군 값은 5자리 미만이어야 합니다.")).nullish(), + // combResdch: z.preprocess((v) => (v !== null && v !== undefined ? Number(v) : v), z.number().max(9999, "결합잔류염소 값은 5자리 미만이어야 합니다.")).nullish(), + // trbt: z.preprocess((v) => (v !== null && v !== undefined ? Number(v) : v), z.number().max(9999, "탁도 값은 5자리 미만이어야 합니다.")).nullish(), + // clIonQnt: z.preprocess((v) => (v !== null && v !== undefined ? Number(v) : v), z.number().max(9999, "염소이온량 값은 5자리 미만이어야 합니다.")).nullish(), + // smll: z.preprocess((v) => (v !== null && v !== undefined ? Number(v) : v), z.number().max(9999, "냄새 값은 5자리 미만이어야 합니다.")).nullish(), + // crmty: z.preprocess((v) => (v !== null && v !== undefined ? Number(v) : v), z.number().max(9999, "색도 값은 5자리 미만이어야 합니다.")).nullish(), + // tph: z.preprocess((v) => (v !== null && v !== undefined ? Number(v) : v), z.number().max(9999, "총인 값은 5자리 미만이어야 합니다.")).nullish(), + // tn: z.preprocess((v) => (v !== null && v !== undefined ? Number(v) : v), z.number().max(9999, "총질소 값은 5자리 미만이어야 합니다.")).nullish(), + // ph: z.preprocess((v) => (v !== null && v !== undefined ? Number(v) : v), z.number().max(9999, "수소이온농도 값은 5자리 미만이어야 합니다.")).nullish(), + // cdtnv: z.preprocess((v) => (v !== null && v !== undefined ? Number(v) : v), z.number().max(9999, "전기전도도 값은 5자리 미만이어야 합니다.")).nullish(), + bod: z.string().nullish(), + grssClfg: z.string().nullish(), + combResdch: z.string().nullish(), + trbt: z.string().nullish(), + clIonQnt: z.string().nullish(), + smll: z.string().nullish(), + crmty: z.string().nullish(), + tph: z.string().nullish(), + tn: z.string().nullish(), + ph: z.string().nullish(), + cdtnv: z.string().nullish(), + designCst: zNumNullish, cstrnSpvsCst: zNumNullish, fcltSbsdCst: zNumNullish, @@ -217,28 +232,41 @@ export const fcBizSchema = z.object({ }); export const pbnstRegZod = z.object({ - pbnstRuseFcltPk: z.string().default(''), - swtrmtFcltFk: z.string().default(""), - ruseUtztnFcltNm: z.string().default(""), - fcltCpct: zNumDefaultZero, - ruseCpct: zNumDefaultZero, - splyExtraCpct: zNumDefaultZero, + pbnstRuseFcltId: z.string().default(""), + swtrmtFcltId: z.string().default(""), + ruseUtztnFcltNm: //z.string().default(''), + z.preprocess( + (v) => (v !== null && v !== undefined ? String(v).trim() : ""), // null/undefined → "", 공백 제거 + z.string().min(1, "시설 이름을 입력하세요")), + fcltCpct: z.preprocess( + (v) => (v !== null && v !== undefined ? Number(v) : v), + z.number().gte(0, { message: "재이용시설 용량은 0보다 크거나 같아야 합니다" }) + ), + ruseCpct: z.preprocess( + (v) => (v !== null && v !== undefined ? Number(v) : v), + z.number().gte(0, { message: "재이용시설 재이용수 공급량은 0보다 크거나 같아야 합니다" }) + ), + splyExtraCpct: z.preprocess( + (v) => (v !== null && v !== undefined ? Number(v) : v), + z.number().gte(0, { message: "재이용시설 공급가능량은 0보다 크거나 같아야 합니다" }) + ), regDt: zDateDefaultNow, rgtr: z.string().default("test"), mdfcnDt: zDateDefaultNow, mdfr: z.string().default("test"), fcFiles: z.array(fcFileSchema).default([]).nullish(), fcBizs: z.array(fcBizSchema).default([]), - fcOpers: z.array(fcOperSchema).default([]).nullish(), + fcOpers://z.array(fcOperSchema).default([]), + z .array(fcOperSchema) + .min(1, { message: "재이용시설의 운영정보를 최소 하나를 등록해주세요" }) }); -export const pbnstRegArrayZod = z.array(pbnstRegZod) +export const pbnstRegArrayZod = z.array(pbnstRegZod); -export type PbnstRegArrrayType = z.infer<typeof pbnstRegArrayZod> +export type PbnstRegArrrayType = z.infer<typeof pbnstRegArrayZod>; export type fcBizType = z.infer<typeof fcBizSchema>; export type fcOrgwtType = z.infer<typeof fcOrgwtSchema>; export type ppInstlType = z.infer<typeof pplInstlSchema>; export type operType = z.infer<typeof fcOperSchema>; export type fcFileType = z.infer<typeof fcFileSchema>; - diff --git a/apps/wr/app/store/ruseFcltMng/fcltReg/rainReg/index.tsx b/apps/wr/app/store/ruseFcltMng/fcltReg/rainReg/index.tsx index 5b3ad6813..8b3aae549 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltReg/rainReg/index.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltReg/rainReg/index.tsx @@ -1,103 +1,56 @@ import type { TableUpperProps } from "@/app/store/common/type/header"; -import { getFetch, postFectch, putFectch } from "@/components/lib/api"; +import { getFetchWithCount, postFectch, putFectch } from "@/components/lib/api"; +import toast from "react-hot-toast"; import { z } from "zod"; import { makeMdfr, makeRstr } from "../bdstReg"; +import { baseReuseBuildingSchema, chgBuildingSchema, validationMessages } from "./type"; -const validationMessages = { - required: (field: string) => `${field}을(를) 입력해주세요.`, - requiredSelect: (field: string) => `${field}을(를) 선택해주세요.`, - yearFormat: '4자리 연도만 입력하세요', -}; - -export const TEST_UUID= '2d4b151d-1828-41e0-91d9-4cd28ea1cc7e' +export const TEST_UUID = '2d4b151d-1828-41e0-91d9-4cd28ea1cc7e' const rainyObjSchema = z.object({ bdstReuseFcltFk: z.number().nullish(), - pntfColKnd: z.string().default(""), - pntfColArea: z.number().default(0), + pntfColKnd: z.string().min(1, { message: validationMessages.requiredSelect("종류") }), + pntfColArea: z.number().nullish(), dtfcltCpct: z.number().default(0), - filtrTrtmfctYn: z.string().default("Y").nullish(), + filtrTrtmfctYn: z.string().default("Y"), filtrTrtmfctCpct: z.number().nullish().default(0), filtrTrtmfctPrs: z.string().default(""), annRnwtQty: z.number().default(0), oilqMeasMthd: z.string().default("").nullish(), - rgtr:z.string().nullish(), - mdfr:z.string().nullish(), -}).refine((data) => data.pntfColKnd.length > 0, { - message: validationMessages.requiredSelect("종류"), - path: ["pntfColKnd"], -}).refine((data) => data.pntfColArea > 0, { - message: validationMessages.required("면적(m³)"), - path: ["pntfColArea"], + rgtr: z.string().nullish(), + mdfr: z.string().nullish(), }); -const reuseBuildingSchema = z.object({ - bdstRuseFcltPk: z.number().nullish(), - bdstInfoFk:z.number().nullish(), - instlCmptnDt: z.coerce.date().default(() => new Date()), - instlCst: z.number().nullish(), - operMngCd: z.string().optional().default(""), - anlOperCst: z.number().default(0), - instlCstSprtYn: z.string().default("Y"), - instlCstSprtAmt: z.number().nullish(), - grntchBdstCertYn: z.string().default("Y"), - grntchBdstCertYr: z.string().default("") - .refine((year)=> year.length===0 || /^\d{4}$/.test(year),{ - message: validationMessages.yearFormat - }), - grntchBdstCertNo: z.string().default(""), - grntchBdstCertGrd: z.string().default(""), - crgRdctYn: z.string().default("N").nullish(), /// 안쓰는 값 - crgRdctBss: z.string().default("").nullish(), /// 안쓰는 값 - crgRdctAmt: z.number().default(0), // 안쓰는 값 - ruseFcltFlg: z.string().default(""), - bdstUsgCd: z.number().default(0), - lawTrgtYn: z.string().default("Y"), - rgtr:z.string().nullish(), - mdfr:z.string().nullish(), - rainyObj: rainyObjSchema.default({}) -}).refine((data) => data.bdstUsgCd > 0, { - message: validationMessages.requiredSelect("건축물용도"), - path: ["bdstUsgCd"], -}); -const chgBuildingSchema = z.object({ - bdstInfoFk: z.number().nullish(), - archChgPrmsnDt: z.coerce.date({required_error: validationMessages.required("허가일")}), - archChgArea: z.string().default("-"), - bdstChgNm: z.string().default(""), - rgtr:z.string().nullish(), - mdfr:z.string().nullish(), -}).refine(data => data.bdstChgNm.length > 0, { - message: validationMessages.requiredSelect("구분"), - path: ["bdstChgNm"] -}); +const rainBuildingSchema = baseReuseBuildingSchema.extend({ + rainyObj: rainyObjSchema, +}) export const rainZod = z.object({ - bdstInfoPk: z.number().optional(), - ctpvCd: z.number().default(0), - sggCd: z.number().default(0), + bdstInfoSn: z.number().optional(), + ctpvCd: z.number().optional(), + sggCd: z.number().optional(), daddr: z.string().default(""), bdstNm: z.string().default(""), frstArchPrmsnDt: z.date().default(() => new Date()), archChgPrmsnDt: z.coerce.date().nullish().default(null), - archArea: z.number().default(0), + archArea: z.string().default(''), archChgArea: z.number().default(0), - rgtr:z.string().nullish(), - mdfr:z.string().nullish(), - reuseBuilding: z.array(reuseBuildingSchema).default([]), + rgtr: z.string().nullish(), + mdfr: z.string().nullish(), + reuseBuilding: z.array(rainBuildingSchema).default([]), chgBuilding: z.array(chgBuildingSchema).default([]) }) -export type chgBuilding={ - bdstInfoFk?: number|null, +export type chgBuilding = { + bdstInfoSn?: number | null, archChgPrmsnDt?: Date, archChgArea?: string, bdstChgNm?: string - rgtr?:string|null; - mdfr?:string|null; + rgtr?: string | null; + mdfr?: string | null; } -export type customRainBdst = z.infer<typeof rainZod> +export type customRainBdst = z.infer<typeof rainZod> export const upHeadList: TableUpperProps[] = [ { id: "cityText", title: "시도" }, @@ -106,7 +59,7 @@ export const upHeadList: TableUpperProps[] = [ { id: "daddr", title: "위치" }, { id: "bdsgUsgText", title: "건축물용도" }, { id: "frstArchPrmsnDt", title: "최초건축허가일" }, - { id: "archArea", title: "건축면적" }, + { id: "archArea", title: "건축면적(㎡)" }, { id: "lawTrgtYn", title: "법적의무대상여부" }, { id: "bdstChgYn", title: "건축물변경여부" }, ]; @@ -121,14 +74,16 @@ export type rainFacState = { isInit: boolean; upHeadList: TableUpperProps[]; rainRes: customRainBdst[]; + rainResCount: number; }; }; type rainFacSActions = { - initRain: (code:string|number) => void; - getRainData:(pageNation:number, code:string|number) =>void; - rainSearch: (txt:string,pageNation:number,code:string|number) => void; - rainBdstReg: (data: customRainBdst, code:string|number) => void; - rainBdstUpdate: (data: customRainBdst, code:string|number) => void; + initRain: (code: string | number) => void; + getRainData: (pageNation: number, code: string | number) => void; + rainSearch: (txt: string, pageNation: number, code: string | number) => void; + rainBdstReg: (data: customRainBdst, code: string | number) => void; + rainBdstUpdate: (data: customRainBdst, code: string | number) => void; + }; export type rainFacType = rainFacState & rainFacSActions; @@ -137,40 +92,42 @@ export const rainFacStateExport: rainFacState = { rain: { isInit: false, rainRes: rainRes, + rainResCount: 1, upHeadList: upHeadList, }, }; -const initFetch = async (code:string|number) => { - const rainRes =await getFetch("wr", `/reuseFc/rainyInfoList/1/10/${code}`); +const initFetch = async (code: string | number) => { + // const rainRes =await getFetch("wr", `/reuseFc/rainyInfoList/1/10/${code}`); + const rainRes = await getFetchWithCount("wr", `/reuseFc/rainyInfoList/1/11/${code}`); return rainRes; }; export const rainFacActionsExport: (set: any) => rainFacSActions = ( set: any ) => { return { - getRainData: async(pageNation:number, code:string|number) =>{ - try{ - const rainRes =await getFetch("wr", `/reuseFc/rainyInfoList/${pageNation}/10/${code}`); - - - // 여기서 상태 업데이트 - return set((state: rainFacState) => ({ - ...state, - rain: { - ...state.rain, - rainRes: rainRes, - isInit: false, - }, - })); + getRainData: async (pageNation: number, code: string | number) => { + try { + const rainRes = await getFetchWithCount("wr", `/reuseFc/rainyInfoList/${pageNation}/11/${code}`); - }catch(e){ - console.log(e) + // 여기서 상태 업데이트 + return set((state: rainFacState) => ({ + ...state, + rain: { + ...state.rain, + rainRes: rainRes.data, + rainResCount: rainRes.count, + isInit: false, + }, + })); - } + } catch (e) { + console.log(e) + + } }, - initRain: async (code:string|number) => { + initRain: async (code: string | number) => { try { // 파라이터 수정 통합관리자 =0, 시관리자=시코드, 시군구 관리자= 시군구 코드 를 세번째 파라미터로 지정 / 건축물 서치가 있을경우 4번째 파라미터로 던지기 즉 1/20/42(서울시)/영현빌딩 이런식으로) const rainRes = await initFetch(code); @@ -179,94 +136,132 @@ export const rainFacActionsExport: (set: any) => rainFacSActions = ( ...state, rain: { ...state.rain, - rainRes: rainRes, + rainRes: rainRes.data, + rainResCount: rainRes.count, isInit: false, }, })); } catch (error) { console.log("error", error); - } + } }, - rainSearch: async(txt:string,pageNation:number) =>{ - try{ + rainSearch: async (txt: string, pageNation: number) => { + try { - const rainRes = await getFetch('wr' , `/reuseFc/rainyInfoList/${pageNation}/20/41` +(txt? `/${txt}`:'')) + // const rainRes = await getFetch('wr' , `/reuseFc/rainyInfoList/${pageNation}/20/41` +(txt? `/${txt}`:'')) + const rainRes = await getFetchWithCount('wr', `/reuseFc/rainyInfoList/${pageNation}/11/41` + (txt ? `/${txt}` : '')) + console.log("rainRes", rainRes) set((state: rainFacState) => { return { rain: { ...state.rain, - rainRes: rainRes, + rainRes: rainRes.data, + rainResCount: rainRes.count, isInit: false, }, }; - })} - catch(erTxt){ - console.log("error", erTxt) - } - }, - rainBdstReg: async (data: customRainBdst,code:string|number) => { + }) + } + catch (erTxt) { + console.log("error", erTxt) + } + }, + rainBdstReg: async (data: customRainBdst, code: string | number) => { try { - const test =await makeRstr(data); + const test = await makeRstr(data); const res = await postFectch("/reuseFc/rainyInfo", test); - if(res.ok){ - if(data.chgBuilding.length > 0){ - await res.json().then((resData: any) => { - const chPK = resData?.data?.bdstInfoPk - const makeCh= resData.data.chgBuilding.map((item: chgBuilding) => - ( { - ...item, - bdstInfoFk: chPK, - }) - ); - const chch= postFectch("/reuseFc/chgBuilding", makeCh); - return chch; - }).then((chch: any) => { - if(chch.ok) { - alert("등록이 완료되었습니다."); - } + if (res.status === 200) { + if (data.chgBuilding.length > 0) { + + const chPK = res?.data?.bdstInfoSn + const makeCh = res.data.chgBuilding.map((item: chgBuilding) => + ({ + ...item, + bdstInfoSn: chPK, }) - }else{ - alert("등록이 완료되었습니다."); + ); + const chch = await postFectch("/reuseFc/chgBuilding", makeCh); + + if (chch.status === 200) { + toast.success("건축물 등록이 완료되었습니다."); + } else { + toast.error("건축물 증축/개축/중축 오류입니다."); + } + } else { + toast.success("건축물 등록이 완료되었습니다."); } + } else { + toast.error("건축물 등록이 완료되었습니다."); } const rainRes = await initFetch(code); set((state: rainFacState) => ({ ...state, rain: { ...state.rain, - rainRes: rainRes, + rainRes: rainRes.data, + rainResCount: rainRes.count, isInit: false, }, })); } catch (e) { console.log("e", e); - alert("등록 오류입니다."); + toast.error("등록시 오류가 발생하였습니다."); } finally { } }, - rainBdstUpdate: async (data: customRainBdst,code:string|number) => { + rainBdstUpdate: async (data: customRainBdst, code: string | number) => { try { - const chData =await makeMdfr(data); + const chData = await makeMdfr(data); const res = await putFectch("/reuseFc/rainyInfo", chData); - console.log('rainUpdate', res); - if(res.ok){ - alert(" 수정이 완료되었습니다.") + console.log("res", res) + console.log("data", data) + if (res.status === 200) { + if (data.chgBuilding.length > 0) { + const chPK = data?.bdstInfoSn; + console.log("chPK", chPK) + const makeCh = data.chgBuilding.map((item: chgBuilding) => + ({ + ...item, + bdstInfoSn: chPK, + }) + ); + console.log("makeCh", makeCh) + const chch = await postFectch("/reuseFc/chgBuilding", makeCh); + console.log("chch", chch) + + if (chch.status === 200) { + toast.success("건축물 수정이 완료되었습니다."); + } else { + toast.error("건축물 증축/개축/중축 오류입니다."); + } + } else { + toast.success("건축물 수정이 완료되었습니다."); + } + } else { + toast.error("건축물 수정이 완료되었습니다."); } + + // } + // toast.success("수정이 완료되었습니다."); + + // } const rainRes = await initFetch(code); set((state: rainFacState) => ({ ...state, rain: { ...state.rain, - rainRes: rainRes, + rainRes: rainRes.data, + rainResCount: rainRes.count, isInit: false, }, })); } catch (e) { console.log("e", e); - alert("업데이트 오류입니다."); + toast.error("수정시 오류가 발생하였습니다."); } finally { } } - }} - + } +} + diff --git a/apps/wr/app/store/ruseFcltMng/fcltReg/rainReg/type.tsx b/apps/wr/app/store/ruseFcltMng/fcltReg/rainReg/type.tsx new file mode 100644 index 000000000..7850fec93 --- /dev/null +++ b/apps/wr/app/store/ruseFcltMng/fcltReg/rainReg/type.tsx @@ -0,0 +1,93 @@ +import { z } from "zod"; + + +export const validationMessages = { + required: (field: string) => `${field}을(를) 입력해주세요.`, + requiredSelect: (field: string) => `${field}을(를) 선택해주세요.`, + yearFormat: '4자리 연도만 입력하세요', + }; + +export const baseReuseBuildingSchema = z.object({ + bdstRuseFcltSn: z.number().nullish(), + bdstInfoSn: z.number().nullish(), + instlCmptnDt: z.coerce.date().default(() => new Date()), + instlCst: z + .number() + .max(9_999_999, { message: "최대 9,999,999까지 입력 가능합니다." }) + .nullish(), + operMngCd: z.string().optional().default(""), + anlOperCst: z.number().default(0), + instlCstSprtYn: z.string().default("Y"), + instlCstSprtAmt: z.number().nullish(), + grntchBdstCertYn: z.string().default("Y"), + grntchBdstCertYr: z + .string() + .max(4, { message: validationMessages.yearFormat }) + .refine( + (year) => year.length === 0 || /^\d{4}$/.test(year), + { + message: validationMessages.yearFormat, + } + ), + grntchBdstCertNo: z.string().default(""), + grntchBdstCertGrd: z.string().default(""), + crgRdctYn: z.string().default("N").nullish(), /// 안쓰는 값 + crgRdctBss: z.string().default("").nullish(), /// 안쓰는 값 + crgRdctAmt: z.number().default(0), // 안쓰는 값 + ruseFcltFlg: z.string().default(""), + bdstUsgCd: z + .number() + .default(0) + .refine((val) => val === -1 || val >= 1, { + message: validationMessages.requiredSelect("건축물용도"), + }), + lawTrgtYn: z.string().default("Y"), + rgtr: z.string().nullish(), + mdfr: z.string().nullish(), +}); + +export const chgBuildingSchema = z.object({ + bdstInfoSn: z.number().nullish(), + archChgPrmsnDt: z.coerce.date({required_error: validationMessages.required("허가일")}), + archChgArea: z.string().default("-"), + bdstChgNm: z.string().min(1, { message: validationMessages.requiredSelect("구분") }), + rgtr:z.string().nullish(), + mdfr:z.string().nullish(), +}); + + + +export const htdflReuseBuildingSchema = z.object({ + bdstRuseFcltSn: z.number().nullish(), + bdstInfoSn: z.number().nullish(), + instlCmptnDt: z.coerce.date().default(() => new Date()), + instlCst: z.number().nullish(), + operMngCd: z.string().optional().nullish(), + anlOperCst: z.number().default(0), + instlCstSprtYn: z.string().default("Y"), + instlCstSprtAmt: z.number().nullish(), + grntchBdstCertYn: z.string().default("Y"), + grntchBdstCertYr: z + .string() + .max(4, { message: validationMessages.yearFormat }) + .refine( + (year) => year.length === 0 || /^\d{4}$/.test(year), + { + message: validationMessages.yearFormat, + } + ) + .nullish(), + grntchBdstCertNo: z.string().default(""), + grntchBdstCertGrd: z.string().default(""), + crgRdctYn: z.string().default("N").nullish(), /// 안쓰는 값 + crgRdctBss: z.string().default("").nullish(), /// 안쓰는 값 + crgRdctAmt: z.number().default(0), // 안쓰는 값 + ruseFcltFlg: z.string().default(""), + bdstUsgCd: z.number().default(0).nullish(), + lawTrgtYn: z.string().default("Y").nullish(), + rgtr: z.string().nullish(), + mdfr: z.string().nullish(), +}); + + + diff --git a/apps/wr/app/store/ruseFcltMng/fcltReg/rswtReg/index.tsx b/apps/wr/app/store/ruseFcltMng/fcltReg/rswtReg/index.tsx index 3c2504b9a..21983d466 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltReg/rswtReg/index.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltReg/rswtReg/index.tsx @@ -1,93 +1,51 @@ -import { getFetch, postFectch, putFectch } from "@/components/lib/api"; +import { getFetchWithCount, postFectch, putFectch } from "@/components/lib/api"; import { upHeadList, type chgBuilding } from "../rainReg"; import type { TableUpperProps } from "@/app/store/common/type/header"; +import toast from "react-hot-toast"; import { z } from "zod"; import { makeMdfr, makeRstr } from "../bdstReg"; +import { baseReuseBuildingSchema, chgBuildingSchema, validationMessages } from "../rainReg/type"; + +const rswtrObjSchema = z.object({ + bdstRuseFcltSn: z.number().nullish(), + //wtrUsqty: z.number({ required_error: validationMessages.required("물사용량(m³)") }).positive({ message: validationMessages.required("물사용량(m³)") }), + //oblgTrgtWtrUsqty: z.number().nullish().default(0), + orgwtKndCd: z.string({ required_error: validationMessages.requiredSelect("원수 종류") }).min(1, { message: validationMessages.requiredSelect("원수 종류") }), + filtrTrtmfctCpct: z.number({ required_error: validationMessages.required("처리시설 용량") }), + filtrTrtmfctProcs: z.string({ required_error: validationMessages.required("주요처리공정") }).min(1, { message: validationMessages.required("주요처리공정") }), + filtrTrtmfctUsgCd: z.string().default("").nullish(), + rswtrUsqty: z.number().default(0), + altUswtrUtztnYn: z.enum(["Y", "N"]).nullish(), //안쓰는값 + altUswtrKndCd: z.string().default("").nullish(), //안쓰는값 + altUswtrUtztnQnt: z.number().default(0), //안쓰는값 + altUswtrIdntyMthdCd: z.string().default("").nullish(), //안쓰는값 + rgtr: z.string().nullish().default(null), + mdfr: z.string().nullish().default(null), +}); + +const rswtBuildingSchema = baseReuseBuildingSchema.extend({ + rswtrObj: rswtrObjSchema, +}) export const rswtZod = z.object({ - bdstInfoPk: z.number().nullish(), - ctpvCd: z.number().default(0), - sggCd: z.number().default(0), + bdstInfoSn: z.number().nullish(), + ctpvCd: z.number({ required_error: '시도를 선택하세요.' }), + sggCd: z.number({ required_error: '시군구를 선택하세요.' }), daddr: z.string().default(""), bdstNm: z.string().default(""), frstArchPrmsnDt: z.date().default(() => new Date()), archChgPrmsnDt: z.coerce.date().nullish().default(null), chBuildingYn: z.string().nullish(), - archArea: z.number().default(0), + archArea: z.string().default(''), archChgArea: z.number().default(0), rgtr: z.string().nullish().default(null), mdfr: z.string().nullish().default(null), bdstUsgText: z.string().default(""), cityText: z.string().default(""), sggText: z.string().default(""), - - reuseBuilding: z - .array( - z.object({ - bdstRuseFcltPk: z.number().nullish(), - bdstInfoFk: z.number().nullish(), - instlCmptnDt: z.coerce.date().default(() => new Date()), - instlCst: z.number().default(0), - operMngCd: z.string().nullish().default(""), - - anlOperCst: z.number().default(0), - instlCstSprtYn: z.enum(["Y", "N"]), - instlCstSprtAmt: z.number().default(0), - - grntchBdstCertYn: z.enum(["Y", "N"]), - grntchBdstCertYr: z - .string() - .default("") - .refine((year) => year.length === 0 || /^\d{4}$/.test(year), { - message: "4자리 연도만 입력하세요", - }), - grntchBdstCertNo: z.string().default(""), - grntchBdstCertGrd: z.string().default(""), - - crgRdctYn: z.string().default("N").nullish(), // 안쓰는 값 - crgRdctBss: z.string().default("").nullish(), /// 안쓰는 값 - crgRdctAmt: z.number().default(0), // 안쓰는 값 - ruseFcltFlg: z.string().default(""), - bdstUsgCd: z.number().default(0), - lawTrgtYn: z.string().default(""), - rgtr: z.string().nullish().default(null), - mdfr: z.string().nullish().default(null), - - rswtrObj: z - .object({ - bdstRuseFcltFk: z.number().nullish(), - wtrUsqty: z.number().default(0), - oblgTrgtWtrUsqty: z.number().nullish().default(0), - orgwtKndCd: z.string().default(""), - filtrTrtmfctCpct: z.number().default(0), - filtrTrtmfctProcs: z.string().default(""), - filtrTrtmfctUsgCd: z.string().default("").nullish(), - rswtrUsqty: z.number().default(0), - altUswtrUtztnYn: z.enum(["Y", "N"]).nullish(), //안쓰는값 - altUswtrKndCd: z.string().default("").nullish(), //안쓰는값 - altUswtrUtztnQnt: z.number().default(0), //안쓰는값 - altUswtrIdntyMthdCd: z.string().default("").nullish(), //안쓰는값 - rgtr: z.string().nullish().default(null), - mdfr: z.string().nullish().default(null), - }) - .default({}), - }) - ) - .default([]), - - chgBuilding: z - .array( - z.object({ - bdstInfoFk: z.number().nullish(), - archChgPrmsnDt: z.coerce.date(), - archChgArea: z.string().default("-"), - bdstChgNm: z.string().default(""), - rgtr: z.string().nullish().default(null), - mdfr: z.string().nullish().default(null), - }) - ) - .default([]), + reuseBuilding: z.array(rswtBuildingSchema).default([]), + chgBuilding: z.array(chgBuildingSchema).default([]), }); export type customRswtBdst = z.infer<typeof rswtZod>; @@ -99,6 +57,7 @@ export type rswtFacState = { isInit: boolean; upHeadList: TableUpperProps[]; rswtRes: customRswtBdst[]; + rswtResCount: number; }; }; type rswtFacSActions = { @@ -120,11 +79,12 @@ export const rswtFacStateExport: rswtFacState = { isInit: false, upHeadList: upHeadList, rswtRes: rswtRes, + rswtResCount: 1, }, }; const initFetch = async (code: string | number) => { - const rswtRes = await getFetch("wr", `/reuseFc/waterreuseFc/1/10/${code}`); + const rswtRes = await getFetchWithCount("wr", `/reuseFc/waterreuseFc/1/11/${code}`); return rswtRes; }; export const rswtFacActionsExport: (set: any) => rswtFacSActions = ( @@ -137,17 +97,18 @@ export const rswtFacActionsExport: (set: any) => rswtFacSActions = ( code: string | number ) => { try { - const rswtRes = await getFetch( + const rswtRes = await getFetchWithCount( "wr", - `/reuseFc/waterreuseFc/${pageNation}/20/${code}` + - (txt ? `/${txt}` : "") + `/reuseFc/waterreuseFc/${pageNation}/11/${code}` + + (txt ? `/${txt}` : "") ); set((state: rswtFacState) => { return { ...state, rswt: { ...state.rswt, - rswtRes: rswtRes, + rswtRes: rswtRes.data, + rswtResCount: rswtRes.count, isInit: false, }, }; @@ -158,9 +119,9 @@ export const rswtFacActionsExport: (set: any) => rswtFacSActions = ( }, getRswtBdstData: async (pageNation: number, code: string | number) => { try { - const rswtRes = await getFetch( + const rswtRes = await getFetchWithCount( "wr", - `/reuseFc/waterreuseFc/${pageNation}/10/${code}` + `/reuseFc/waterreuseFc/${pageNation}/11/${code}` ); console.log(pageNation); // 여기서 상태 업데이트 @@ -168,7 +129,8 @@ export const rswtFacActionsExport: (set: any) => rswtFacSActions = ( ...state, rswt: { ...state.rswt, - rswtRes: rswtRes, + rswtRes: rswtRes.data, + rswtResCount: rswtRes.count, isInit: false, }, })); @@ -183,7 +145,8 @@ export const rswtFacActionsExport: (set: any) => rswtFacSActions = ( ...state, rswt: { ...state.rswt, - rswtRes: rswtRes, + rswtRes: rswtRes.data, + rswtResCount: rswtRes.count, isInit: false, }, })); @@ -195,23 +158,42 @@ export const rswtFacActionsExport: (set: any) => rswtFacSActions = ( try { const chData = await makeMdfr(data); const res = await putFectch("/reuseFc/waterreuseFc", chData); - if (res.ok) { - alert("등록이 완료되었습니다."); - const rswtRes = await initFetch(code); - set((state: rswtFacState) => ({ - ...state, - rswt: { - ...state.rswt, - rswtRes: rswtRes, - isInit: false, - }, - })); + if (res.status === 200) { + if (data.chgBuilding.length > 0) { + const chPK = data?.bdstInfoSn; + const makeCh = data.chgBuilding.map((item: chgBuilding) => + ({ + ...item, + bdstInfoSn: chPK, + }) + ); + const chch = await postFectch("/reuseFc/chgBuilding", makeCh); + + if (chch.status === 200) { + toast.success("중수도 건축물 수정이 완료되었습니다."); + } else { + toast.error("중수도 건축물 증축/개축/중축 오류입니다."); + } + } else { + toast.success("중수도 건축물 수정이 완료되었습니다."); + } + } else { + toast.error("중수도 건축물 수정이 완료되었습니다."); } + const rswtRes = await initFetch(code); + set((state: rswtFacState) => ({ + ...state, + rswt: { + ...state.rswt, + rswtRes: rswtRes.data, + rswtResCount: rswtRes.count, + isInit: false, + }, + })); + } catch (errorUpdate) { - alert( - "업데이트 오류입니다. " + - (errorUpdate instanceof Error ? errorUpdate.message : "") - ); + toast.error("중수도 건축물 수정 오류입니다."); + console.error(errorUpdate); } finally { } }, @@ -219,42 +201,44 @@ export const rswtFacActionsExport: (set: any) => rswtFacSActions = ( try { const chData = await makeRstr(data); const res = await postFectch("/reuseFc/waterreuseFc", chData); - if (res.ok) { + if (res.status === 200) { if (data.chgBuilding.length > 0) { - await res - .json() - .then((resData: any) => { - const chPK = resData?.data?.bdstInfoPk; - const makeCh = resData.data.chgBuilding.map( - (item: chgBuilding) => ({ - ...item, - bdstInfoFk: chPK, - }) - ); - const chch = postFectch("/reuseFc/chgBuilding", makeCh); - return chch; - }) - .then((chch: any) => { - if (chch.ok) { - alert("등록이 완료되었습니다."); - } - }); + + const chPK = res?.data?.bdstInfoSn + const makeCh = res.data.chgBuilding.map((item: chgBuilding) => + ({ + ...item, + bdstInfoSn: chPK, + }) + ); + const chch = await postFectch("/reuseFc/chgBuilding", makeCh); + + if (chch.status === 200) { + + console.log('chch.status', chch.status) + toast.success("건축물(중수도) 및 증축/개축/중축 등록이 완료되었습니다."); + } else { + toast.error("건축물(중수도 증축/개축/중축 오류입니다."); + } } else { - alert("등록이 완료되었습니다."); + toast.success("건축물(중수도 등록이 완료되었습니다."); } + } else { + toast.error("건축물(중수도) 등록이 완료하였습니다."); } - const rainRes = await initFetch(code); + const rswtRes = await initFetch(code); set((state: rswtFacState) => ({ ...state, rain: { ...state.rswt, - rswtRes: rswtRes, + rswtRes: rswtRes.data, + rswtResCount: rswtRes.count, isInit: false, }, })); } catch (e) { console.log("e", e); - alert("등록 오류입니다."); + toast.error("등록시 오류가 발생하였습니다."); } finally { } }, diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/htdflSttn/excel.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/htdflSttn/excel.tsx index 591623911..18e045025 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/htdflSttn/excel.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/htdflSttn/excel.tsx @@ -2,13 +2,13 @@ import type { SelectDataType } from "@common/business_components/ui"; import type { customHtdflBdst } from "../../fcltReg/htdflReg"; export const excelHtdflSttnParsing = ( - data: customHtdflBdst[], + excel: { data: customHtdflBdst[] }, sidoCode: SelectDataType[], sggCode: SelectDataType[] ): string[][] => { const excelExportData: string[][] = []; - data.forEach((item) => { + excel.data.forEach((item) => { excelExportData.push([ sidoCode.find((sido) => sido.val === item.ctpvCd.toString())?.text ?? "", sggCode.find((sgg) => sgg.val === item.sggCd.toString())?.text ?? "", diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/htdflSttn/headList.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/htdflSttn/headList.tsx index 756c47b9b..a37067775 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/htdflSttn/headList.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/htdflSttn/headList.tsx @@ -105,7 +105,7 @@ export const htdflSttHeadListExport: z.infer<typeof sttnHeadListType> = [ export const htdflSttNodeListExport: CommonTableProps[] = [ { id: "bdnstInfo", - title: "1.건축물정보", + title: "건축물정보", visible: true, row: 1, rowSpan: 1, @@ -113,7 +113,7 @@ export const htdflSttNodeListExport: CommonTableProps[] = [ }, { id: "htdflFcltInfo", - title: "2.온배수 시설현황", + title: "온배수 시설현황", visible: true, row: 1, rowSpan: 1, @@ -121,11 +121,11 @@ export const htdflSttNodeListExport: CommonTableProps[] = [ }, { id: "htdflMngInfo", - title: "3.온배수운영현황", + title: "온배수운영현황", visible: true, row: 1, rowSpan: 1, - colSpan: 11, + colSpan: 10, }, { @@ -162,7 +162,7 @@ export const htdflSttNodeListExport: CommonTableProps[] = [ // 1번째 줄 건축물정보 아래의 행으로 만들 헤더 { id: "bdstNm", - title: "발전소명", + title: "시설명", visible: true, upName: "bdnstInfo", row: 2, @@ -245,7 +245,7 @@ export const htdflSttNodeListExport: CommonTableProps[] = [ { id: "amtgnr", - title: "발생량", + title: "발생량(m³/년)", upName: "htdflMngInfo", visible: true, row: 2, @@ -255,7 +255,7 @@ export const htdflSttNodeListExport: CommonTableProps[] = [ { id: "prcsProcsYn", title: "처리공정여부", - upName: "prcsProcs", + upName: "htdflMngInfo", visible: true, row: 2, rowSpan: 2, @@ -263,7 +263,7 @@ export const htdflSttNodeListExport: CommonTableProps[] = [ }, { id: "prcsFcltCpct", - title: "처리시설용량(m³)", + title: "처리시설용량(m³/일)", upName: "htdflMngInfo", visible: true, row: 2, @@ -281,7 +281,7 @@ export const htdflSttNodeListExport: CommonTableProps[] = [ }, { id: "utztnCpct", - title: "이용량(m³)", + title: "이용량(m³/년)", upName: "htdflMngInfo", visible: true, row: 2, diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/htdflSttn/index.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/htdflSttn/index.tsx index dccc8719c..d62ee3340 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/htdflSttn/index.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/htdflSttn/index.tsx @@ -1,10 +1,12 @@ import type { CommonTableProps } from "@/app/store/common/type/header"; -import { htdflSttHeadListExport, htdflSttNodeListExport } from "./headList"; -import type { sttnHeadListType } from "../rainSttn/headList"; +import { fetchType, getFetchWithCount } from "@/components/lib/api"; +import type { SelectDataType } from "@common/business_components/ui"; import type { z } from "zod"; -import { controlHeadView } from "../rainSttn"; import type { customHtdflBdst } from "../../fcltReg/htdflReg"; -import { fetchType, getFetch } from "@/components/lib/api"; +import { controlHeadView } from "../rainSttn"; +import type { sttnHeadListType } from "../rainSttn/headList"; +import { excelHtdflSttnParsing } from "./excel"; +import { htdflSttHeadListExport, htdflSttNodeListExport } from "./headList"; const htdflSttHeadList = htdflSttHeadListExport; const htdflSttNodeList = htdflSttNodeListExport; @@ -16,12 +18,16 @@ export type htdflSttnState = { htdflSttNodeList: CommonTableProps[]; htdflSttnRes: customHtdflBdst[] excelList: []; - + htdflSttnResCount: number; }; }; type htdflSttnActions = { - initHtdfl: (con: string) => void; - htdflSttnSearch: (page: number, total: string, con: string) => void; + initHtdfl: (isExcel: boolean, page: number, total: string, con: string, txt: string, + sidoCode: SelectDataType[], + sggCode: SelectDataType[]) => void; + htdflSttnSearch: (isExcel: boolean, page: number, total: string, con: string, txt: string, + sidoCode: SelectDataType[], + sggCode: SelectDataType[]) => void; htdflChSttnHead: ( headList: z.infer<typeof sttnHeadListType>, ) => void; @@ -36,12 +42,18 @@ export const htdflSttnStateExport: htdflSttnState = { htdflSttnRes: [], excelList: [], isInit: false, + htdflSttnResCount: 1, }, }; -const initFetch = async (code: string | number) => { - const htdflRes = await getFetch( +const initFetch = async (page: number, total: string, con: string, txt: string) => { + const htdflRes = await getFetchWithCount( "wr", - `/drain/facilityList/1/20/${code}`) + `/drain/facilityList` + + (page ? `/${page}` : "/1") + + (total ? `/${total}` : "/20") + + (con ? `/${con}` : "/0") + + (txt ? `/${txt}` : "") + ); return htdflRes; }; @@ -50,39 +62,60 @@ export const htdflSttnActionsExport: (set: any) => htdflSttnActions = ( set: any ) => { return { - initHtdfl: async (code: string | number) => { + initHtdfl: async (isExcel: boolean, page: number, total: string, con: string, txt: string, + sidoCode: SelectDataType[], + sggCode: SelectDataType[]) => { try { - const htdflSttnRes = await initFetch(code); - set((state: htdflSttnState) => { - return { - ...state, - htdflSttn: { - ...state.htdflSttn, - htdflSttnRes: htdflSttnRes, - isInit: false, - }, - }; - }); - } catch (error) { - console.log(error); - } - }, - htdflSttnSearch: async (page: number, total: string, con: string) => { - try { - const htdflSttnRes = await getFetch( - fetchType.WR, - `/drain/facilityList/${page}/${total}/${con}` - ); - if (Number(total) > 9000) { - - + const htdflSttnRes = await initFetch(page, total, con, txt); + if (isExcel) { + const htdflExcel = excelHtdflSttnParsing(htdflSttnRes, sidoCode, sggCode); set((state: htdflSttnState) => { - return { ...state, htdflSttn: { ...state.htdflSttn, - excelList: htdflSttnRes, + excelList: htdflExcel, + }, + }; + }); + } else { + set((state: htdflSttnState) => { + return { + ...state, + htdflSttn: { + ...state.htdflSttn, + htdflSttnRes: htdflSttnRes.data, + htdflSttnResCount: htdflSttnRes.count, + isInit: false, + }, + }; + }); + } + } catch (error) { + console.log(error); + } + }, + htdflSttnSearch: async (isExcel: boolean, page: number, total: string, con: string, txt: string, + sidoCode: SelectDataType[], + sggCode: SelectDataType[]) => { + try { + const htdflSttnRes = await getFetchWithCount( + fetchType.WR, + `/drain/facilityList` + + (page ? `/${page}` : "/1") + + (total ? `/${total}` : "/20") + + (con ? `/${con}` : "/0") + + (txt ? `/${txt}` : "") + ); + + if (isExcel) { + const htdflExcel = excelHtdflSttnParsing(htdflSttnRes, sidoCode, sggCode); + set((state: htdflSttnState) => { + return { + ...state, + htdflSttn: { + ...state.htdflSttn, + excelList: htdflExcel, }, }; }); @@ -92,7 +125,8 @@ export const htdflSttnActionsExport: (set: any) => htdflSttnActions = ( ...state, htdflSttn: { ...state.htdflSttn, - htdflSttnRes: htdflSttnRes, + htdflSttnRes: htdflSttnRes.data, + htdflSttnResCount: htdflSttnRes.count, isInit: false, }, }; diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/excel.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/excel.tsx index 8a1db05a2..9e0af4ffa 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/excel.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/excel.tsx @@ -1,4 +1,3 @@ -import type { customPbnst } from "../../fcltReg/pbnstReg/type"; export const excelPbnstSttnParsing = (data: any[]): string[][] => { const excelExportData: string[][] = []; @@ -13,15 +12,19 @@ export const excelPbnstSttnParsing = (data: any[]): string[][] => { String(item.gugun ?? ""), String(item.bscFctNm ?? ""), String(item.efflWqltStndrdNm ?? ""), - String(item.fctCpc ?? ""), String(item.tel ?? ""), + String(item.fctCpc ?? ""), String(item.ruseUtztnFcltNm ?? ""), String(item.fcltCpct ?? ""), String(item.splyExtraCpct ?? ""), String(item.ruseCpct ?? ""), String(item.usageTotal ?? ""), String(item.inUsage ?? ""), - String(item.outUsage ?? ""), + String(item.outUsage ?? ""), + String(item.operMthd ?? ""), + String(item.rprtvBzenty ?? ""), + String(item.bgngDt ?? ""), + String(item.endDt ?? ""), ]) }) return excelExportData; diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/headList.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/headList.tsx index 7c2953932..cdac90a77 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/headList.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/headList.tsx @@ -100,7 +100,7 @@ export const pbnstSttNodeListExport: CommonTableProps[] = [ colSpan: 3, }, { - id: "operMngCd", + id: "rgnNm", title: "환경청", visible: true, row: 1, @@ -116,7 +116,7 @@ export const pbnstSttNodeListExport: CommonTableProps[] = [ colSpan: 2, }, { - id: "bdnstNm", + id: "bscFctNm", title: "시설명", visible: true, row: 1, @@ -124,7 +124,7 @@ export const pbnstSttNodeListExport: CommonTableProps[] = [ colSpan: 1, }, { - id: "locDiv", + id: "efflWqltStndrdNm", title: "지역구분", visible: true, row: 1, @@ -141,14 +141,14 @@ export const pbnstSttNodeListExport: CommonTableProps[] = [ }, { id: "fctCpc", - title: "시설용량(톤/일)", + title: "시설용량(m³/일)", visible: true, row: 1, rowSpan: 2, colSpan: 1, }, { - id: "rusNm", + id: "ruseUtztnFcltNm", title: "재이용시설명", visible: true, row: 1, @@ -157,7 +157,7 @@ export const pbnstSttNodeListExport: CommonTableProps[] = [ }, { id: "rusSup", - title: "재이용수공급가능량", + title: "재이용수공급가능량(m³/일)", visible: true, row: 1, rowSpan: 1, @@ -180,7 +180,7 @@ export const pbnstSttNodeListExport: CommonTableProps[] = [ colSpan: 4, }, { - id: "a", + id: "rvbsn", title: "권역", upName: "catchment", visible: true, @@ -189,7 +189,7 @@ export const pbnstSttNodeListExport: CommonTableProps[] = [ colSpan: 1, }, { - id: "aaa", + id: "lgbsn", title: "단위유역", upName: "catchment", visible: true, @@ -198,7 +198,7 @@ export const pbnstSttNodeListExport: CommonTableProps[] = [ colSpan: 1, }, { - id: "aaaa", + id: "mdmbsn", title: "중권역", upName: "catchment", visible: true, @@ -207,7 +207,7 @@ export const pbnstSttNodeListExport: CommonTableProps[] = [ colSpan: 1, }, { - id: "cityText", + id: "sido", title: "시도", upName: "adminDiv", visible: true, @@ -216,7 +216,7 @@ export const pbnstSttNodeListExport: CommonTableProps[] = [ colSpan: 1, }, { - id: "sggText", + id: "gugun", title: "시군구", upName: "adminDiv", visible: true, @@ -226,7 +226,7 @@ export const pbnstSttNodeListExport: CommonTableProps[] = [ }, { id: "fcltCpct", - title: "시설용량(m³/일)", + title: "시설용량", upName: "rusSup", visible: true, row: 2, @@ -234,8 +234,8 @@ export const pbnstSttNodeListExport: CommonTableProps[] = [ colSpan: 1, }, { - id: "trbt", - title: "하수처리량(m³/일)", + id: "splyExtraCpct", + title: "하수처리량", upName: "rusSup", visible: true, row: 2, @@ -244,7 +244,7 @@ export const pbnstSttNodeListExport: CommonTableProps[] = [ }, { id: "ruseCpct", - title: "재이용량(m³/일)", + title: "재이용량", upName: "rusSup", visible: true, row: 2, @@ -252,7 +252,7 @@ export const pbnstSttNodeListExport: CommonTableProps[] = [ colSpan: 1, }, { - id: "total", + id: "usageTotal", title: "합계", upName: "yearResult", visible: true, @@ -261,7 +261,7 @@ export const pbnstSttNodeListExport: CommonTableProps[] = [ colSpan: 1, }, { - id: "ithlWtr", + id: "inUsage", title: "장내용수", upName: "yearResult", visible: true, @@ -270,7 +270,7 @@ export const pbnstSttNodeListExport: CommonTableProps[] = [ colSpan: 1, }, { - id: "othlWtr", + id: "outUsage", title: "장외용수", upName: "yearResult", visible: true, @@ -279,7 +279,7 @@ export const pbnstSttNodeListExport: CommonTableProps[] = [ colSpan: 1, }, { - id: "mngMeth", + id: "operMthd", title: "운영방식", upName: "mngInfo", visible: true, @@ -297,7 +297,7 @@ export const pbnstSttNodeListExport: CommonTableProps[] = [ colSpan: 1, }, { - id: "firstDay", + id: "bgngDt", title: "시작일", upName: "mngInfo", visible: true, @@ -306,7 +306,7 @@ export const pbnstSttNodeListExport: CommonTableProps[] = [ colSpan: 1, }, { - id: "LastDay", + id: "endDt", title: "종료일", upName: "mngInfo", visible: true, @@ -337,7 +337,7 @@ export const ruseFcltNodeListExport: TableUpperProps[] = [ }, { id: "mainSplyPumpCpct", - title: "메인공급펌프용량", + title: "메인공급펌프용량(m³/일)", visible: true, row: 1, rowSpan: 3, @@ -345,7 +345,7 @@ export const ruseFcltNodeListExport: TableUpperProps[] = [ }, { id: "buziFee", - title: "사업비(백만원)", + title: "사업비(원)", visible: true, row: 2, rowSpan: 1, @@ -418,7 +418,7 @@ export const ruseFcltNodeListExport: TableUpperProps[] = [ }, { id: "cusrAlotm", - title: "민간투자비", + title: "민간투자비", visible: true, row: 3, rowSpan: 1, @@ -438,7 +438,7 @@ export const ruseFcltNodeListExport: TableUpperProps[] = [ export const ruseSplyHeadListExport: TableUpperProps[] = [ { id: "cycl", title: "차수", visible: true }, - { id: "orgwtWtritQnt", title: "원수취수량", visible: true }, + { id: "orgwtWtritQnt", title: "원수취수량(m³/일)", visible: true }, { id: "tmsYn", title: "하수처리량TMS 전후", visible: true }, { id: "dschgFmYn", title: "하수처리장 방류 유량계 전·후​", visible: true }, { id: "dtlPstnExpln", title: "상세위치 설명", visible: true }, @@ -447,8 +447,8 @@ export const ruseSplyHeadListExport: TableUpperProps[] = [ export const rusePplHeadListExport: TableUpperProps[] = [ { id: "cycl", title: "차수", visible: true }, { id: "ppMqlt", title: "관재질", type: "select", visible: true }, - { id: "pdmt", title: "관경", type: "select", visible: true }, - { id: "cpct", title: "연장", edit: true, visible: true }, + { id: "pdmt", title: "관경(mm)", type: "select", visible: true }, + { id: "cpct", title: "연장(m)", edit: true, visible: true }, ]; export const ruseGuranteeHeadListExport: TableUpperProps[] = [ @@ -458,10 +458,10 @@ export const ruseGuranteeHeadListExport: TableUpperProps[] = [ { id: "combResdch", title: "결합잔류염소(mh/L)", visible: true }, { id: "trbt", title: "탁도(NTU)", visible: true }, { id: "smll", title: "냄새", visible: true }, - { id: "crmty", title: "색도", visible: true }, + { id: "crmty", title: "색도(도)", visible: true }, { id: "tn", title: "총질소(mg/L)", visible: true }, { id: "tph", title: "총인(mg/L)", visible: true }, - { id: "ph", title: "수소이온농도", visible: true }, + { id: "ph", title: "수소이온농도(PH)", visible: true }, { id: "clIonQnt", title: "염화물(mg/L)", visible: true }, { id: "cdtnv", title: "전기전도도(μs/cm)​", visible: true }, ]; diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx index 33bce5520..542622d34 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx @@ -1,15 +1,15 @@ import type { CommonTableProps, TableUpperProps } from "@/app/store/common/type/header"; -import type { z } from "zod"; -import { pbnSttHeadListExport, pbnstSttNodeListExport, ruseDmdHeadListExport, ruseFcltHeadListExport, ruseFcltNodeListExport, ruseGuranteeHeadListExport, rusePplHeadListExport, ruseSplyHeadListExport } from "./headList"; +import type { FcPrfType } from "@/app/store/prfmncReg/pbnstPrfmncReg/type"; import { fetchType, getFetch, postFectch } from "@/components/lib/api"; +import type { ReturnType } from "@common/business_components/ui"; +import moment from "moment"; +import { type z } from "zod"; +import type { customPbnst, pubHasuType } from "../../fcltReg/pbnstReg/type"; import { controlHeadView } from "../rainSttn"; import type { sttnHeadListType } from "../rainSttn/headList"; -import type { customPbnst, pubHasuType } from "../../fcltReg/pbnstReg/type"; import { excelPbnstSttnParsing } from "./excel"; -import type { FcPrfType } from "@/app/store/prfmncReg/pbnstPrfmncReg/type"; -import type { ReturnType } from "@common/business_components/ui"; +import { pbnSttHeadListExport, pbnstSttNodeListExport, ruseDmdHeadListExport, ruseFcltHeadListExport, ruseFcltNodeListExport, ruseGuranteeHeadListExport, rusePplHeadListExport, ruseSplyHeadListExport } from "./headList"; import type { WaterReuseFacility } from "./type"; -import moment from "moment"; const pbnstSttHeadList = pbnSttHeadListExport; const pbnstSttNodeList = pbnstSttNodeListExport; @@ -21,13 +21,15 @@ export type pbnstSttnState = { pbnstSttHeadList: CommonTableProps[]; pbnstSttNodeList: CommonTableProps[]; pbnstDetail: customPbnst; - ruseFcltNodeList:TableUpperProps[] - ruseSplyHeadList:TableUpperProps[] - rusePplHeadList:TableUpperProps[] - ruseGuranteeHeadList:TableUpperProps[] - ruseDmdHeadList:TableUpperProps[] - ruseFcltHeadList:TableUpperProps[] + ruseFcltNodeList: TableUpperProps[] + ruseSplyHeadList: TableUpperProps[] + rusePplHeadList: TableUpperProps[] + ruseGuranteeHeadList: TableUpperProps[] + ruseDmdHeadList: TableUpperProps[] + ruseFcltHeadList: TableUpperProps[] + pbnstSttnResCount: number; excelList: []; + fclts: string[] isInit: boolean; }; }; @@ -35,26 +37,32 @@ export type pbnstSttnState = { type pbnstSttnActions = { pbnstChSttnHead: (headList: z.infer<typeof sttnHeadListType>) => void; pbnstSttnSearch: ( + isExcel: boolean, page: string, total: string, reurnValue: ReturnType, searchNm: string | undefined, - year: string + year: string, + fclts: string[] ) => void; + getFlcts: () => void; pbnSttnInit: ( page: string, total: string, reurnValue: ReturnType, - year: string + year: string, + flcts: string[] ) => void; - getPbnStnDetail: (pbnstRuseFcltPk: string) => void; + getPbnStnDetail: (pbnstRuseFcltId: string) => void; pbnNmSearch: ( page: string, total: string, reurnValue: ReturnType, searchNm: string | undefined, - year: string + year: string, + flcts: string[] ) => void; + resetPbnstDetail: () => void; }; export type pbnstSttnType = pbnstSttnState & pbnstSttnActions; @@ -63,16 +71,18 @@ export const pbnstSttnStateExport: pbnstSttnState = { pbnstSttn: { pbnstSttHeadList: pbnstSttHeadList, pbnstSttNodeList: pbnstSttNodeList, - ruseFcltNodeList:ruseFcltNodeListExport, - ruseSplyHeadList:ruseSplyHeadListExport, - rusePplHeadList:rusePplHeadListExport, - ruseGuranteeHeadList:ruseGuranteeHeadListExport, - ruseDmdHeadList:ruseDmdHeadListExport, - ruseFcltHeadList:ruseFcltHeadListExport, + ruseFcltNodeList: ruseFcltNodeListExport, + ruseSplyHeadList: ruseSplyHeadListExport, + rusePplHeadList: rusePplHeadListExport, + ruseGuranteeHeadList: ruseGuranteeHeadListExport, + ruseDmdHeadList: ruseDmdHeadListExport, + ruseFcltHeadList: ruseFcltHeadListExport, pbnstSttnRes: [], + pbnstSttnResCount: 1, pbnstDetail: {} as customPbnst, pbnstSearchData: [], excelList: [], + fclts: [], isInit: false, }, }; @@ -82,99 +92,111 @@ const hasuListFetch = async ( total: string, reurnValue: ReturnType, searchNm: string | undefined, - year: string + year: string, + flcts: string[] ) => { - let url = ""; + let url = `/prcsFclt/ruseInfo/`; + if (reurnValue.partValue === "A") { - url = `/prcsFclt/info/${page}/${total}/${reurnValue.partValue}/${ - reurnValue.sidoValue ? reurnValue.sidoValue : reurnValue.sigunValue + url += `${page}/${total}/${reurnValue.partValue}/${ + // reurnValue.sidoValue ? reurnValue.sidoValue : reurnValue.sigunValue + reurnValue.sigunValue !== '0' ? reurnValue.sigunValue : reurnValue.sidoValue }/0/1/SW` } else if (reurnValue.partValue === "R") { - url = `/prcsFclt/info/${page}/${total}/${reurnValue.partValue}/${ - reurnValue.regionValue - ? reurnValue.regionValue - : reurnValue.unitWaterShedValue - ? reurnValue.unitWaterShedValue - : reurnValue.middleRegionValue + url += `${page}/${total}/${reurnValue.partValue}/${reurnValue.regionValue + ? reurnValue.regionValue + : reurnValue.unitWaterShedValue + ? reurnValue.unitWaterShedValue + : reurnValue.middleRegionValue }/0/1/SW` + } else if (reurnValue.partValue === 'G') { + + + url += `${page}/${total}/${reurnValue.envPartValue}/${reurnValue.sigunValue !== '0' ? reurnValue.sigunValue : reurnValue.sidoValue}/0/1/SW` + } else { - url = `/prcsFclt/info/${page}/${total}/${reurnValue.envPartValue}/${reurnValue.sidoValue ? reurnValue.sidoValue : reurnValue.sigunValue}/0/1/SW` - + url += `1/2/A/0/0/1/SW` } - if( searchNm&& searchNm?.length>0){ - url+="/"+searchNm + + if (searchNm && searchNm?.length > 0) { + url += "/" + searchNm } - const pbnList = await getFetch(fetchType.COMMON, url); - const arrayList = pbnList.map((item: pubHasuType) => item.bscFctCd); + + const pbnList = await postFectch(url, flcts, fetchType.COMMON); const getRuseList = await postFectch( `/reuseFc/pubDetailList/${year}`, - arrayList, + flcts, fetchType.WR ); const checkWrRegListRes = getRuseList.data; + + const data: WaterReuseFacility[] = checkWrRegListRes.map( (item: FcPrfType) => { - const findData = pbnList.find( + const findData = pbnList.data.find( (fclt: pubHasuType) => - fclt.bscFctCd === item.pbnstRuseFcltPk.substring(0, 11) + fclt.bscFctCd === item.pbnstRuseFcltId.substring(0, 11) ); - const usage = item.prfList.reduce( - (acc, cur) => { - acc.inUsage += - cur.wshwtr + - cur.coolUswtr + - cur.cleanUswtr + - cur.drwtUswtr + - cur.dltnUswtr + - cur.ithlRswtr + - cur.ithlEtcUswtr + - cur.cleanToilUswtr + - cur.resn; - acc.outUsage += - cur.lndwtr + - cur.hydUswtr + - cur.ugwtChr + - cur.farmUswtr + - cur.rvrKeepUswtr + - cur.indstUswtr; - acc.total = acc.inUsage + acc.outUsage; + if (findData) { - return acc; - }, - { total: 0, inUsage: 0, outUsage: 0 } - ); + const usage = item.prfList.reduce( + (acc, cur) => { + acc.inUsage += + cur.wshwtr + + cur.coolUswtr + + cur.cleanUswtr + + cur.drwtUswtr + + cur.dltnUswtr + + cur.ithlRswtr + + cur.ithlEtcUswtr + + cur.cleanToilUswtr + + cur.resn; + acc.outUsage += + cur.lndwtr + + cur.hydUswtr + + cur.ugwtChr + + cur.farmUswtr + + cur.rvrKeepUswtr + + cur.indstUswtr; + acc.total = acc.inUsage + acc.outUsage; + + return acc; + }, + { total: 0, inUsage: 0, outUsage: 0 } + ); + return { + rvbsn: findData?.rvbsn || '-', + lgbsn: findData?.lgbsn || '-', + mdmbsn: findData?.mdmbsn || '-', + rgnNm: findData?.rgnNm || '-', + sido: findData?.sido || '-', + gugun: findData?.gugun || '-', + bscFctNm: findData?.bscFctNm || '-', + efflWqltStndrdNm: findData?.efflWqltStndrdNm || '-', + fctCpc: Number(findData?.fctCpc) || 0, + tel: findData?.tel ?? "-", + ruseUtztnFcltNm: item?.ruseUtztnFcltNm || '-', + fcltCpct: item?.fcltCpct || '-', + splyExtraCpct: item?.splyExtraCpct || '-', + ruseCpct: item?.ruseCpct || '-', + pbnstRuseFcltId: item.pbnstRuseFcltId || '-', + usageTotal: usage?.total, + inUsage: usage?.inUsage, + outUsage: usage?.outUsage, + operMthd: item.operList[0]?.operMthd || '-', + rprtvBzenty: item.operList[0]?.rprtvBzenty, + bgngDt: moment(item.operList[0]?.bgngDt).format("YYYY-MM-DD") ?? "", + endDt: moment(item.operList[0]?.endDt).format("YYYY-MM-DD") ?? "", + searchNm: findData?.bscFctNm || '-', + searchPk: findData?.bscFctCd || '-', + prfList: item?.prfList ?? [] + }; + } - return { - rvbsn: findData?.rvbsn, - lgbsn: findData?.lgbsn, - mdmbsn: findData?.mdmbsn, - rgnNm: findData?.rgnNm, - sido: findData?.sido, - gugun: findData?.gugun, - bscFctNm: findData?.bscFctNm, - efflWqltStndrdNm: findData?.efflWqltStndrdNm, - fctCpc: findData.fctCpc, - tel: findData?.tel ?? "-", - ruseUtztnFcltNm: item.ruseUtztnFcltNm, - fcltCpct: item.fcltCpct, - splyExtraCpct: item.splyExtraCpct, - ruseCpct: item.ruseCpct, - pbnstRuseFcltPk: item.pbnstRuseFcltPk, - usageTotal: usage.total, - inUsage: usage.inUsage, - outUsage: usage.outUsage, - operMthd: item.operList[0]?.operMthd, - rprtvBzenty: item.operList[0]?.rprtvBzenty, - bgngDt: moment(item.operList[0]?.bgngDt).format("YYYY-MM-DD") ?? "", - endDt: moment(item.operList[0]?.endDt).format("YYYY-MM-DD") ?? "", - searchNm: findData.bscFctNm, - searchPk: findData.bscFctCd, - prfList:item?.prfList??[] - }; } - ); + ).filter(Boolean); return data; }; @@ -182,6 +204,16 @@ export const pbnstSttnActionsExport: (set: any) => pbnstSttnActions = ( set: any ) => { return { + getFlcts: async () => { + const fclts = await getFetch(fetchType.WR, '/reuseFc/pubFc') + + return set((state: pbnstSttnState) => ({ + pbnstSttn: { + ...state.pbnstSttn, + fclts: fclts + }, + })); + }, pbnstChSttnHead: async ( headList: z.infer<typeof sttnHeadListType> ): Promise<void> => { @@ -196,22 +228,28 @@ export const pbnstSttnActionsExport: (set: any) => pbnstSttnActions = ( })); }, pbnstSttnSearch: async ( + isExcel: boolean, page: string, total: string, reurnValue: ReturnType, searchNm: string | undefined, - year: string + year: string, + fclts: string[] ) => { try { + + const pbnstSttnRes = await hasuListFetch( page, total, reurnValue, searchNm, - year + year, + fclts, ); - console.log("pbnstSttnRes", pbnstSttnRes); - if (Number(total) > 9000) { + + // if (Number(total) > 9000) { + if (isExcel) { const pbnstExcel = excelPbnstSttnParsing(pbnstSttnRes); set((state: pbnstSttnState) => { return { @@ -222,17 +260,20 @@ export const pbnstSttnActionsExport: (set: any) => pbnstSttnActions = ( }, }; }); - } else + } else { + set((state: pbnstSttnState) => { return { ...state, pbnstSttn: { ...state.pbnstSttn, pbnstSttnRes: pbnstSttnRes, - isInit: false, + pbnstSttnResCount: pbnstSttnRes.length + // isInit: false, }, }; }); + } } catch (erTxt) { console.log("error", erTxt); } @@ -241,26 +282,28 @@ export const pbnstSttnActionsExport: (set: any) => pbnstSttnActions = ( page: string, total: string, reurnValue: ReturnType, - year: string + year: string, + fclts: string[] ) => { try { - const pbnstSttnRes = await hasuListFetch( page, total, reurnValue, undefined, - year + year, + fclts ); - console.log('pbnstSttnRes ', pbnstSttnRes) set((state: pbnstSttnState) => { return { ...state, pbnstSttn: { ...state.pbnstSttn, pbnstSttnRes: pbnstSttnRes, + pbnstSttnResCount: pbnstSttnRes.length, isInit: false, + pbnstDetail: {} as customPbnst }, }; }); @@ -268,13 +311,23 @@ export const pbnstSttnActionsExport: (set: any) => pbnstSttnActions = ( console.log("error", erTxt); } }, - getPbnStnDetail: async (pbnstRuseFcltPk: string) => { + resetPbnstDetail: async () => { + set((state: pbnstSttnState) => { + return { + ...state, + pbnstSttn: { + ...state.pbnstSttn, + pbnstDetail: {} as customPbnst + }, + }; + }) + }, + getPbnStnDetail: async (pbnstRuseFcltId: string) => { try { - let pbnstDetail = await getFetch("wr", `/reuseFc/pubFc/${pbnstRuseFcltPk}`); - if(pbnstDetail.length>=0){ - pbnstDetail= pbnstDetail.find((item: customPbnst) =>item.pbnstRuseFcltPk === pbnstRuseFcltPk) + let pbnstDetail = await getFetch("wr", `/reuseFc/pubFc/${pbnstRuseFcltId}`); + if (pbnstDetail.length >= 0) { + pbnstDetail = pbnstDetail.find((item: customPbnst) => item.pbnstRuseFcltId === pbnstRuseFcltId) } - console.log( 'pbnstDetail ', pbnstDetail) set((state: pbnstSttnState) => { return { ...state, @@ -293,17 +346,19 @@ export const pbnstSttnActionsExport: (set: any) => pbnstSttnActions = ( total: string, reurnValue: ReturnType, searchNm: string | undefined, - year: string - ) => { + year: string, + flcts: string[] + ) => { try { - - const pbnstSttnRes = await hasuListFetch( - page, - total, - reurnValue, - searchNm, - year - ); + + const pbnstSttnRes = await hasuListFetch( + page, + total, + reurnValue, + searchNm, + year, + flcts + ); set((state: pbnstSttnState) => { return { @@ -311,6 +366,7 @@ export const pbnstSttnActionsExport: (set: any) => pbnstSttnActions = ( pbnstSttn: { ...state.pbnstSttn, pbnstSearchData: pbnstSttnRes, + pbnstSttnResCount: pbnstSttnRes.length, }, }; }); diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/type.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/type.tsx index e4e86da5d..7d0f8a068 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/type.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/type.tsx @@ -10,7 +10,7 @@ export type WaterReuseFacility = { lgbsn: string; // 대수계 (예: "한강본류") mdmbsn: string; // 중권역 (예: "한강서울") outUsage: number; // 유출량 (예: 900) - pbnstRuseFcltPk: string; // 공공하수 재이용 시설 PK (예: "11000SW001R1") + pbnstRuseFcltId: string; // 공공하수 재이용 시설 PK (예: "11000SW001R1") rgnNm: string; // 관할 기관명 (예: "한강유역환경청") ruseCpct: number; // 재이용 가능 용량 (예: 123) ruseUtztnFcltNm: string; // 재사용 활용 시설명 (예: "난지 재사용") diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/rainSttn/excel.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/rainSttn/excel.tsx index 13530a81f..b57e0edde 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/rainSttn/excel.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/rainSttn/excel.tsx @@ -1,22 +1,24 @@ import type { SelectDataType } from "@common/business_components/ui"; import type { customRainBdst } from "../../fcltReg/rainReg"; - +import { bdstUsgCd } from "@/app/store/common/code/basicCode"; +//TODO: 공통코드시 삭제되니 수정 해야됨 export const excelRainSttnParsing = ( - data: customRainBdst[], + excel: { data: customRainBdst[] }, sidoCode: SelectDataType[], sggCode: SelectDataType[] ): string[][] => { const excelExportData: string[][] = []; - data.forEach((item) => { + excel.data.forEach((item) => { + console.log("item", item) excelExportData.push([ - sidoCode.find((sido) => sido.val === item.ctpvCd.toString())?.text ?? "", - sggCode.find((sgg) => sgg.val === item.sggCd.toString())?.text ?? "", + sidoCode.find((sido) => sido.val === item.ctpvCd?.toString())?.text ?? "", + sggCode.find((sgg) => sgg.val === item.sggCd?.toString())?.text ?? "", item.bdstNm ?? "", item.daddr ?? "", - String(item.bdstInfoPk ?? ""), + bdstUsgCd.find((cd) => cd.val === item.reuseBuilding?.[0]?.bdstUsgCd.toString())?.text ?? "", //건축물용도 String(item.frstArchPrmsnDt ?? ""), - String(item.archArea ?? ""), + String(item.archArea.split('.')[0] === '-1' ? '확인불가' : item.archArea.split('.')[0]), String(item.chgBuilding.length > 0 ? "Y" : "N"), String(item.reuseBuilding?.[0]?.lawTrgtYn ?? ""), String(item.reuseBuilding?.[0]?.instlCmptnDt ?? ""), @@ -28,7 +30,7 @@ export const excelRainSttnParsing = ( String(item.reuseBuilding?.[0]?.rainyObj?.filtrTrtmfctPrs ?? ""), String(item.reuseBuilding?.[0]?.rainyObj?.dtfcltCpct ?? ""), String(item.reuseBuilding?.[0]?.instlCstSprtYn ?? ""), - String(item.reuseBuilding?.[0]?.instlCstSprtAmt ?? ""), + String(item.reuseBuilding?.[0]?.instlCstSprtAmt === -1 ? "확인불가" : String(item.reuseBuilding?.[0]?.instlCstSprtAmt) ?? ""), String(item.reuseBuilding?.[0]?.grntchBdstCertYn ?? ""), String(item.reuseBuilding?.[0]?.grntchBdstCertYr ?? ""), String(item.reuseBuilding?.[0]?.grntchBdstCertNo ?? ""), diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/rainSttn/headList.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/rainSttn/headList.tsx index 7db00678d..58f2d491a 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/rainSttn/headList.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/rainSttn/headList.tsx @@ -31,8 +31,8 @@ export const rainSttHeadListExport: z.infer<typeof sttnHeadListType> = [ visible: true, }, { - id: "bdstUsgCd", - title: "건축물용도코드", + id: "bdsgUsgText", + title: "건축물용도", visible: true, }, { @@ -136,7 +136,7 @@ export const rainSttHeadListExport: z.infer<typeof sttnHeadListType> = [ export const rainSttNodeListExport: CommonTableProps[] = [ { id: "bdnstInfo", - title: "1.건축물정보", + title: "건축물정보", visible: true, row: 1, rowSpan: 1, @@ -144,7 +144,7 @@ export const rainSttNodeListExport: CommonTableProps[] = [ }, { id: "rainFcltInfo", - title: "2.빗물이용시설정보", + title: "빗물이용시설정보", visible: true, row: 1, rowSpan: 1, @@ -152,7 +152,7 @@ export const rainSttNodeListExport: CommonTableProps[] = [ }, { id: "insenInfo", - title: "3.인센티브 정보", + title: "인센티브 정보", visible: true, row: 1, rowSpan: 1, @@ -209,8 +209,8 @@ export const rainSttNodeListExport: CommonTableProps[] = [ colSpan: 1, }, { - id: "bdstUsgCd", - title: "건축물용도코드", + id: "bdsgUsgText", + title: "건축물용도", upName: "bdnstInfo", visible: true, row: 2, @@ -228,7 +228,7 @@ export const rainSttNodeListExport: CommonTableProps[] = [ }, { id: "archArea", - title: "건축면적", + title: "건축면적(m³)", upName: "bdnstInfo", visible: true, row: 2, @@ -265,7 +265,7 @@ export const rainSttNodeListExport: CommonTableProps[] = [ { id: "instlCst", - title: "설치비용", + title: "설치비용(원)", upName: "rainFcltInfo", visible: true, row: 2, @@ -285,7 +285,7 @@ export const rainSttNodeListExport: CommonTableProps[] = [ { id: "pntfColKnd", - title: "집수지점종류", + title: "종류", upName: "pntfCol", visible: true, row: 3, @@ -294,7 +294,7 @@ export const rainSttNodeListExport: CommonTableProps[] = [ }, { id: "pntfColArea", - title: "집수지점면적", + title: "면적(m²)", upName: "pntfCol", visible: true, row: 3, @@ -304,7 +304,7 @@ export const rainSttNodeListExport: CommonTableProps[] = [ { id: "dtfcltCpct", - title: "저류조용량", + title: "저류조용량(m³)", upName: "rainFcltInfo", visible: true, row: 2, @@ -322,7 +322,7 @@ export const rainSttNodeListExport: CommonTableProps[] = [ }, { id: "filtrTrtmfctCpct", - title: "여과처리시설용량", + title: "여과처리시설용량(m³)", upName: "filtrTrtmfct", visible: true, row: 3, @@ -343,7 +343,7 @@ export const rainSttNodeListExport: CommonTableProps[] = [ id: "filtrTrtmfct", title: "여과등 처리시설", visible: true, - upName: "insenInfo", + upName: "rainFcltInfo", row: 2, rowSpan: 1, colSpan: 3, @@ -359,7 +359,7 @@ export const rainSttNodeListExport: CommonTableProps[] = [ }, { id: "instlCstSprtAmt", - title: "설치지원비용", + title: "설치지원비용(원)", upName: "instlCstSprt", visible: true, row: 3, @@ -379,7 +379,7 @@ export const rainSttNodeListExport: CommonTableProps[] = [ { id: "grntchBdstCertYn", - title: "녹색건축물인증여부", + title: "인증여부", upName: "grntchBdstCert", visible: true, row: 3, @@ -388,7 +388,7 @@ export const rainSttNodeListExport: CommonTableProps[] = [ }, { id: "grntchBdstCertYr", - title: "녹색건축물인증년도", + title: "인증년도", upName: "grntchBdstCert", visible: true, row: 3, @@ -397,7 +397,7 @@ export const rainSttNodeListExport: CommonTableProps[] = [ }, { id: "grntchBdstCertNo", - title: "녹색건축물인증번호", + title: "인증번호", upName: "grntchBdstCert", visible: true, row: 3, @@ -406,7 +406,7 @@ export const rainSttNodeListExport: CommonTableProps[] = [ }, { id: "grntchBdstCertGrd", - title: "녹색건축물인증등급", + title: "인증등급", upName: "grntchBdstCert", visible: true, row: 3, diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/rainSttn/index.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/rainSttn/index.tsx index 5aa54ee48..6c987298f 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/rainSttn/index.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/rainSttn/index.tsx @@ -1,5 +1,5 @@ -import type { rainChkZodType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn"; -import { fetchType, getFetch } from "@/components/lib/api"; +import type { rainChkZodType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn"; +import { fetchType, getFetch, getFetchWithCount } from "@/components/lib/api"; import { type customRainBdst, } from "../../fcltReg/rainReg"; @@ -16,34 +16,34 @@ import type { CommonTableProps } from "@/app/store/common/type/header"; import type { SelectDataType } from "@common/business_components/ui"; // 테이블 헤드 리스트 -const rainSttHeadList = rainSttHeadListExport; +const rainSttHeadList = rainSttHeadListExport; const rainSttNodeList = rainSttNodeListExport; const rainSttnDetailNodeList = rainSttnDetailNodeListExport; const rainSttnDetailHeadList = rainSttnDetailHeadListExport; const rainSttnRes: customRainBdst[] = []; -const rainChkRes : rainChkZodType ={} as rainChkZodType +const rainChkRes: rainChkZodType = {} as rainChkZodType -const visibleHeadList = (falseHeads:CommonTableProps[], orgNodeList:CommonTableProps[])=>{ +const visibleHeadList = (falseHeads: CommonTableProps[], orgNodeList: CommonTableProps[]) => { const visibleHeads = orgNodeList.map((node) => { - const copyNode = {...node} - falseHeads.forEach((head)=>{ - if(copyNode.id ===head.id) - copyNode.visible = head.visible + const copyNode = { ...node } + falseHeads.forEach((head) => { + if (copyNode.id === head.id) + copyNode.visible = head.visible }) return copyNode; }) return visibleHeads } -const getColspanReductionMap = (falseHeads:CommonTableProps[], orgList:CommonTableProps[]):Record<string, number> => { - const reduceMap:Record<string, number> = {}; +const getColspanReductionMap = (falseHeads: CommonTableProps[], orgList: CommonTableProps[]): Record<string, number> => { + const reduceMap: Record<string, number> = {}; falseHeads.forEach(head => { const node = orgList.find(n => n.id === head.id); if (node?.upName) { reduceMap[node.upName] = (reduceMap[node.upName] || 0) + 1; - if (node.row&& node.row >= 3) { + if (node.row && node.row >= 3) { const upperUpName = orgList.find(n => n.id === node.upName)?.upName; if (upperUpName) { reduceMap[upperUpName] = (reduceMap[upperUpName] || 0) + 1; @@ -78,35 +78,38 @@ const applyColspanReduction = ( }); }; // 일단 헤더가 3개 이상인경우 visible 처리를 위해서 공통함수로 작업 -export const controlHeadView = (headList:CommonTableProps[], orgNodeList:CommonTableProps[] ) =>{ +export const controlHeadView = (headList: CommonTableProps[], orgNodeList: CommonTableProps[]) => { const falseHeads = headList.filter((item) => item.visible == false); const viewHeadList = visibleHeadList(falseHeads, orgNodeList) - const reduceMap= getColspanReductionMap( falseHeads,viewHeadList) - return applyColspanReduction(viewHeadList,reduceMap); + const reduceMap = getColspanReductionMap(falseHeads, viewHeadList) + return applyColspanReduction(viewHeadList, reduceMap); } - - export type rainSttnState = { rainSttn: { rainSttnRes: customRainBdst[]; rainSttHeadList: CommonTableProps[]; rainSttNodeList: CommonTableProps[]; - rainSttnDetailHeadList:CommonTableProps[] - rainSttnDetailNodeList:CommonTableProps[] - rainChkRes:rainChkZodType + rainSttnDetailHeadList: CommonTableProps[] + rainSttnDetailNodeList: CommonTableProps[] + rainChkRes: rainChkZodType excelList: []; isInit: boolean; + rainSttnResCount: number; }; }; type rainSttnActions = { - rainSttnInit:(con:string) => void - rainSttnSearch: (page: number, total: string, con: string, sidoCode: SelectDataType[], sggCode: SelectDataType[]) => void; + rainSttnInit: (isExcel: boolean, page: number, total: string, con: string, txt: string, + sidoCode: SelectDataType[], + sggCode: SelectDataType[]) => void + rainSttnSearch: (isExcel: boolean, page: number, total: string, con: string, txt: string, + sidoCode: SelectDataType[], + sggCode: SelectDataType[]) => void; rainChSttnHead: ( headList: z.infer<typeof sttnHeadListType>, ) => void; - rainSttnChkSearch:(rusePk:string) => void; + rainSttnChkSearch: (rusePk: string) => void; }; export type rainSttnType = rainSttnState & rainSttnActions; @@ -115,12 +118,13 @@ export const rainSttnStateExport: rainSttnState = { rainSttn: { rainSttHeadList: rainSttHeadList, rainSttNodeList: rainSttNodeList, - rainSttnDetailHeadList:rainSttnDetailHeadList, - rainSttnDetailNodeList:rainSttnDetailNodeList, + rainSttnDetailHeadList: rainSttnDetailHeadList, + rainSttnDetailNodeList: rainSttnDetailNodeList, rainSttnRes: rainSttnRes, - rainChkRes:rainChkRes, + rainChkRes: rainChkRes, excelList: [], isInit: false, + rainSttnResCount: 1, }, }; @@ -128,36 +132,20 @@ export const rainSttnActionsExport: (set: any) => rainSttnActions = ( set: any ) => { return { - rainSttnInit: async( con:string)=>{ - try{ - - const rainSttnRes = await getFetch( - fetchType.WR, - `/reuseFc/rainyInfoList/1/20/${con}` - ); - set((state: rainSttnState) => { - return { - ...state, - rainSttn: { - ...state.rainSttn, - rainSttnRes: rainSttnRes, - isInit: false, - }, - }; - }); - }catch(e){ - console.log(e) - } - }, - rainSttnSearch: async (page: number, total: string, con: string, - sidoCode: SelectDataType[], - sggCode: SelectDataType[]) => { + rainSttnInit: async (isExcel: boolean, page: number, total: string, con: string, txt: string, + sidoCode: SelectDataType[], + sggCode: SelectDataType[]) => { try { - const rainSttnRes = await getFetch( + + const rainSttnRes = await getFetchWithCount( fetchType.WR, - `/reuseFc/rainyInfoList/${page}/${total}/${con}` + `/reuseFc/rainyInfoList` + + (page ? `/${page}` : "/1") + + (total ? `/${total}` : "/20") + + (con ? `/${con}` : "/0") + + (txt ? `/${txt}` : "") ); - if (Number(total) > 9000) { + if (isExcel) { const rainExcel = excelRainSttnParsing(rainSttnRes, sidoCode, sggCode); set((state: rainSttnState) => { return { @@ -168,13 +156,54 @@ export const rainSttnActionsExport: (set: any) => rainSttnActions = ( }, }; }); + } else { + set((state: rainSttnState) => { + return { + ...state, + rainSttn: { + ...state.rainSttn, + rainSttnRes: rainSttnRes.data, + isInit: false, + }, + }; + }); + } + } catch (e) { + console.log(e) + } + }, + rainSttnSearch: async (isExcel: boolean, page: number, total: string, con: string, txt: string, + sidoCode: SelectDataType[], + sggCode: SelectDataType[]) => { + try { + const rainSttnRes = await getFetchWithCount( + fetchType.WR, + `/reuseFc/rainyInfoList` + + (page ? `/${page}` : "/1") + + (total ? `/${total}` : "/20") + + (con ? `/${con}` : "/0") + + (txt ? `/${txt}` : "") + ); + if (isExcel) { + const rainExcel = excelRainSttnParsing(rainSttnRes, sidoCode, sggCode); + set((state: rainSttnState) => { + return { + ...state, + rainSttn: { + ...state.rainSttn, + excelList: rainExcel, + rainSttnResCount: rainSttnRes.count, + }, + }; + }); } else set((state: rainSttnState) => { return { ...state, rainSttn: { ...state.rainSttn, - rainSttnRes: rainSttnRes, + rainSttnRes: rainSttnRes.data, + rainSttnResCount: rainSttnRes.count, isInit: false, }, }; @@ -186,7 +215,7 @@ export const rainSttnActionsExport: (set: any) => rainSttnActions = ( rainChSttnHead: async ( headList: z.infer<typeof sttnHeadListType> ) => { - const changeNodeList= controlHeadView(headList,rainSttNodeList) + const changeNodeList = controlHeadView(headList, rainSttNodeList) return set((state: rainSttnState) => ({ rainSttn: { @@ -196,21 +225,21 @@ export const rainSttnActionsExport: (set: any) => rainSttnActions = ( }, })); }, - rainSttnChkSearch: async( - rusePk:string - )=>{ + rainSttnChkSearch: async ( + rusePk: string + ) => { const rainChkRes = await getFetch( fetchType.WR, `/reuseChk/rainy/${rusePk}` ); - - return set((state: rainSttnState) =>({ + + return set((state: rainSttnState) => ({ rainSttn: { ...state.rainSttn, - rainChkRes:rainChkRes + rainChkRes: rainChkRes }, - }) ) + })) } }; diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/rswtSttn/excel.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/rswtSttn/excel.tsx index ac57a8016..803dc2d83 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/rswtSttn/excel.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/rswtSttn/excel.tsx @@ -1,35 +1,35 @@ +import { bdstUsgCd } from "@/app/store/common/code/basicCode"; import type { SelectDataType } from "@common/business_components/ui"; import type { customRswtBdst } from "../../fcltReg/rswtReg"; -import { useWrStore } from "@/app/store"; export const excelRSwtSttnParsing = ( - data: customRswtBdst[], + excel: { data: customRswtBdst[] }, sidoCode: SelectDataType[], sggCode: SelectDataType[] ): string[][] => { const excelExportData: string[][] = []; - - data.forEach((item) => { + excel.data.forEach((item) => { + console.log("item", item) excelExportData.push([ sidoCode.find((sido) => sido.val === item.ctpvCd.toString())?.text ?? "", sggCode.find((sgg) => sgg.val === item.sggCd.toString())?.text ?? "", item.bdstNm ?? "", item.daddr ?? "", - String(item.bdstUsgText ?? ""), + bdstUsgCd.find((cd)=> cd.val === item.reuseBuilding?.[0]?.bdstUsgCd.toString())?.text ?? "", //건축물용도 String(item.frstArchPrmsnDt ?? ""), - String(item.archArea ?? ""), - String(item.reuseBuilding?.[0]?.rswtrObj.wtrUsqty ?? "0"), //물사용량 + String(item.archArea.split('.')[0] === '-1' ? '확인불가' : item.archArea.split('.')[0]), + // String(item.reuseBuilding?.[0]?.rswtrObj.wtrUsqty ?? "0"), //물사용량 String(item.chgBuilding.length > 0 ? "Y" : "N"), //중축・개축・재축여부 String(item.reuseBuilding?.[0]?.lawTrgtYn ?? ""), //법적의무대상여부 - String(item.reuseBuilding?.[0]?.rswtrObj.oblgTrgtWtrUsqty ?? ""), //의무대상물사용량 + // String(item.reuseBuilding?.[0]?.rswtrObj.oblgTrgtWtrUsqty ?? ""), //의무대상물사용량 String(item.reuseBuilding?.[0]?.instlCmptnDt ?? ""), //설치완료일시 - String(item.reuseBuilding?.[0]?.instlCst ?? ""), //설치비용 + String(item.reuseBuilding?.[0]?.instlCst === -1 ? "확인불가" : item.reuseBuilding?.[0]?.instlCst ?? ""), //설치비용 item.reuseBuilding?.[0]?.rswtrObj?.orgwtKndCd ?? "", //원수종류 String(item.reuseBuilding?.[0]?.rswtrObj.filtrTrtmfctCpct ?? ""), //처리시설용량 String(item.reuseBuilding?.[0]?.rswtrObj?.filtrTrtmfctProcs ?? ""), //주요처리공정 String(item.reuseBuilding?.[0]?.rswtrObj?.filtrTrtmfctUsgCd ?? ""), //이용용도 String(item.reuseBuilding?.[0]?.instlCstSprtYn ?? ""), //설치지원비용여부 - String(item.reuseBuilding?.[0]?.instlCstSprtAmt ?? ""), //설치지원비용 + String(item.reuseBuilding?.[0]?.instlCstSprtAmt === -1 ? "확인불가" : item.reuseBuilding?.[0]?.instlCstSprtAmt ?? ""), //설치지원비용 String(item.reuseBuilding?.[0]?.grntchBdstCertYn ?? ""), String(item.reuseBuilding?.[0]?.grntchBdstCertYr ?? ""), String(item.reuseBuilding?.[0]?.grntchBdstCertNo ?? ""), diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/rswtSttn/headList.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/rswtSttn/headList.tsx index 804617ad8..e4bcce08a 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/rswtSttn/headList.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/rswtSttn/headList.tsx @@ -38,11 +38,11 @@ export const rswtSttHeadListExport: z.infer<typeof sttnHeadListType> = [ title: "건축면적", visible: true, }, - { - id: "wtrUsqty", - title: "물사용량", - visible: true, - }, + // { + // id: "wtrUsqty", + // title: "물사용량", + // visible: true, + // }, { id: "chBuildingYn", title: "중축・개축・재축여부", @@ -54,12 +54,11 @@ export const rswtSttHeadListExport: z.infer<typeof sttnHeadListType> = [ title: "법적의무대상여부", visible: true, }, - - { - id: "oblgTrgtWtrUsqty", - title: "의무대상물사용량 ", - visible: true, - }, + // { + // id: "oblgTrgtWtrUsqty", + // title: "의무대상물사용량 ", + // visible: true, + // }, { id: "instlCmptnDt", title: "설치완료일시", @@ -130,15 +129,15 @@ export const rswtSttHeadListExport: z.infer<typeof sttnHeadListType> = [ export const rswtSttNodeListExport: CommonTableProps[] = [ { id: "bdnstInfo", - title: "1.건축물정보", + title: "건축물정보", visible: true, row: 1, rowSpan: 1, - colSpan: 12, + colSpan: 10, }, { id: "rswtFcltInfo", - title: "2.중수도시설정보", + title: "중수도시설정보", visible: true, row: 1, rowSpan: 1, @@ -146,7 +145,7 @@ export const rswtSttNodeListExport: CommonTableProps[] = [ }, { id: "insenInfo", - title: "3.인센티브 정보", + title: "인센티브 정보", visible: true, row: 1, rowSpan: 1, @@ -222,22 +221,22 @@ export const rswtSttNodeListExport: CommonTableProps[] = [ }, { id: "archArea", - title: "건축면적", - upName: "bdnstInfo", - visible: true, - row: 2, - rowSpan: 2, - colSpan: 1, - }, - { - id: "wtrUsqty", - title: "물사용량", + title: "건축면적(m²)", upName: "bdnstInfo", visible: true, row: 2, rowSpan: 2, colSpan: 1, }, + // { + // id: "wtrUsqty", + // title: "물사용량", + // upName: "bdnstInfo", + // visible: true, + // row: 2, + // rowSpan: 2, + // colSpan: 1, + // }, { id: "chBuildingYn", title: "중축・개축・재축여부", @@ -257,15 +256,15 @@ export const rswtSttNodeListExport: CommonTableProps[] = [ rowSpan: 2, colSpan: 1, }, - { - id: "oblgTrgtWtrUsqty", - title: "의무대상물사용량", - upName: "bdnstInfo", - visible: true, - row: 2, - rowSpan: 2, - colSpan: 1, - }, + // { + // id: "oblgTrgtWtrUsqty", + // title: "의무대상물사용량", + // upName: "bdnstInfo", + // visible: true, + // row: 2, + // rowSpan: 2, + // colSpan: 1, + // }, { id: "instlCmptnDt", title: "설치완료일시", @@ -277,7 +276,7 @@ export const rswtSttNodeListExport: CommonTableProps[] = [ }, { id: "instlCst", - title: "설치비용", + title: "설치비용(원)", upName: "rswtFcltInfo", visible: true, row: 2, @@ -297,7 +296,7 @@ export const rswtSttNodeListExport: CommonTableProps[] = [ { id: "filtrTrtmfctCpct", - title: "처리시설용량", + title: "처리시설용량(m³)", upName: "rswtFcltInfo", visible: true, row: 2, @@ -325,7 +324,7 @@ export const rswtSttNodeListExport: CommonTableProps[] = [ }, { id: "instlCstSprtYn", - title: "설치지원비용여부", + title: "지원여부", upName: "instlCstSprt", visible: true, row: 3, @@ -334,7 +333,7 @@ export const rswtSttNodeListExport: CommonTableProps[] = [ }, { id: "instlCstSprtAmt", - title: "설치지원비용", + title: "지원비용(원)", upName: "instlCstSprt", visible: true, row: 3, @@ -354,7 +353,7 @@ export const rswtSttNodeListExport: CommonTableProps[] = [ { id: "grntchBdstCertYn", - title: "녹색건축물인증여부", + title: "인증여부", upName: "grntchBdstCert", visible: true, row: 3, @@ -363,7 +362,7 @@ export const rswtSttNodeListExport: CommonTableProps[] = [ }, { id: "grntchBdstCertYr", - title: "녹색건축물인증년도", + title: "인증년도", upName: "grntchBdstCert", visible: true, row: 3, @@ -372,7 +371,7 @@ export const rswtSttNodeListExport: CommonTableProps[] = [ }, { id: "grntchBdstCertNo", - title: "녹색건축물인증번호", + title: "인증번호", upName: "grntchBdstCert", visible: true, row: 3, @@ -381,7 +380,7 @@ export const rswtSttNodeListExport: CommonTableProps[] = [ }, { id: "grntchBdstCertGrd", - title: "녹색건축물인증등급", + title: "인증등급", upName: "grntchBdstCert", visible: true, row: 3, diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/rswtSttn/index.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/rswtSttn/index.tsx index 301cd965e..97c9dfe5b 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/rswtSttn/index.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/rswtSttn/index.tsx @@ -1,12 +1,12 @@ import type { CommonTableProps } from "@/app/store/common/type/header"; -import { fetchType, getFetch } from "@/components/lib/api"; +import { fetchType, getFetchWithCount } from "@/components/lib/api"; +import type { SelectDataType } from "@common/business_components/ui"; import type { z } from "zod"; import type { customRswtBdst } from "../../fcltReg/rswtReg"; import { controlHeadView } from "../rainSttn"; import type { sttnHeadListType } from "../rainSttn/headList"; import { excelRSwtSttnParsing } from "./excel"; import { rswtSttHeadListExport, rswtSttNodeListExport } from "./headList"; -import type { SelectDataType } from "@common/business_components/ui"; @@ -24,12 +24,16 @@ export type rswtSttnState = { rswtSttNodeList: CommonTableProps[]; excelList: []; isInit: boolean; + rswtSttnResCount: number; }; }; type rswtSttnActions = { - rswtSttnInit: (con: string) => void; - rswtSttnSearch: (page: number, total: string, con: string, sidoCode: SelectDataType[], + rswtSttnInit: (isExcel: boolean, page: number, total: string, con: string, txt: string, + sidoCode: SelectDataType[], + sggCode: SelectDataType[]) => void; + rswtSttnSearch: (isExcel: boolean, page: number, total: string, con: string, txt: string, + sidoCode: SelectDataType[], sggCode: SelectDataType[]) => void; rswtChSttnHead: ( headList: z.infer<typeof sttnHeadListType>, @@ -45,6 +49,7 @@ export const rswtSttnStateExport: rswtSttnState = { rswtSttnRes: rswtSttnRes, excelList: [], isInit: false, + rswtSttnResCount: 1, }, }; @@ -54,36 +59,19 @@ export const rswtSttnActionsExport: (set: any) => rswtSttnActions = ( set: any ) => { return { - rswtSttnInit: async (con: string) => { - try { - - const rswtSttnRes = await getFetch( - fetchType.WR, - `/reuseFc/waterreuseFc/1/20/${con}` - ); - set((state: rswtSttnState) => { - return { - ...state, - rswtSttn: { - ...state.rswtSttn, - rswtSttnRes: rswtSttnRes, - isInit: false, - }, - }; - }); - } catch (e) { - console.log(e) - } - }, - rswtSttnSearch: async (page: number, total: string, con: string, + rswtSttnInit: async (isExcel: boolean, page: number, total: string, con: string, txt: string, sidoCode: SelectDataType[], sggCode: SelectDataType[]) => { try { - const rswtSttnRes = await getFetch( + const rswtSttnRes = await getFetchWithCount( fetchType.WR, - `/reuseFc/waterreuseFc/${page}/${total}/${con}` + `/reuseFc/waterreuseFc` + + (page ? `/${page}` : "/1") + + (total ? `/${total}` : "/20") + + (con ? `/${con}` : "/0") + + (txt ? `/${txt}` : "") ); - if (Number(total) > 9000) { + if (isExcel) { const rswtExcel = excelRSwtSttnParsing(rswtSttnRes, sidoCode, sggCode); set((state: rswtSttnState) => { return { @@ -101,7 +89,48 @@ export const rswtSttnActionsExport: (set: any) => rswtSttnActions = ( ...state, rswtSttn: { ...state.rswtSttn, - rswtSttnRes: rswtSttnRes, + rswtSttnRes: rswtSttnRes.data, + rswtSttnResCount: rswtSttnRes.count, + isInit: false, + }, + }; + }); + } catch (e) { + console.log(e) + } + }, + rswtSttnSearch: async (isExcel: boolean, page: number, total: string, con: string, txt: string, + sidoCode: SelectDataType[], + sggCode: SelectDataType[]) => { + try { + const rswtSttnRes = await getFetchWithCount( + fetchType.WR, + `/reuseFc/waterreuseFc` + + (page ? `/${page}` : "/1") + + (total ? `/${total}` : "/20") + + (con ? `/${con}` : "/0") + + (txt ? `/${txt}` : "") + ); + if (isExcel) { + const rswtExcel = excelRSwtSttnParsing(rswtSttnRes, sidoCode, sggCode); + set((state: rswtSttnState) => { + return { + ...state, + rswtSttn: { + ...state.rswtSttn, + excelList: rswtExcel, + }, + }; + }); + } else + set((state: rswtSttnState) => { + + return { + ...state, + rswtSttn: { + ...state.rswtSttn, + rswtSttnRes: rswtSttnRes.data, + rswtSttnResCount: rswtSttnRes.count, isInit: false, }, }; diff --git a/apps/wr/app/store/stats/matStat/headList.tsx b/apps/wr/app/store/stats/matStat/headList.tsx deleted file mode 100644 index 881662774..000000000 --- a/apps/wr/app/store/stats/matStat/headList.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import type { TableUpperProps } from "../../common/type/header"; - -export const matStatHeadListExport :TableUpperProps[]=[ - { - id: "시도", - title: "시도", - row:0, - rowSpan:2 - }, - { - id: "개소", - title: "개소", - row:0, - rowSpan:2 - }, - { - id: "요구수량", - title: "요구수량", - row:0, - rowSpan:2 - }, - - - { - id: "용도", - title: "용도", - row:0, - colSpan:6 - }, - { - id: "조경용수", - title: "조경용수", - row:1, - }, - { - id: "청소화장실용수", - title: "청소화장실용수", - row:1, - }, - { - id: "농업용수", - title: "농업용수", - row:1, - }, - { - id: "공업용수", - title: "공업용수", - row:1, - }, - { - id: "친수용수", - title: "친수용수", - row:1, - }, - { - id: "기타", - title: "기타", - row:1, - }, - { - id: "사업비", - title: "사업비(천원)", - row:0, - rowSpan:2 - }, - - -] \ No newline at end of file diff --git a/apps/wr/app/store/stats/matStat/index.tsx b/apps/wr/app/store/stats/matStat/index.tsx deleted file mode 100644 index 3f2e5813f..000000000 --- a/apps/wr/app/store/stats/matStat/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { TableUpperProps } from "../../common/type/header"; -import { matStatHeadListExport } from "./headList"; - -type matStatActions = { -} - - -export type matStatState = { - matStat: { - matStatHeadList:TableUpperProps[] - } -} - -export type matStatType = matStatState & matStatActions; - -export const matStatExport: matStatState = { - matStat: { - matStatHeadList:matStatHeadListExport - } -} - -export const matStatActionsExport: (set: any) => matStatActions = ( - set: any -) => { - return { - - } -} \ No newline at end of file diff --git a/apps/wr/app/store/stats/rainStat/index.tsx b/apps/wr/app/store/stats/rainStat/index.tsx index d6e48a53e..ec46c2b4e 100644 --- a/apps/wr/app/store/stats/rainStat/index.tsx +++ b/apps/wr/app/store/stats/rainStat/index.tsx @@ -2,157 +2,215 @@ import { fetchType, getFetch } from "@/components/lib/api"; import type { TableUpperProps } from "../../common/type/header"; import { excelRainStat } from "./excel"; import { rainStatHeadListExport } from "./headList"; +import type { SelectDataType } from "@common/business_components/ui"; export type RainStatProps = { - ctpvCd: string //시도 - sggCd: string //시군구 - areaNm: string //행정구역명 - bdstNm: string //시설명 - bdstUsgCd: string //건축물용도코드 - daddr: string //주소 - lawTrgtYn: string //법적의무대상여부 - instlCmptnDt: string //설치완료일자 - instlCst: number //설치비용 - pntfColKnd: string //접수면종류 - filtrTrtmfctYn: string //여과등처리시설유무 - archArea: number //건축연면적 - dtfcltCpct: number //처리용량(저류조용량) - wtrUsqty: number //연간사용량 - utztnUsgCd: string //이용용도코드 - anlOperCst: number //연간운영비 - annRnwtQty: number //물사용량 - jungsudo: number //중수도​이용량 - dachae: number //대체수​이용량 - lndwtr: number //조경용수 - cleanToilUswtr: number //청소화장실용수 - resn: number //세척살수용수 - farmUswtr: number //농업용수 - indstUswtr: number //공업용수 - hydUswtr: number //친수용수 - ithlEtcUswtr: number //기타 - tmp: number //개소 - guideStats: GuideStatProps[] -} + cityText: string; + ctpvCd: string; //시도 + sggCd: string; //시군구 + areaNm: string; //행정구역명 + bdstNm: string; //시설명 + bdstUsgCd: string; //건축물용도코드 + daddr: string; //주소 + lawTrgtYn: string; //법적의무대상여부 + instlCmptnDt: string; //설치완료일자 + instlCst: number; //설치비용 + pntfColKnd: string; //접수면종류 + filtrTrtmfctYn: string; //여과등처리시설유무 + archArea: number; //건축연면적 + dtfcltCpct: number; //처리용량(저류조용량) + wtrUsqty: number; //연간사용량 + utztnUsgCd: string; //이용용도코드 + anlOperCst: number; //연간운영비 + annRnwtQty: number; //물사용량 + jungsudo: number; //중수도​이용량 + dachae: number; //대체수​이용량 + lndwtr: number; //조경용수 + cleanToilUswtr: number; //청소화장실용수 + resn: number; //세척살수용수 + farmUswtr: number; //농업용수 + indstUswtr: number; //공업용수 + hydUswtr: number; //친수용수 + ithlEtcUswtr: number; //기타 + tmp: number; //개소 + guideStats: GuideStatProps[]; +}; export type GuideStatProps = { - utztnUsgCd: string - splyUswtrUtztnQnt : number, - ruseFcltUtztnQnt: number -} + utztnUsgCd: string; + splyUswtrUtztnQnt: number; + ruseFcltUtztnQnt: number; +}; type ExcelType = { - [key: string]: string | number | undefined; + [key: string]: string | number | undefined; }; const excelDownList: ExcelType[] = []; const fetchRainStatList = async (year: string) => { - const res = await getFetch(fetchType.WR, `/reuseChk/rainy/stat/${year}`) - return res; -} + const res = await getFetch(fetchType.WR, `/reuseChk/rainy/stat/${year}`); + return res; +}; type rainStatActions = { - searchRainStatList: (year: string) => void -} + searchRainStatList: (year: string, city: SelectDataType[]) => void; +}; export type rainStatState = { - rainStat: { - rainStatHeadList: TableUpperProps[], - rainStatList: RainStatProps[], - excelList: ExcelType[] - rainDetailList : [][] - } -} + rainStat: { + rainStatHeadList: TableUpperProps[]; + rainStatList: RainStatProps[]; + excelList: ExcelType[]; + rainDetailList: [][]; + }; +}; export type rainStatType = rainStatState & rainStatActions; export const rainStatExport: rainStatState = { - rainStat: { - rainStatHeadList: rainStatHeadListExport, - rainStatList: [], - excelList: [], - rainDetailList : [] - } -} + rainStat: { + rainStatHeadList: rainStatHeadListExport, + rainStatList: [], + excelList: [], + rainDetailList: [], + }, +}; export const rainStatActionsExport = ( - set: (fn: (state: rainStatState) => rainStatState) => void + set: (fn: (state: rainStatState) => rainStatState) => void ) => { - return { - searchRainStatList: async (year: string) => { - const res = await fetchRainStatList(year); - const cityArr: any[] = []; - res.forEach((item: RainStatProps) => { + return { + searchRainStatList: async (year: string, city: SelectDataType[]) => { + const res = await fetchRainStatList(year); + const cityArr: any[] = []; + - let obj = cityArr.find((e) => { - return e.ctpvCd == item.ctpvCd - }) + res.filter((resFilter: RainStatProps) => + city.some((cityCd) => + resFilter.ctpvCd?.trim() === cityCd.val.trim() + ) + ) + .forEach((item: RainStatProps) => { + let obj = cityArr.find((e) => { + return e.ctpvCd == item.ctpvCd; + }); - if (obj) { - obj.instlCst += item.instlCst - obj.dtfcltCpct += item.dtfcltCpct - obj.wtrUsqty += item.wtrUsqty - obj.anlOperCst += item.anlOperCst - obj.tmp += 1 - } else { - obj = {}; - obj.ctpvCd = item.ctpvCd - obj.instlCst = item.instlCst - obj.tmp = 1 - obj.dtfcltCpct = item.dtfcltCpct - obj.wtrUsqty = item.wtrUsqty - obj.anlOperCst = item.anlOperCst + if (obj) { + obj.instlCst += item.instlCst; + obj.dtfcltCpct += item.dtfcltCpct; + obj.wtrUsqty += item.wtrUsqty; + obj.anlOperCst += item.anlOperCst; + obj.tmp += 1; + } else { + obj = {}; + obj.ctpvCd = item.ctpvCd; + obj.instlCst = item.instlCst; + obj.tmp = 1; + obj.dtfcltCpct = item.dtfcltCpct; + obj.wtrUsqty = item.wtrUsqty; + obj.anlOperCst = item.anlOperCst; - cityArr.push(obj) + cityArr.push(obj); + } + + if (item.guideStats && item.guideStats.length > 0) { + item.guideStats.forEach((guide: GuideStatProps) => { + rainStatHeadListExport.forEach((headItem: TableUpperProps) => { + if (headItem.title == guide.utztnUsgCd) { + if (!obj[headItem.id] || typeof obj[headItem.id] !== "number") + obj[headItem.id] = 0; + if (typeof obj[headItem.id] === "number") { + obj[headItem.id] += guide.ruseFcltUtztnQnt; + } } + }); + }); + } + }); - if (item.guideStats && item.guideStats.length > 0) { - item.guideStats.forEach((guide: GuideStatProps) => { - rainStatHeadListExport.forEach((headItem: TableUpperProps) => { + const totalObj = { + ctpvCd: "0", + tmp: 0, + instlCst: 0, //설치비 + dtfcltCpct: 0, //저류조용량 + bdstUsgCd: 0, //연간사용량 + lndwtr: 0, //조경용수 + cleanToilUswtr: 0, //청소화장실용수 + resn: 0, //세척살수용수 + farmUswtr: 0, //농업용수 + indstUswtr: 0, //공업용수 + hydUswtr: 0, //친수용수 + ithlEtcUswtr: 0, //기타 + anlOperCst: 0, //연간운영비 + } as any; - if (headItem.title == guide.utztnUsgCd) { - if (!obj[headItem.id] || typeof obj[headItem.id] !== "number") obj[headItem.id] = 0; - if (typeof obj[headItem.id] === "number") { + cityArr.forEach((item) => { + // if(item.ctpvCd === '0') return; // Skip total object if exists + totalObj.tmp += !isNaN(Number(item.tmp)) ? Number(item.tmp) : 0; + totalObj.instlCst += !isNaN(Number(item.instlCst)) + ? Number(item.instlCst) + : 0; + totalObj.dtfcltCpct += !isNaN(Number(item.dtfcltCpct)) + ? Number(item.dtfcltCpct) + : 0; + totalObj.bdstUsgCd += !isNaN(Number(item.bdstUsgCd)) + ? Number(item.bdstUsgCd) + : 0; + totalObj.lndwtr += !isNaN(Number(item.lndwtr)) + ? Number(item.lndwtr) + : 0; + totalObj.cleanToilUswtr += !isNaN(Number(item.cleanToilUswtr)) + ? Number(item.cleanToilUswtr) + : 0; + totalObj.resn += !isNaN(Number(item.resn)) ? Number(item.resn) : 0; + totalObj.farmUswtr += !isNaN(Number(item.farmUswtr)) + ? Number(item.farmUswtr) + : 0; + totalObj.indstUswtr += !isNaN(Number(item.indstUswtr)) + ? Number(item.indstUswtr) + : 0; + totalObj.hydUswtr += !isNaN(Number(item.hydUswtr)) + ? Number(item.hydUswtr) + : 0; + totalObj.ithlEtcUswtr += !isNaN(Number(item.ithlEtcUswtr)) + ? Number(item.ithlEtcUswtr) + : 0; + totalObj.anlOperCst += !isNaN(Number(item.anlOperCst)) + ? Number(item.anlOperCst) + : 0; - obj[headItem.id] += guide.ruseFcltUtztnQnt; - } - } - }); - }) - } - }) + excelDownList.push({ + ctpvCd: item.ctpvCd, + tmp: item.tmp, + instlCst: item.instlCst, //설치비 + dtfcltCpct: item.dtfcltCpct, //저류조용량 + bdstUsgCd: item.bdstUsgCd, //연간사용량 + lndwtr: item.lndwtr, //조경용수 + cleanToilUswtr: item.cleanToilUswtr, //청소화장실용수 + resn: item.resn, //세척살수용수 + farmUswtr: item.farmUswtr, //농업용수 + indstUswtr: item.indstUswtr, //공업용수 + hydUswtr: item.hydUswtr, //친수용수 + ithlEtcUswtr: item.ithlEtcUswtr, //기타 + anlOperCst: item.anlOperCst, //연간운영비 + }); + }); - cityArr.forEach(item => { - excelDownList.push({ - ctpvCd: item.ctpvCd, - tmp: item.tmp, - instlCst: item.instlCst, //설치비 - dtfcltCpct: item.dtfcltCpct, //저류조용량 - bdstUsgCd: item.bdstUsgCd, //연간사용량 - lndwtr: item.lndwtr, //조경용수 - cleanToilUswtr: item.cleanToilUswtr, //청소화장실용수 - resn: item.resn, //세척살수용수 - farmUswtr: item.farmUswtr, //농업용수 - indstUswtr: item.indstUswtr, //공업용수 - hydUswtr: item.hydUswtr, //친수용수 - ithlEtcUswtr: item.ithlEtcUswtr, //기타 - anlOperCst: item.anlOperCst, //연간운영비 - }) - }) - //상세 - const rainDetailList : RainStatProps[] = []; - res.forEach((item:RainStatProps) => { - const utztnUsgArray = new Array<string>(); - item.guideStats.forEach((guideStat) => { - utztnUsgArray.push(guideStat.utztnUsgCd); - }) - rainDetailList.push({ - ...item, - utztnUsgCd : utztnUsgArray.join(', '), - }) - }) - const excelDetailData = excelRainStat(rainDetailList); - /* const rainStatHeadList = data.map((item:RainStatProps) => ({ + //상세 + const rainDetailList: RainStatProps[] = []; + res.forEach((item: RainStatProps) => { + const utztnUsgArray = new Array<string>(); + item.guideStats.forEach((guideStat) => { + utztnUsgArray.push(guideStat.utztnUsgCd); + }); + rainDetailList.push({ + ...item, + utztnUsgCd: utztnUsgArray.join(", "), + }); + }); + const excelDetailData = excelRainStat(rainDetailList); + /* const rainStatHeadList = data.map((item:RainStatProps) => ({ ...item, ctpvCd:item.ctpvCd, sggCd:item.sggCd, @@ -171,14 +229,19 @@ export const rainStatActionsExport = ( way7:item.way7, yearMoney:item.yearMoney, })) */ - set((state: rainStatState) => ({ - rainStat: { - ...state.rainStat, - rainDetailList: excelDetailData as [][], - rainStatList: cityArr, - excelList: excelDownList - } - })); - } - } -} + + console.log(" cityArr ", cityArr); + console.log(" excelDownList ", excelDownList); + + cityArr.unshift(totalObj); + set((state: rainStatState) => ({ + rainStat: { + ...state.rainStat, + rainDetailList: excelDetailData as [][], + rainStatList: cityArr, + excelList: excelDownList, + }, + })); + }, + }; +}; diff --git a/apps/wr/app/store/stats/rswtStat/excel.tsx b/apps/wr/app/store/stats/rswtStat/excel.tsx index 35f567253..00935b8fb 100644 --- a/apps/wr/app/store/stats/rswtStat/excel.tsx +++ b/apps/wr/app/store/stats/rswtStat/excel.tsx @@ -1,4 +1,5 @@ import type { RswtStatProps } from "."; +import { bdstUsgCd } from "../../common/code/basicCode"; export const excelRswtStat = (excelList:RswtStatProps[]) : (string | number)[][] => { const excelData: (string | number)[][] = []; @@ -14,7 +15,7 @@ export const excelRswtStat = (excelList:RswtStatProps[]) : (string | number)[][] String(item.bdstNm) ?? "", //건축물용도코드(빈칸) //String(item.bdstUsgCd) ?? "", - "", + bdstUsgCd.find((cd)=> cd.val === item.bdstUsgCd.toString())?.text ?? "", //건축물용도 //주소 String(item.daddr) ?? "", //법적의무대상여부 @@ -25,7 +26,7 @@ export const excelRswtStat = (excelList:RswtStatProps[]) : (string | number)[][] //String(item.instlCst) ?? "", "", //건축물면적 - item.archArea ?? "", + // item.archArea ?? "", //처리용량 item.filtrTrtmfctCpct ?? "", //물사용량 diff --git a/apps/wr/app/store/stats/rswtStat/headList.tsx b/apps/wr/app/store/stats/rswtStat/headList.tsx index 877089092..c0860fea7 100644 --- a/apps/wr/app/store/stats/rswtStat/headList.tsx +++ b/apps/wr/app/store/stats/rswtStat/headList.tsx @@ -13,21 +13,22 @@ export const rswtStatHeadListExport :TableUpperProps[]=[ row:0, rowSpan:2 }, - { - id: "a", - title: "시설현황", - row:0, - colSpan:2 - }, - { - id: "archArea", - title: "건축연면적(m³)", - row:1, - }, + // { + // id: "a", + // title: "시설현황", + // row:0, + // colSpan:2 + // }, + // { + // id: "archArea", + // title: "건축연면적(m³)", + // row:1, + // }, { id: "filtrTrtmfctCpct", title: "처리용량(m³)", - row:1, + row:0, + rowSpan:2 }, { id: "wtrUsqty", diff --git a/apps/wr/app/store/stats/rswtStat/index.tsx b/apps/wr/app/store/stats/rswtStat/index.tsx index 4e2a7ecc8..89c4089c0 100644 --- a/apps/wr/app/store/stats/rswtStat/index.tsx +++ b/apps/wr/app/store/stats/rswtStat/index.tsx @@ -4,8 +4,9 @@ import { rswtStatHeadListExport } from "./headList"; import { excelRswtStat } from "./excel"; export type RswtStatProps = { + cityText: string //시도명 ctpvCd: string //시도 - sggCd : string //시군구 + sggCd: string //시군구 areaNm: string //행정구역명 bdstNm: string //시설명 daddr: string //주소 @@ -25,9 +26,10 @@ export type RswtStatProps = { ithlEtcUswtr: number //기타 anlOperCst: number //연간운영비 guideStats: GuideStatProps[] - utztnUsgCd : string //이용코드 - lawTrgtYn : string //법적의무대상여부 - instlCmptnDt : string //설치완료일자 + utztnUsgCd: string //이용코드 + lawTrgtYn: string //법적의무대상여부 + instlCmptnDt: string //설치완료일자 + bdstUsgCd: number } export type GuideStatProps = { @@ -36,7 +38,7 @@ export type GuideStatProps = { } type rswtStatActions = { - searchRswtStatList: (year: string) => void; + searchRswtStatList: (year: string, city: { val: string; text: string }[]) => void; } export type ExcelType = { @@ -70,37 +72,44 @@ export const rswtStatActionsExport: (set: any) => rswtStatActions = ( set: any ) => { return { - searchRswtStatList: async (year: string) => { + searchRswtStatList: async (year: string, city: { val: string; text: string }[]) => { const res = await getFetch(fetchType.WR, `/reuseChk/reuseWater/stat/${year}`) - const rswtStatList: RswtStatProps[] = []; - + const data = res.filter(item => { + const code = item.ctpvCd.trim(); + return city.some(el => el.val === code); + }); const cityArr: any[] = []; - res.forEach((item: RswtStatProps) => { + data.forEach((item: RswtStatProps) => { let obj = cityArr.find((e) => { return e.ctpvCd == item.ctpvCd }) if (obj) { - obj.ctpvCnt += 1 - obj.archArea += item.archArea - obj.filtrTrtmfctCpct += item.filtrTrtmfctCpct - obj.wtrUsqty += item.wtrUsqty - obj.rswtAltSum += (item.rswtrUsqty + item.altUswtrUtztnQnt) - obj.rswtrUsqty += item.rswtrUsqty - obj.altUswtrUtztnQnt += item.altUswtrUtztnQnt - obj.anlOperCst += item.anlOperCst + obj.ctpvCnt += 1 //개소 + // obj.archArea += item.archArea 건축면적 삭제 + obj.filtrTrtmfctCpct += item.filtrTrtmfctCpct //처리용량 + obj.wtrUsqty += item.wtrUsqty //물사용량 + obj.rswtAltSum += (item.rswtrUsqty + item.altUswtrUtztnQnt) //중수도+대체수 + obj.rswtrUsqty += item.rswtrUsqty //중수도사용량 + obj.altUswtrUtztnQnt += item.altUswtrUtztnQnt //대체수이용량 + obj.anlOperCst += item.anlOperCst //연간운영비용 } else { - obj = {}; - obj.ctpvCd = item.ctpvCd - obj.ctpvCnt = 1 - obj.archArea = item.archArea - obj.filtrTrtmfctCpct = item.filtrTrtmfctCpct - obj.wtrUsqty = item.wtrUsqty - obj.rswtAltSum = item.rswtrUsqty + item.altUswtrUtztnQnt - obj.rswtrUsqty = item.rswtrUsqty - obj.altUswtrUtztnQnt = item.altUswtrUtztnQnt - obj.anlOperCst = item.anlOperCst - + obj = { + ctpvCd: item.ctpvCd, + ctpvCnt: 1, + filtrTrtmfctCpct: item.filtrTrtmfctCpct, + wtrUsqty: item.wtrUsqty, + rswtAltSum: item.rswtrUsqty + item.altUswtrUtztnQnt, + rswtrUsqty: item.rswtrUsqty, + altUswtrUtztnQnt: item.altUswtrUtztnQnt, + anlOperCst: item.anlOperCst, + lndwtr: 0, + cleanToilUswtr: 0, + farmUswtr: 0, + indstUswtr: 0, + hydUswtr: 0, + ithlEtcUswtr: 0, + } cityArr.push(obj) } @@ -120,11 +129,47 @@ export const rswtStatActionsExport: (set: any) => rswtStatActions = ( } }); + const total: any = { + ctpvCd: "0", + ctpvCnt: 0, + filtrTrtmfctCpct: 0, + wtrUsqty: 0, + rswtAltSum: 0, + rswtrUsqty: 0, + altUswtrUtztnQnt: 0, + anlOperCst: 0, + lndwtr: 0, + cleanToilUswtr: 0, + farmUswtr: 0, + indstUswtr: 0, + hydUswtr: 0, + ithlEtcUswtr: 0, + }; + + cityArr.forEach(item => { + total.ctpvCnt += item.ctpvCnt; + total.archArea += item.archArea; + total.filtrTrtmfctCpct += item.filtrTrtmfctCpct; + total.wtrUsqty += item.wtrUsqty; + total.rswtAltSum += item.rswtAltSum; + total.rswtrUsqty += item.rswtrUsqty; + total.altUswtrUtztnQnt += item.altUswtrUtztnQnt; + total.anlOperCst += item.anlOperCst; + total.lndwtr += item.lndwtr; + total.cleanToilUswtr += item.cleanToilUswtr; + total.farmUswtr += item.farmUswtr; + total.indstUswtr += item.indstUswtr; + total.hydUswtr += item.hydUswtr; + total.ithlEtcUswtr += item.ithlEtcUswtr; + }); + + cityArr.unshift(total); + cityArr.forEach(item => { excelDownList.push({ ctpvCd: item.ctpvCd, ctpvCnt: item.ctpvCnt, - archArea: item.archArea, + // archArea: item.archArea, filtrTrtmfctCpct: item.filtrTrtmfctCpct, wtrUsqty: item.wtrUsqty, rswtAltSum: item.rswtAltSum, @@ -141,19 +186,18 @@ export const rswtStatActionsExport: (set: any) => rswtStatActions = ( }) //상세 - const rswtDetailList : RswtStatProps[] = []; - res.forEach((item:RswtStatProps) => { + const rswtDetailList: RswtStatProps[] = []; + res.forEach((item: RswtStatProps) => { const utztnUsgArray = new Array<string>(); item.guideStats.forEach((guideStat) => { utztnUsgArray.push(guideStat.utztnUsgCd); }) rswtDetailList.push({ ...item, - rswtAltSum : item.rswtrUsqty + item.altUswtrUtztnQnt, - utztnUsgCd : utztnUsgArray.join(', '), + rswtAltSum: item.rswtrUsqty + item.altUswtrUtztnQnt, + utztnUsgCd: utztnUsgArray.join(', '), }) }); - const excelDetailData = excelRswtStat(rswtDetailList); set((state: rswtStatState) => ({ rswtStat: { diff --git a/apps/wr/app/store/store.tsx b/apps/wr/app/store/store.tsx index 4557662f8..284f300c5 100644 --- a/apps/wr/app/store/store.tsx +++ b/apps/wr/app/store/store.tsx @@ -11,12 +11,19 @@ import { type comCodeState, type comCodeType, } from "./common/code"; +import { comFcltSearchAction, comFcltStateExport, type comFcltState, type comFcltType } from "./common/searchCode"; import { matDmdActionsExport, matDmdStateExport, type matDmdState, type matDmdType, } from "./matDmd"; +import { coRvwActionsExport, coRvwStateExport, type coRvwState, type coRvwType } from "./matDmd/coMatRvw"; +import { coSttnActionsExport, coSttnStateExport, type coSttnState, type coSttnType } from "./matDmd/coSttn"; // 옮겨야함 +import { drgtActionsExport, drgtStateExport, type drgtState, type drgtType } from "./matDmd/drgtSttn"; +import { ldrmntActionsExport, ldrmntStateExport, type ldrmntState, type ldrmntType } from "./matDmd/ldrmntCoSttn"; +import { ruseCaseActionsExport, ruseCaseStateExport, type ruseCaseState, type ruseCaseType } from "./matDmd/ruseCaseMng"; +import { ruseHstryActionsExport, ruseHstryStateExport, type ruseHstryState, type ruseHstryType } from "./matDmd/ruseRvwHstry"; import { rainChkgdnActionsExport, rainChkgdnStateExport, @@ -29,6 +36,15 @@ import { type rswtChkgdnState, type rswtChkgdnType, } from "./prfmncReg/chkgdnReg/rswrtChkgdn"; +import { pbnstPrfmncActionsExport, pbnstPrfmncStateExport, type pbnstPrfmncState, type pbnstPrfmncType } from "./prfmncReg/pbnstPrfmncReg"; +import { + rainChkSttnActionsExport, + rainChkSttnStateExport, + type rainChkSttnState, + type rainChkSttnType, +} from "./prfmncSttn/chkgdnSttn/rainChkgdnSttn"; +import { rswtChkStateExport, rswtChkSttnActionsExport, type rswtChkSttnState, type rswtChkSttnType } from "./prfmncSttn/chkgdnSttn/rswtChkgdnSttn"; +import { pbnstPrfmncSttnActionsExport, pbnstPrfmncSttnExport, type pbnstPrfmncSttnState, type pbnstPrfmncSttnType } from "./prfmncSttn/pbnstPrfmncSttn"; import { bdstActionsExport, bdstStateExport, @@ -59,6 +75,13 @@ import { type rswtFacState, type rswtFacType, } from "./ruseFcltMng/fcltReg/rswtReg"; +import { + htdflSttnActionsExport, + htdflSttnStateExport, + type htdflSttnState, + type htdflSttnType, +} from "./ruseFcltMng/fcltSttn/htdflSttn"; +import { pbnstSttnActionsExport, pbnstSttnStateExport, type pbnstSttnState, type pbnstSttnType } from "./ruseFcltMng/fcltSttn/pbnstSttn"; import { rainSttnActionsExport, rainSttnStateExport, @@ -71,32 +94,11 @@ import { type rswtSttnState, type rswtSttnType, } from "./ruseFcltMng/fcltSttn/rswtSttn"; -import { - htdflSttnActionsExport, - htdflSttnStateExport, - type htdflSttnState, - type htdflSttnType, -} from "./ruseFcltMng/fcltSttn/htdflSttn"; -import { - rainChkSttnActionsExport, - rainChkSttnStateExport, - type rainChkSttnState, - type rainChkSttnType, -} from "./prfmncSttn/chkgdnSttn/rainChkgdnSttn"; -import { rswtChkStateExport, rswtChkSttnActionsExport, type rswtChkSttnState, type rswtChkSttnType } from "./prfmncSttn/chkgdnSttn/rswtChkgdnSttn"; -import { pbnstSttnActionsExport, pbnstSttnStateExport, type pbnstSttnState, type pbnstSttnType } from "./ruseFcltMng/fcltSttn/pbnstSttn"; -import { coSttnActionsExport, coSttnStateExport, type coSttnState, type coSttnType } from "./matDmd/coSttn"; // 옮겨야함 -import { searchActions, type comFcltActions, type comFcltState } from "./common/searchCode"; -import { pbnstPrfmncActionsExport, pbnstPrfmncStateExport, type pbnstPrfmncState, type pbnstPrfmncType } from "./prfmncReg/pbnstPrfmncReg"; -import { coRvwActionsExport, coRvwStateExport, type coRvwState, type coRvwType } from "./matDmd/coMatRvw"; -import { ruseHstryActionsExport, ruseHstryStateExport, type ruseHstryState, type ruseHstryType } from "./matDmd/ruseRvwHstry"; -import { pbnstPrfmncSttnActionsExport, pbnstPrfmncSttnExport, type pbnstPrfmncSttnState, type pbnstPrfmncSttnType } from "./prfmncSttn/pbnstPrfmncSttn"; -import { drgtActionsExport, drgtStateExport, type drgtState, type drgtType } from "./matDmd/drgtSttn"; -import { ruseCaseActionsExport, ruseCaseStateExport, type ruseCaseType, type ruseCaseState } from "./matDmd/ruseCaseMng"; import { rainStatActionsExport, rainStatExport, type rainStatState, type rainStatType } from "./stats/rainStat"; -import { type rswtStatType, type rswtStatState, rswtStatExport, rswtStatActionsExport } from "./stats/rswtStat"; -import { matStatActionsExport, matStatExport, type matStatState, type matStatType } from "./stats/matStat"; -import { ldrmntActionsExport, ldrmntStateExport, type ldrmntState, type ldrmntType } from "./matDmd/ldrmntCoSttn"; +import { rswtStatActionsExport, rswtStatExport, type rswtStatState, type rswtStatType } from "./stats/rswtStat"; +//import { isLogin } from "./common/login"; +import { LoginApi } from "@/src/entities/auth"; +import { authActionsExport, authStateExport, type authState, type authType } from "./common/auth"; export type WrStore = comCodeType & pubFacType & @@ -113,7 +115,6 @@ export type WrStore = comCodeType & rainChkSttnType& rswtChkSttnType& pbnstSttnType& - comFcltActions&comFcltState& coSttnType& // 옮겨야함 ldrmntType& // 옮겨야함 pbnstPrfmncType& @@ -124,7 +125,9 @@ export type WrStore = comCodeType & ruseCaseType& rainStatType& rswtStatType& - matStatType + comFcltType & + authType + ; export const StoreZus = ( @@ -141,6 +144,7 @@ export const StoreZus = ( rswtSttnState & htdflSttnState & rainChkSttnState& + comFcltState& rswtChkSttnState & pbnstSttnState& coSttnState& // 옮겨야함 @@ -153,7 +157,7 @@ export const StoreZus = ( ruseCaseState& rainStatState& rswtStatState& - matStatState + authState = { ...comCodeStateExport, ...pubFacStateExport, @@ -180,7 +184,8 @@ export const StoreZus = ( ...ruseCaseStateExport, ...rainStatExport, ...rswtStatExport, - ...matStatExport + ...comFcltStateExport, + ...authStateExport } ) => { return createStore<WrStore>()((set) => { @@ -190,9 +195,10 @@ export const StoreZus = ( getMiddleRegionCd(set); getUnitWaterShedCd(set); getEnvPartCd(set); + LoginApi.login({userId: "CLEANWATER", password: "gktneh12!@"}) return { ...initState, - ...searchActions(set), + ...comFcltSearchAction(set), ...pubFacActionsExport(set), ...rainFacActionsExport(set), ...rswtFacActionsExport(set), @@ -218,7 +224,7 @@ export const StoreZus = ( ...ruseCaseActionsExport(set), ...rainStatActionsExport(set), ...rswtStatActionsExport(set), - ...matStatActionsExport(set) + ...authActionsExport(set) }; }); }; diff --git a/apps/wr/components/lib/api/index.ts b/apps/wr/components/lib/api/index.ts index 452788eb0..7823c2ddd 100644 --- a/apps/wr/components/lib/api/index.ts +++ b/apps/wr/components/lib/api/index.ts @@ -1,6 +1,6 @@ //8880 -import { CS_URL, DEV_URL_FO, DEV_URL_WR, LOCAL_URL } from "../config/"; +import { DEV_URL_FO, DEV_URL_WR, LOCAL_URL } from "../config/"; export const fetchType = { COMMON: "common", WR: "wr", LOCAL: "LOCAL" , CS: "CS" }; @@ -66,11 +66,14 @@ export const getFetchWithCount = async (type: string, url: string) => { if (!response.ok) { console.log(`오류 발생: ${response.status}`); } + const contentType = response.headers.get("content-type") ?? ""; if (contentType.includes("application/json")) { // JSON 응답 처리 const res = await response.json(); + + return res; } else { // 파일 응답 처리 (ex. PDF, Excel 등) @@ -105,8 +108,36 @@ export const postFectch = async (url: string, params: any, type?: string) => { return await response.json(); }; -export const putFectch = async (url: string, params: any) => { - const response = await fetch(DEV_URL_WR + url, { +export const cmsPostFectch = async (url: string, params: any,headers:any) => { + const response = fetch(url, { + method: "POST", + headers:headers , + body: JSON.stringify(params), + }).then((res) => + { + console.log(' res ' , res) + console.log(' access-token ' , res.headers.get('access-token')) + console.log(' refresh-token ' , res.headers.get('refresh-token')) + console.log(' token-id ' , res.headers.get('token-id')) + + }) + + + return await response; +}; + +export const putFectch = async (url: string, params: any,type?:string) => { + const div = + type === fetchType.COMMON + ? DEV_URL_FO + : type === fetchType.LOCAL + ? LOCAL_URL + : type === fetchType.CS + ? '' + : DEV_URL_WR; + + + const response = await fetch(div + url, { method: "PUT", headers: { "Content-Type": "application/json", @@ -129,7 +160,6 @@ export const deleteFectch = async (url: string, params?: any, type?: string) => ? '' : DEV_URL_WR; - console.log("deleteFectch url", div + url); const response = await fetch(div + url, { method: "DELETE", headers: { diff --git a/apps/wr/components/lib/config/index.ts b/apps/wr/components/lib/config/index.ts index f85f7bad9..ff44511b4 100644 --- a/apps/wr/components/lib/config/index.ts +++ b/apps/wr/components/lib/config/index.ts @@ -1,17 +1,24 @@ +import { + PROXY_HOST, + SERVER_API_CS_TUS_URL, + SERVER_API_CS_URL, + SERVER_API_FO_URL, + SERVER_API_WR_URL, +} from "@/src/shared/config"; -const LOCAL_PORT = "3001"; -const DEV_PORT = "11080"; -const DEV_PORT_NEW="11082" - - -export const LOCAL_URL_WR = `http://localhost:${LOCAL_PORT}/api/v1`; -export const DEV_URL_WR = `http://con.mind-one.co.kr:${DEV_PORT}/api/v1`; +//const DEV_PORT = "11080"; +//const DEV_PORT_NEW="11082" +//export const DEV_URL_WR = `http://con.mind-one.co.kr:${DEV_PORT}/api/v1`; // export const DEV_URL_WR = `http://192.168.0.91:8880/api/v1`; - /// export const DEV_URL_FO = `http://192.168.0.91:8882/api/v1`; -export const DEV_URL_FO = `http://con.mind-one.co.kr:${DEV_PORT_NEW}/api/v1`; +//export const DEV_URL_FO = `http://con.mind-one.co.kr:${DEV_PORT_NEW}/api/v1`; +//export const LOCAL_URL = `http://localhost:${LOCAL_PORT}`; +//export const CS_URL= 'http://dev.onasys.co.kr:8000' +//export const CS_TUS_UPLOAD= "/attach-service/api/v1/tus/upload"; - -export const LOCAL_URL = `http://localhost:${LOCAL_PORT}`; -export const CS_URL= 'http://dev.onasys.co.kr:8000' -export const CS_TUS_UPLOAD= "/attach-service/api/v1/tus/upload"; \ No newline at end of file +export const LOCAL_URL_WR = PROXY_HOST + `/api/v1`; +export const DEV_URL_WR = SERVER_API_WR_URL; +export const DEV_URL_FO = SERVER_API_FO_URL; +export const LOCAL_URL = PROXY_HOST; +export const CS_URL = SERVER_API_CS_URL; +export const CS_TUS_UPLOAD = SERVER_API_CS_TUS_URL; diff --git a/apps/wr/components/lib/func/TableData.tsx b/apps/wr/components/lib/func/TableData.tsx index dbb3aa7a6..192bc43c3 100644 --- a/apps/wr/components/lib/func/TableData.tsx +++ b/apps/wr/components/lib/func/TableData.tsx @@ -1,14 +1,16 @@ +import { numSize } from "@/app/store/common/code/basicCode"; import type { TableUpperProps } from "@/app/store/common/type/header"; import type { rainChkZodType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn"; import type { rswtChkZodType } from "@/app/store/prfmncReg/chkgdnReg/rswrtChkgdn"; import { Checkbox, Input, TableCell, TableRow } from "@common/components/ui"; +import { formatCommaNumber, handleCommaNumberChange } from "./commom"; export const rainChkTableData = (chkData: rainChkZodType[]) => { - // 1. chkgdnFk + utztnUsgCd 기준 누계 정보 만들기 + // 1. chkgdnSn + utztnUsgCd 기준 누계 정보 만들기 const maketotal = chkData.map((item: rainChkZodType) => { const selectTotal = item.guideDetails.reduce((acc: any, cur: any) => { const code = cur.utztnUsgCd; - const fk = cur.chkgdnFk; + const fk = cur.chkgdnSn; const key = `${fk}_${code}`; acc[key] = acc[key] || { @@ -21,7 +23,7 @@ export const rainChkTableData = (chkData: rainChkZodType[]) => { return acc; }, {}); - return selectTotal; // 모든 키: `${chkgdnFk}_${utztnUsgCd}` 형태 + return selectTotal; // 모든 키: `${chkgdnSn}_${utztnUsgCd}` 형태 }); // 2. 누계 정보들을 하나의 객체로 합치기 (평탄화) @@ -32,7 +34,7 @@ export const rainChkTableData = (chkData: rainChkZodType[]) => { const updatedRainyDetails: any[] = []; chkData.forEach((rd: rainChkZodType) => { rd.rainyDetails.forEach((rainyItem: any) => { - const key = `${rainyItem.chkgdnFk}_${rainyItem.utztnUsgCd}`; + const key = `${rainyItem.chkgdnSn}_${rainyItem.utztnUsgCd}`; const totals = flatTotalMap[key] || { totalSplyUswtrUtztnQnt: 0, totalRuseFcltUtztnQnt: 0, @@ -52,29 +54,34 @@ export const rainChkTableData = (chkData: rainChkZodType[]) => { }; export const rswtChkTableData = (chkData: rswtChkZodType[]) => { - // 1. chkgdnFk + utztnUsgCd 기준 누계 정보 만들기 + // 1. chkgdnSn + utztnUsgCd 기준 누계 정보 만들기 const makeRswtData: any[] = []; - chkData.forEach((root) => { - root.rswtrDetails.forEach((detail) => { - let flatRswtChk = {}; - const findGuide = root.guideDetails.find( - (guide) => guide.utztnUsgCd === detail.utztnUsgCd - ); - const findPoluttion = root.rswtrPollutions.find( - (polution) => polution.utztnUsgCd === detail.utztnUsgCd - ); - const operMngCd = root?.reuseBuilding.operMngCd; - flatRswtChk = { - ...root, - ...detail, - ...findGuide, - ...findPoluttion, - ...flatRswtChk, - operMngCd: operMngCd, - }; - makeRswtData.push(flatRswtChk); + + + if (chkData && chkData.length > 0) { + + chkData?.forEach((root) => { + root.rswtrDetails.forEach((detail) => { + let flatRswtChk = {}; + const findGuide = root.guideDetails.find( + (guide) => guide.utztnUsgCd === detail.utztnUsgCd + ); + const findPoluttion = root.rswtrPollutions.find( + (polution) => polution.utztnUsgCd === detail.utztnUsgCd + ); + const operMngCd = root?.reuseBuilding.operMngCd; + flatRswtChk = { + ...root, + ...detail, + ...findGuide, + ...findPoluttion, + ...flatRswtChk, + operMngCd: operMngCd, + }; + makeRswtData.push(flatRswtChk); + }); }); - }); + } return makeRswtData; }; @@ -88,14 +95,17 @@ export const chRowspanTable = ( ) => void, toggleSelection: (pk: number, checked: boolean) => void, selectedIds: number[], - handleCellClick: (index: number, headId: string) => void + handleCellClick: (index: number, headId: string) => void, + disable? : boolean ) => { let order = 0 // rowspan된 셀 렌더링 여부 추적용 const renderedMap = new Map<string, Set<number>>(); + return ( <> - {makeTableData.map((row, rowIndex) => ( + {/* {makeTableData.map((row, rowIndex) => ( */} + {makeTableData && makeTableData.length > 0 ? (makeTableData.map((row, rowIndex) => ( <TableRow key={rowIndex}> {headList .map((head) => { @@ -103,6 +113,7 @@ export const chRowspanTable = ( const key = head.id; const isMerged = head.mergeCell; const value = row[key]; + // 병합 대상이면 중복 렌더링 여부 판단 if (isMerged) { const renderedSet = renderedMap.get(key) ?? new Set<number>(); @@ -124,15 +135,21 @@ export const chRowspanTable = ( > {key === "checkBox" ? ( <Checkbox - checked={selectedIds.includes(row.chkgdnPk)} + checked={selectedIds.includes(row.chkgdnSn)} onCheckedChange={(checked) => - toggleSelection(row.chkgdnPk, checked === true) + toggleSelection(row.chkgdnSn, checked === true) } + disabled={disable} ></Checkbox> ) : key === "order" ? ( ++order ) : ( - value + // value + (["anlOperCst", "dtfcltCpct", "prcsCpct"].includes(head.id)) ? ( + formatCommaNumber(value) + ) : ( + <>{value}</> + ) )} </TableCell> ); @@ -146,19 +163,44 @@ export const chRowspanTable = ( onClick={() => handleCellClick(rowIndex, head.id)} > {head.edit === true ? ( - <Input - className={`${head.type === "number" ? "text-right" : "text-center"} !text-xl`} - type={head.type} - onChange={(e) => { - handleChange(rowIndex, head.id, e.target.value); - }} - value={value ?? ""} - disabled={!selectedIds.includes(row.chkgdnPk)} - /> + (["levyAmt", "anlOperCst", "dtfcltCpct", "prcsCpct"].includes(head.id)) ? ( + <Input + className={`${head.type === "number" ? "text-right" : "text-center"} !text-xl`} + type={head.type} + onChange={(e) => { + handleCommaNumberChange(e, val => + handleChange(rowIndex, head.id, val)) + }} + value={formatCommaNumber(value) || ""} + disabled={!selectedIds.includes(row.chkgdnSn)} + maxLength={numSize} + onInput={(e: React.ChangeEvent<HTMLInputElement>) => { + if (head.type === 'number' && e.target.value.length > numSize) { + e.target.value = e.target.value.slice(0, numSize); + } + }} + /> + ) : ( + <Input + className={`${head.type === "number" ? "text-right" : "text-center"} !text-xl`} + type={head.type} + onChange={(e) => { + handleChange(rowIndex, head.id, e.target.value); + }} + value={value || ""} + disabled={!selectedIds.includes(row.chkgdnSn)} + maxLength={numSize} + onInput={(e: React.ChangeEvent<HTMLInputElement>) => { + if (head.type === 'number' && e.target.value.length > numSize) { + e.target.value = e.target.value.slice(0, numSize); + } + }} + /> + ) ) : ( - (head.id === "totalSplyUswtrUtztnQnt" || head.id === "totalRuseFcltUtztnQnt" || head.id === "splyUswtrUtztnQnt" || head.id === "ruseFcltUtztnQnt") ? ( - <span className="underline text-blue-600 cursor-pointer"> - {value} + (["totalSplyUswtrUtztnQnt", "totalRuseFcltUtztnQnt", "splyUswtrUtztnQnt", "ruseFcltUtztnQnt", "levyAmt"].includes(head.id)) ? ( + <span className={["totalSplyUswtrUtztnQnt", "totalRuseFcltUtztnQnt", "splyUswtrUtztnQnt", "ruseFcltUtztnQnt"].includes(head.id) ? "underline text-blue-600 cursor-pointer" : ""}> + {formatCommaNumber(value)} </span> ) : ( <>{value}</> @@ -166,16 +208,19 @@ export const chRowspanTable = ( )} </TableCell> ); - })} </TableRow> - ))} + ))) : ( + <TableRow> + <TableCell colSpan={headList.length} className="h-24 text-center text-gray-500"> + 조회된 결과가 없습니다. + </TableCell> + </TableRow> + )} </> ); }; - - export const headVisibleChange = (headList: TableUpperProps[], isEdit: boolean) => { return headList.forEach((head) => { if (head.edit !== undefined) @@ -187,63 +232,147 @@ export const headVisibleChange = (headList: TableUpperProps[], isEdit: boolean) }); } - const handleChange = ( rowIndex: number, field: any, - value: string | number, - setInputData: (data:any) => void + value: string, + setInputData: (data: any) => void ) => { - if(Number(value) < 0 ||! Number(value)){ - value = 0; - }else{ - value = Number(value); + if (Number(value) < 0 || !Number(value)) { + value = "0"; } - setInputData((prevData:any) => - prevData?.map((row:any, index: number) => { - if (index === rowIndex) { - return { - ...row, - [field ]: value, - }; - } - return row; - }) - ); + setInputData((prevData: any) => + prevData?.map((row: any, index: number) => { + if (index === rowIndex) { + return { + ...row, + [field]: Number(value.replace(/,/g, "")), + }; + } + return row; + }) + ); }; ; - - -export const InputTableBody = (chgRes: any[] | [] ,headList:TableUpperProps[], setInputData: (data: any[]) => void) => { +export const InputTableBody = ( + chgRes: any[] | [], + headList: TableUpperProps[], + setInputData: (data: any[]) => void, + isEditable: boolean = true +) => { + // mergeCell 처리용 + const getRowSpan = (data: any[], key: string, rowIndex: number) => { + if (rowIndex > 0 && data[rowIndex][key] === data[rowIndex - 1][key]) { + return 0; // 이전과 같으면 셀을 그리지 않음 + } + let span = 1; + for (let i = rowIndex + 1; i < data.length; i++) { + if (data[i][key] === data[rowIndex][key]) span++; + else break; + } + return span; + }; return ( <> - {chgRes.map((data, dataIndex) => ( - <TableRow key={dataIndex + "row"}> - {headList.map((head, headIndex) => ( - <TableCell key={head.id + headIndex} className="w-36" > - {head.edit ===true && data.edit !==false? - <Input - className={`${head.id !== "utztnUsgCd" ? "text-right" : "text-center"} !text-xl h-10 w-full`} - onChange={(e) => - handleChange(dataIndex, head.id, e.target.value,setInputData) - } - value={data[head.id as keyof any] ?? ""} - /> : ( - <div - className={` - ${head.id !== "utztnUsgCd" ? "text-right" : "text-center"} - !text-xl w-full h-10 - `} - > - {data[head.id as keyof any] ?? ""} - </div> - ) + {chgRes.map((data, dataIndex) => { + // 소계/총합계 행 판별 + const isSubTotal = data["용수명"] === "소계"; + const isTotal = data["구분"] === 0; + + const bgClass = isTotal + ? "bg-[#d5e1eb] font-bold" + : isSubTotal + ? "bg-[#e6eef5] font-semibold" + : ""; + + return ( + <TableRow key={dataIndex + "row"}> + {headList.map((head, headIndex) => { + const isTextColumn = head.id === '구분' || head.id === '용수명'; + const textAlignClass = isTextColumn ? 'text-center' : 'text-right'; + + // 총합계 행이면 "구분"에서 colSpan=2로 병합 + if ( + (data["구분"] === 0 || data["용수명"] === 0) && + head.id === "구분" + ) { + return ( + <TableCell + key={head.id + headIndex} + colSpan={2} + className="font-bold text-center bg-[#d5e1eb]" + > + 총합계 + </TableCell> + ); } - </TableCell> - ))} + // 총합계(합계) 행이면 "용수명" 컬럼은 렌더링하지 않음 + if ( + (data["구분"] === 0 || data["용수명"] === 0) && + head.id === "용수명" + ) { + return null; + } + + // 총합계 행의 나머지 셀에도 색상 적용 + if (data["구분"] === 0 || data["용수명"] === 0) { + return ( + <TableCell key={head.id + headIndex} className="bg-[#d5e1eb] font-bold text-right"> + {Number(data[head.id]) ? formatCommaNumber(data[head.id as keyof any]) : data[head.id as keyof any] ?? ""} + </TableCell> + ); + } + // mergeCell 처리 + if (head.mergeCell) { + const rowSpan = getRowSpan(chgRes, head.id, dataIndex); + if (rowSpan === 0) return null; + return ( + <TableCell key={head.id + headIndex} rowSpan={rowSpan} className={`w-36 align-middle text-center whitespace-nowrap ${bgClass}`}> + {Number(data[head.id]) ? formatCommaNumber(data[head.id as keyof any]) : data[head.id as keyof any] ?? ""} + </TableCell> + ); + } + // 기본 셀 + // return head.edit === true && data.edit !== false ? ( + return isEditable && head.edit === true && data.edit !== false ? ( + <TableCell key={head.id + headIndex} className={`w-36 ${bgClass}`}> + <Input + // className={`${head.id !== "utztnUsgCd" ? "text-right" : "text-center"} !text-xl h-10 w-full`} + className={`${textAlignClass} !text-xl h-10 w-full`} + onChange={(e) => + handleCommaNumberChange(e, val => + handleChange(dataIndex, head.id, String(val), setInputData) + ) + } + value={formatCommaNumber(data[head.id as keyof any]) ?? ""} + maxLength={numSize} + /> + </TableCell> + ) : ( + <TableCell key={head.id + headIndex} className={`w-36 ${bgClass}`}> + <div + // className={` + // ${head.id !== "utztnUsgCd" ? "text-right" : "text-center"} + // !text-xl w-full h-10 + // `} + className={`${textAlignClass} !text-xl w-full h-10`} + > + {Number(data[head.id]) ? formatCommaNumber(data[head.id as keyof any]) : data[head.id as keyof any] ?? ""} + </div> + </TableCell> + ); + })} + </TableRow> + ); + })} + {chgRes.length === 0 && ( + <TableRow> + <TableCell colSpan={headList.length} className="h-24 text-center text-gray-500"> + 조회된 결과가 없습니다. + </TableCell> </TableRow> - ))} + )} </> ); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/apps/wr/components/lib/func/commom.tsx b/apps/wr/components/lib/func/commom.tsx index e5dfc8c0c..4902ccf3d 100644 --- a/apps/wr/components/lib/func/commom.tsx +++ b/apps/wr/components/lib/func/commom.tsx @@ -1,20 +1,30 @@ import type { comCodeState } from "@/app/store/common/code"; import { filePickedBlob, - type SelectDataType, - type ReturnType, filePickedBlobArray, + type ReturnType, + type SelectDataType, type SheetParameter, } from "@common/business_components/ui"; +import { Button } from "@common/components/ui"; import type { RefObject } from "react"; +import toast from "react-hot-toast"; export const getCity = (val: string, sggCd: SelectDataType[]) => { - const data = sggCd - .filter((item) => String(item.val).substring(0, 2) === val) - .map((item) => ({ + let data: any; + if (val !== "0") { + data = sggCd + .filter((item) => String(item.val).substring(0, 2) === val) + .map((item) => ({ + val: item.val, + text: item.text, // 한국어 텍스트 + })); + } else { + data = sggCd.map((item) => ({ val: item.val, text: item.text, // 한국어 텍스트 })); + } data.unshift({ val: "0", text: "전체" }); return data; }; @@ -82,7 +92,7 @@ export const excelDownload = ( export const excelArrayDownload = ( fileNm: string, sampleFile: string, - sheetArray:SheetParameter [] + sheetArray: SheetParameter[] ) => { if (!sheetArray) return; fetch(`/${sampleFile}`, {}).then(async (response) => { @@ -106,6 +116,27 @@ export const handleCommaNumberChange = ( onChange(raw === "" ? "" : Number(raw)); }; +export const handleOnlyNumberChange = ( + e: React.ChangeEvent<HTMLInputElement>, + onChange: (value: number | "") => void +) => { + // 콤마 제거 후 숫자와 소수점만 남기기 + const cleaned = e.target.value.replace(/,/g, "").replace(/[^0-9.]/g, ""); + // 숫자 및 소수점 외 입력 차단 + if (!/^\d*\.?\d*$/.test(cleaned)) return; + onChange(cleaned === "" ? "" : Number(cleaned)); +}; + +// onchange 텍스트를 숫자로 and ,제거 +export const handleMinusNumberChange = ( + e: React.ChangeEvent<HTMLInputElement>, + onChange: (value: number | "") => void +) => { + const raw = e.target.value.replace(/,/g, ""); + if (!/^\d*$/.test(raw)) return; + onChange(raw === "" ? -1 : Number(raw)); +}; + // 숫자입력시 , 삽입 export const formatCommaNumber = ( value: number | string | undefined | null @@ -144,18 +175,81 @@ export const yearOptions: { val: string; text: string }[] = Array.from( }) ); -export const getdmdMatFlag = (flag: string|undefined) => { - let flagMat = ""; +export const getdmdMatFlag = (flag: string | undefined | null) => { + const cleanFlag = (flag ?? "").trim(); - if(flag === undefined || flag === null) { - flagMat = "신청"; - }else if(flag.trim() === "Sub") { - flagMat = "검토"; - - }else if(flag.trim() === "matched") { - flagMat = "매칭완료"; - }else { - flagMat = "신청"; + switch (cleanFlag) { + case "Sub": + return "검토"; + case "Matched": + return "매칭완료"; + case "Checked": + return "검토완료"; + default: + return "신청"; } - return flagMat; +}; + +export const formatPhoneNumber = (tel: string) => { + const onlyNums = tel.replace(/[^0-9]/g, ""); + if (onlyNums.length === 10) { + return onlyNums.replace(/(\d{2})(\d{4})(\d{4})/, "$1-$2-$3"); + } else if (onlyNums.length === 11) { + return onlyNums.replace(/(\d{3})(\d{4})(\d{4})/, "$1-$2-$3"); + } else { + return tel; // 유효하지 않으면 빈 문자열 + } +}; + +export const confirmToast = (content: string, saveFunc: () => void) => { + toast( + (t) => ( + <span className="flex flex-col items-center"> + <div className="mb-6">{content}</div> + <div className="flex flex-row space-x-2"> + <Button + size={"sm"} + onClick={() => { + saveFunc(); + toast.dismiss(t.id); + }} + > + 확인 + </Button> + <Button size={"sm"} onClick={() => toast.dismiss(t.id)}> + 취소 + </Button> + </div> + </span> + ), + { + id: "global-confirm-toast", // 중복 토스트 방지 + duration: Infinity, // 자동 사라짐 방지 + } + ); +}; + +export const showConfirmToast = (message: string) => { + toast.custom( + (t) => ( + <div + className={`w-[500px] min-h-[140px] bg-white shadow-lg rounded-xl p-6 border border-gray-200 + ${t.visible ? "animate-enter" : "animate-leave"}`} + > + <div className="text-3xl font-extrabold text-red-600 bg-yellow-100 px-4 py-2 rounded-md shadow-md"> + {message} + </div>{" "} + <div className="flex justify-end gap-2"> + <Button + size={"sm"} + className="px-4 py-1.5 rounded bg-gray-200 text-sm hover:bg-gray-300" + onClick={() => toast.dismiss(t.id)} + > + 확인완료 + </Button> + </div> + </div> + ), + { duration: Infinity } + ); // 수동 닫기 }; diff --git a/apps/wr/components/lib/func/excelFunc.tsx b/apps/wr/components/lib/func/excelFunc.tsx new file mode 100644 index 000000000..2383bdbb3 --- /dev/null +++ b/apps/wr/components/lib/func/excelFunc.tsx @@ -0,0 +1,17 @@ +// ✨ 오류 검사 - 가공된 데이터 기준 +export const isInvalidNumber = (val: any) => { + return val !== undefined && val !== null && val !== "" && isNaN(Number(val)); + }; + + export const isInvalidDate = (val: any) => { + //yyyymmdd 즉 20201212 이런형식만 허용 + return !(typeof val === "string" && /^\d{8}$/.test(val.trim())); + }; + + export const parseRawValue = (val: any): number | string => { + if (val === "" || val === undefined || val === null) return 0; + if (typeof val === "string" && ["보류", "N/A"].includes(val.trim())) + return val; + return !isNaN(val) && val !== " " ? Number(val) : val; + }; + \ No newline at end of file diff --git a/apps/wr/components/lib/func/prfData.tsx b/apps/wr/components/lib/func/prfData.tsx new file mode 100644 index 000000000..08cc93444 --- /dev/null +++ b/apps/wr/components/lib/func/prfData.tsx @@ -0,0 +1,88 @@ +import { + initValInUsage, + initValOutUsage, + subTotalObj, + totalObj, + type PrfType, + type PrfUsageSummary, +} from "@/app/store/prfmncReg/pbnstPrfmncReg/type"; + +export const getTablePrfData = (prfList: PrfType[]) => { + const inUsageData = structuredClone(initValInUsage); + const outUsageData = structuredClone(initValOutUsage); + const inSubtotal = structuredClone(subTotalObj); + const outSubtotal = structuredClone(subTotalObj); + + if (prfList) { + prfList?.forEach((prfItem) => { + const month = Number(prfItem?.ym?.slice(4)); // "MM" + const monthKey = `${month}월`; + + inUsageData.forEach((inData: PrfUsageSummary) => { + const usageKey = inData.usage as keyof PrfType; + const value = (prfItem[usageKey] as number) ?? 0; + + (inData[monthKey as keyof PrfUsageSummary] as number) = value; + inData.total += value; + + if (inSubtotal&& usageKey !== "subtotal" as keyof PrfType) { + (inSubtotal[monthKey as keyof PrfUsageSummary] as number) = + ((inSubtotal[monthKey as keyof PrfUsageSummary] as number) ?? 0) + + value; + inSubtotal.total += value; + } + }); + + outUsageData.forEach((outData: PrfUsageSummary) => { + const usageKey = outData.usage as keyof PrfType; + const value = (prfItem[usageKey] as number) ?? 0; // Type assertion for prfItem[usageKey] + + (outData[monthKey as keyof PrfUsageSummary] as number) = value; + outData.total += value; + + if (outSubtotal && usageKey !== "subtotal" as keyof PrfType) { + (outSubtotal [monthKey as keyof PrfUsageSummary] as number) = + ((outSubtotal[monthKey as keyof PrfUsageSummary] as number) ?? 0) + value; + outSubtotal.total += value; + } // End of if (outSubtotal && usageKey !== "subtotal") + }); // End of outUsageData.forEach + }); + (inSubtotal as any)["구분"] = "장내용수"; + (outSubtotal as any)["구분"] = "장내용수"; + + + inUsageData.push(inSubtotal); + outUsageData.push(outSubtotal); + const usageTotalObj: { [key: string]: any } = structuredClone(totalObj); + for (const key in usageTotalObj) { + if (["usage", "gubun", "edit"].includes(key)) continue; + const val = + (Number((inSubtotal as any)[key]) || 0) + + (Number((outSubtotal as any)[key]) || 0); + + (usageTotalObj as any)[key] = val; + } + const tableData = [...inUsageData, ...outUsageData, usageTotalObj]; + + return { + inPrf: inUsageData, + outPrf: outUsageData, + totalPrf: tableData, + }; + } +}; + + + +// const usageKey = inData.usage as keyof PrfType; +// const value = (prfItem[usageKey] as number) ?? 0; + +// (inData[monthKey as keyof PrfUsageSummary] as number) = value; +// inData.total += value; + +// if (inSubtotal&& usageKey !== "subtotal" as keyof PrfType) { +// (inSubtotal[monthKey as keyof PrfUsageSummary] as number) = +// ((inSubtotal[monthKey as keyof PrfUsageSummary] as number) ?? 0) + +// value; +// inSubtotal.total += value; +// } \ No newline at end of file diff --git a/apps/wr/components/lib/useTus_wr/useTusWr.tsx b/apps/wr/components/lib/useTus_wr/useTusWr.tsx index b9441e6a4..c7580f84f 100644 --- a/apps/wr/components/lib/useTus_wr/useTusWr.tsx +++ b/apps/wr/components/lib/useTus_wr/useTusWr.tsx @@ -1,12 +1,14 @@ +import type { FilesType } from "@/components/ui/form/fcltReg/pbnstReg/mngReg/mngRegForm"; +import toast from "react-hot-toast"; import { Upload, + type DetailedError, type HttpRequest, type HttpResponse, type OnSuccessPayload, - type DetailedError, } from "tus-js-client"; -import { CS_TUS_UPLOAD, CS_URL } from "../config"; -import type { FilesType } from "@/components/ui/form/fcltReg/pbnstReg/mngReg/mngRegForm"; +import { fetchType, putFectch } from "../api"; +import { CS_TUS_UPLOAD } from "../config"; type UploadOptions = { handleUploadStart?: (upload: Upload) => void; @@ -23,16 +25,14 @@ export type ReturnFileType = { lnkgDmnId: string | null; lnkgDmnNm: string | null; regDt: Date | null; - atflCd: String; + atflCd: string; atflSn: number; - atflId: String; - physFileNm: String; + atflId: string; + physFileNm: string; }; export const TusClientUpload = (oFile: FilesType, options?: UploadOptions) => new Promise<ReturnFileType>((resolve, reject) => { try { - const Authorization = "Authorization"; - const AUTH_USER_ID = "token-id"; let fileInfo: string | undefined; //CS_URL const uploader = new Upload(oFile.file, { @@ -60,8 +60,30 @@ export const TusClientUpload = (oFile: FilesType, options?: UploadOptions) => onSuccess(payload: OnSuccessPayload) { options?.handleSuccess?.(payload); if (fileInfo) { - console.log('fileInfo', fileInfo); - resolve(JSON.parse(fileInfo) as ReturnFileType); + + + const WrTableInfo = { + lnkgDmnNm: "tbWrRuseFcltFile", + lnkgDmnId: "fileTkn", + }; + + console.log('fileInfo ', fileInfo) + const fileParse = JSON.parse(fileInfo) as ReturnFileType; + putFectch( + `/attach-service/api/v1/atfl/attachments/${fileParse.atflCd}/info`, + WrTableInfo, + fetchType.CS + ) + .then(() => { + resolve(fileParse); + }) + .catch(() => { + reject({ + causingError: { + cause: "파일 업로드 업데이트 실패", + }, + } as DetailedError); + }); } else reject({} as DetailedError); }, }); @@ -71,6 +93,8 @@ export const TusClientUpload = (oFile: FilesType, options?: UploadOptions) => } catch (e) { console.log(e); reject(e as DetailedError); + + toast.error(String(e)); } // setUpload(uploader); }); diff --git a/apps/wr/components/ui/form/coMat/conCostForm/conCostForm.tsx b/apps/wr/components/ui/form/coMat/conCostForm/conCostForm.tsx index b22f9474e..1d175f1c6 100644 --- a/apps/wr/components/ui/form/coMat/conCostForm/conCostForm.tsx +++ b/apps/wr/components/ui/form/coMat/conCostForm/conCostForm.tsx @@ -1,3 +1,4 @@ +"use client"; import { useWrStore } from "@/app/store"; import { usageProcData } from "@/app/store/matDmd/coMatRvw/headList"; import type { @@ -18,12 +19,10 @@ import { Form, FormField, FormUi, - UiModal, UiTable, } from "@common/business_components/ui"; import PopoverUi from "@common/business_components/ui/popover_ui"; -import { Button, Input, Label } from "@common/components/ui"; -import type { read } from "fs"; +import { Button, Checkbox, Input, Label } from "@common/components/ui"; import { useCallback, useEffect, useRef, useState } from "react"; import { type UseFormReturn } from "react-hook-form"; import toast from "react-hot-toast"; @@ -31,9 +30,13 @@ import toast from "react-hot-toast"; export default function ConCostForm({ form, type, + selectionVersion, + readonly = false, }: { form: UseFormReturn<DmdCoReqType>; type: string; + selectionVersion: number; + readonly: boolean; }) { const { constCostRes, @@ -46,6 +49,8 @@ export default function ConCostForm({ const ref = useRef<boolean>(false); + const [checkFinish, setCheckFinish] = useState<boolean>(false); + useEffect(() => { if (!ref.current) { const currentYear = new Date().getFullYear(); @@ -55,6 +60,11 @@ export default function ConCostForm({ } }, [ref, getConstCostFee, getRateFee]); + useEffect(() => { + // 다른 행을 선택하면(selectionVersion 변경) 체크박스 상태를 초기화합니다. + setCheckFinish(false); + }, [selectionVersion]); + const zipDowonLoad = () => { fetch(`/2024_constarctFeeProgram.zip`, {}).then(async (response) => { const blobRet = await response.blob(); @@ -73,23 +83,36 @@ export default function ConCostForm({ const calculateConstCost = useCallback(() => { const usgPrcsProcs = usgPrcsProcsWatch; - const dmndQtyYear = dmndQtyWatch * 365; + if (!dmndQtyWatch) { + toast.error("요구수량(m³/일)을 확인해주세요 계산할수 없습니다. "); + return; + } + if (usgPrcsProcs === null || usgPrcsProcs === undefined) { + toast.error( + "용도에 따른 처리공정을 확인할수 없습니다. 용도를 확인해주세요. " + ); + return; + } + + const dmndQtyYear = dmndQtyWatch; if (usgPrcsProcs && usgPrcsProcs?.length > 0) { - const firstProc = getConstFctCode(usgPrcsProcs?.split("+")[0] || "") || ""; - const secondProc = getConstFctCode(usgPrcsProcs?.split("+")[1] || "") || ""; + const firstProc = + getConstFctCode(usgPrcsProcs?.split("+")[0] || "") || ""; + const secondProc = + getConstFctCode(usgPrcsProcs?.split("+")[1] || "") || ""; const { lower: firstLower, upper: firstUpper } = findBounds( constCostRes, firstProc || "", - 'fctCd', - 'fctCpc', + "fctCd", + "fctCpc", Number(dmndQtyYear) ); const { lower: secondLower, upper: secondUpper } = findBounds( constCostRes, secondProc || "", - 'fctCd', - 'fctCpc', + "fctCd", + "fctCpc", Number(dmndQtyYear) ); @@ -108,48 +131,97 @@ export default function ConCostForm({ const groupCodes = ["BD", "CE", "SE", "WD"]; const groupedBounds: { [key: string]: { lower?: any; upper?: any } } = {}; for (const code of groupCodes) { - - groupedBounds[code] = findBounds(rateFeeRes, code, 'incExpCd', 'stdCost',result); + groupedBounds[code] = findBounds( + rateFeeRes, + code, + "incExpCd", + "stdCost", + result + ); } const resultRate = transformAndInterpolate(groupedBounds, result); - const fcltSbsdCstrnCst = (result* (resultRate.BD + resultRate.CE + resultRate.SE + resultRate.WD) / 100); + const fcltSbsdCstrnCst = + (result * + (resultRate.BD + resultRate.CE + resultRate.SE + resultRate.WD)) / + 100; const realResult = result + fcltSbsdCstrnCst; - form.setValue("demandCst.prcsFcltCpctCstrnCst", Math.round(result) ); - form.setValue("demandCst.fcltSbsdCstrnCst", Math.round(fcltSbsdCstrnCst)) ; - form.setValue("totalConstCost", Math.round(realResult)); + form.setValue("demandCst.prcsFcltCpctCstrnCst", Math.round(result)); + form.setValue("demandCst.fcltSbsdCstrnCst", Math.round(fcltSbsdCstrnCst)); + form.setValue("totalConstCost", Math.round(realResult)); } }, [constCostRes, rateFeeRes, usgPrcsProcsWatch, dmndQtyWatch]); const onSubmit = (value: DmdCoReqType) => { - - if( !form.getValues('demandCst.prcsFcltCpctCstrnCst') ){ - toast.error('공사비 계산 진행 후 저장을 눌러주새요') - return + if (value.demandCst && !value.demandCst.prcsFcltCpctCstrnCst) { + toast.error("공사비 계산 진행 후 저장을 눌러주새요"); + return; } + + const splycd = form.getValues("splyCndtList") + const pk = splycd&&splycd[0]?.ruseUtztnFcltCd?splycd[0]?.ruseUtztnFcltCd:'11000SW001R1' + if (type === "mat") { - const data: DedMatUpdateType = { - dmdInfoSn: form.getValues("dmdInfoSn"), - usgPrcsProcs: form.getValues("demandCst.usgPrcsProcs"), - cstrnCst: form.getValues("demandCst.cstrnCst"), - fcltSbsdCstrnCst: form.getValues("demandCst.fcltSbsdCstrnCst"), - prcsFcltCpctCstrnCst: form.getValues("demandCst.prcsFcltCpctCstrnCst"), - rgtr: TEST_UUID, - }; - coMatRvwUpdate("11000SW001R1", form.getValues("dmdInfoFk"), data); + if (value.demandCst) { + const data: DedMatUpdateType = { + dmdInfoSn: value.dmdInfoSn, + usgPrcsProcs: value.demandCst.usgPrcsProcs || "", + cstrnCst: value.demandCst.cstrnCst || 0, + fcltSbsdCstrnCst: value.demandCst.fcltSbsdCstrnCst || 0, + prcsFcltCpctCstrnCst: value.demandCst.prcsFcltCpctCstrnCst || 0, + flg: checkFinish ? "Checked" : "Sub", + rgtr: TEST_UUID, + }; + if (checkFinish) { + toast((t) => ( + <span className="flex flex-col items-center text-2xl"> + 검토완료 변경시 공사비는 변경할수 없습니다. + <br /> + 진행하시겠습니까? + <div className="flex gap-2 mt-2"> + <Button + size="sm" + onClick={() => { + toast.dismiss(t.id); + + coMatRvwUpdate( + pk, + form.getValues("dmdInfoId"), + data + ); + }} + > + 확인 + </Button> + <Button size="sm" onClick={() => toast.dismiss(t.id)}> + 취소 + </Button> + </div> + </span> + )); + } else { + + coMatRvwUpdate(pk, form.getValues("dmdInfoId"), data); + } + } } else if (type === "hist") { - - delete value.totalConstCost - delete value.ruseUtztnFcltNm - + delete value.totalConstCost; + delete value.ruseUtztnFcltNm; + console.log("onSubmit data ", pk); ruseRvwCalUpdate(value); } }; useEffect(() => { - console.log(" form ", form.formState.errors); + console.log(" form ", form.formState.errors); }, [form.formState]); + useEffect(() => { + form.register("dmdInfoSn"); + form.register("dmdInfoId"); + form.register("ruseUtztnFcltNm"); + }, []); + return ( <> <hr className="mt-8" /> @@ -163,18 +235,36 @@ export default function ConCostForm({ <Button className="mx-3" size={"sm"} onClick={calculateConstCost}> 계산 </Button> - <Button className="mr-3" size={"sm"} onClick={form.handleSubmit(onSubmit)}> + <Button + className="mr-3" + size={"sm"} + onClick={form.handleSubmit(onSubmit)} + disabled={readonly} + > 저장 </Button> - <Label>관로 표준공사비 산정 프로그램</Label> - <Button - color="green" - size={"sm"} - onClick={zipDowonLoad} - className="bg-none ml-6" - > - 다운로드 - </Button> + <Label>검토완료</Label> + <Checkbox + className="ml-3 mr-6" + checked={checkFinish} + onCheckedChange={ + (checked) => { + if (readonly) return; + setCheckFinish(checked === true); + } + + // 'indeterminate' 상태를 무시하고 순수 boolean 값으로만 상태를 업데이트합니다. + } + ></Checkbox> + <Label>관로 표준공사비 산정 프로그램</Label> + <Button + color="green" + size={"sm"} + onClick={zipDowonLoad} + className="bg-none ml-6" + > + 다운로드 + </Button> </div> <div className="col-span-12 w-full"> <h1 className={formH1Class}>기본사항</h1> @@ -193,6 +283,7 @@ export default function ConCostForm({ field.onChange(e.target.value); }} value={field.value ?? ""} + readOnly={true} /> </FormUi> )} @@ -203,7 +294,7 @@ export default function ConCostForm({ control={form.control} name="dmndQty" render={({ field }) => ( - <FormUi label="요구수량(M3/d)"> + <FormUi label="요구수량(m³/일)"> <Input placeholder="" type="number" @@ -212,6 +303,7 @@ export default function ConCostForm({ field.onChange(Number(e.target.value)); }} value={field.value ?? 0} + readOnly={true} /> </FormUi> )} @@ -239,7 +331,7 @@ export default function ConCostForm({ ]} pageSize={0} total={0} - cellClick={() => {}} + cellClick={undefined} onPageChange={() => {}} /> </> @@ -255,8 +347,9 @@ export default function ConCostForm({ placeholder="" type="text" onChange={field.onChange} - value={form.getValues("demandCst.usgPrcsProcs") ?? ""} + value={field.value ?? ""} className="mt-2" + readOnly={true} /> </FormUi> )} @@ -267,15 +360,17 @@ export default function ConCostForm({ control={form.control} name="dmndQty" render={({ field }) => ( - <FormUi label="처리시설 용량(요구수량 * 365일)"> + <FormUi label="처리시설용량(m³/일)"> <Input placeholder="" type="number" step="0.001" onChange={(e) => { + console.log("dmndQty ", e); //field.onChange(Number(e.target.value) * 365 ); }} - value={Number(field.value) * 365 || 0} + // value={Number(field.value) * 365 || 0} + value={Number(field.value) || 0} readOnly /> </FormUi> @@ -283,23 +378,23 @@ export default function ConCostForm({ /> </div> <div className="col-span-12 w-full"> - <h1 className={formH1Class}>공사비 입력사항</h1> + <h1 className={formH1Class}>공사비 입력사항</h1> </div> - <div className="col-span-3 mt-4"> + + <div className="col-span-2 mt-4"> <div className="flex items-center justify-between"> <PopoverUi childrenTriger={ <> <Label className="underline"> - 처리시설용량에 따른 공사비(백만원) <i className="diveicon di-search" /> + 처리시설용량에 따른 공사비(백만원){" "} + <i className="diveicon di-search" /> </Label> </> } childrenBody={ <> - <ConstCostFeeTable - setReturnData={(e) => {}} - ></ConstCostFeeTable> + <ConstCostFeeTable /> </> } bodyClassName={"w-[40vw] h-full"} @@ -324,7 +419,7 @@ export default function ConCostForm({ )} /> </div> - <div className="col-span-3 mt-4"> + <div className="col-span-2 mt-4"> <div className="flex items-center justify-between"> <PopoverUi childrenTriger={ @@ -336,9 +431,7 @@ export default function ConCostForm({ } childrenBody={ <> - <RateFeeTableModal - setReturnData={(e) => {}} - ></RateFeeTableModal> + <RateFeeTableModal /> </> } bodyClassName={"w-[22vw]"} @@ -363,12 +456,13 @@ export default function ConCostForm({ )} /> </div> - <div className="col-span-3 mt-4"> + <div className="col-span-2 mt-4"> <PopoverUi childrenTriger={ <> <Label className="underline"> - 총사업비 (백만원) <i className="diveicon di-search" /> + 처리시설 총사업비(백만원){" "} + <i className="diveicon di-search" /> </Label> </> } @@ -397,7 +491,9 @@ export default function ConCostForm({ className="mt-2" type="number" step="0.01" - onChange={(e) => {}} + onChange={(e) => { + console.log("totalConstCost ", e); + }} value={field.value ?? 0} readOnly /> @@ -405,7 +501,7 @@ export default function ConCostForm({ )} /> </div> - <div className="col-span-3 mt-4"> + {/* <div className="col-span-3 mt-4"> <div className="flex items-center justify-between"> <Label>공급처 선정</Label> <Button size={"sm"} onClick={(e) => {}} className="bg-none"> @@ -428,8 +524,8 @@ export default function ConCostForm({ </FormUi> )} /> - </div> - <div className="col-span-4 mt-4"> + </div> */} + <div className="col-span-3 mt-4"> <FormField control={form.control} name="demandCst.cstrnCst" @@ -440,6 +536,9 @@ export default function ConCostForm({ type="number" step="0.01" onChange={(e) => { + + if(readonly) return; + field.onChange(Number(e.target.value)); }} value={field.value ?? 0} @@ -448,7 +547,7 @@ export default function ConCostForm({ )} /> </div> - <div className="col-span-4 mt-4 mb-3"> + <div className="col-span-3 mt-4 mb-3"> <Label className="text-xl"> 처리시설 + 공급관로 총사업비(백만원) </Label> diff --git a/apps/wr/components/ui/form/fcltReg/bdstReg/bdstAdd.tsx b/apps/wr/components/ui/form/fcltReg/bdstReg/bdstAdd.tsx new file mode 100644 index 000000000..9647c2731 --- /dev/null +++ b/apps/wr/components/ui/form/fcltReg/bdstReg/bdstAdd.tsx @@ -0,0 +1,148 @@ +"use client"; + +import { useWrStore } from "@/app/store"; +import { numSize } from "@/app/store/common/code/basicCode"; +import { selectClass } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; +import { formH1Class } from "@/components/lib/class/comClass"; +import { safeDate } from "@/components/lib/func/commom"; +import { DatePicker, FormField, FormUi, SelectBox } from "@common/business_components"; +import { Button, Checkbox, Input } from "@common/components"; +import { useState } from "react"; +import { useFieldArray } from "react-hook-form"; + +export default function BdstAdd({ form, readonly }: { form: any; readonly?: boolean; }) { + + const { code } = useWrStore((state) => { + return state; + }); + + const { fields, append, remove } = useFieldArray({ + name: "chgBuilding", + control: form?.control, // Provide a fallback to avoid errors + }); + + const plusHist = () => { + append({ + bdstNm: "", + daddr: "", + bdstUsgCd: "", + }); + }; + const [selectedIds, setSelectedIds] = useState<string[]>([]); + + const toggleSelect = (id: string) => { + setSelectedIds((prev) => + prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id] + ); + }; + + const removeSelected = () => { + const idxsToRemove = fields + .map((item, idx) => (selectedIds.includes(item.id) ? idx : -1)) + .filter((index) => index > -1) + .sort((prevIndex, nextIndex) => nextIndex - prevIndex); + + remove(idxsToRemove); + setSelectedIds([]); + }; + + return ( + <> + <h1 className={formH1Class}>중축, 개축, 재축</h1> + {!readonly && ( + <div className="flex justify-end gap-2 mt-4"> + + <Button type="button" className="" size={"sm"} onClick={plusHist}> + 이력추가 + </Button> + <Button + type="button" + className="!bg-red-500 text-white" + size={"sm"} + onClick={removeSelected} + > + 삭제 + </Button> + + </div> + )} + {fields.map((field, index) => ( + <div + key={field.id} + // className="grid grid-cols-[auto,1fr,1fr,1fr] gap-2 mt-4 items-center" + className={`grid ${ + readonly ? "grid-cols-3" : "grid-cols-[auto,1fr,1fr,1fr]" + } gap-2 mt-4 items-center`} + > + {!readonly && ( + <div className="col-span-1 flex justify-center mr-4"> + <Checkbox + checked={selectedIds.includes(field.id)} + onCheckedChange={() => toggleSelect(field.id)} + /> + </div> + )} + <div className=""> + <FormField + control={form.control} + name={`chgBuilding.${index}.bdstChgNm`} + render={({ field: f }) => ( + <FormUi label="구분"> + <SelectBox + selectClass={selectClass} + selectArray={code.chBuCd} + onSelectValue={(val) => f.onChange(val)} + value={f.value ?? code.chBuCd[0]?.val} + className={""} + label={""} + readOnly={readonly} + /> + </FormUi> + )} + /> + </div> + + <div className=""> + <FormField + control={form.control} + name={`chgBuilding.${index}.archChgPrmsnDt`} + render={({ field: f }) => ( + <FormUi label="허가일"> + <DatePicker + mode="single" + field={{ + value: safeDate(f.value) || new Date(), + onChange: (date) => f.onChange(date), + }} + selectClass="w-full" + readOnly={readonly} + /> + </FormUi> + )} + /> + </div> + <div className=""> + <FormField + control={form.control} + name={`chgBuilding.${index}.archChgArea`} + render={({ field: f }) => ( + <FormUi label="면적(㎡)"> + <Input + placeholder="-" + type="text" + onChange={(e) => f.onChange(e.target.value)} + value={f.value || ''} + className={selectClass} + readOnly={readonly} + maxLength={numSize} + /> + </FormUi> + )} + /> + </div> + </div> + ))} + <div className="border-b border-gray-300 my-4"> </div> + </> + ) +} \ No newline at end of file diff --git a/apps/wr/components/ui/form/fcltReg/bdstReg/bdstReg.tsx b/apps/wr/components/ui/form/fcltReg/bdstReg/bdstReg.tsx index 5ab5edf98..0d50ac706 100644 --- a/apps/wr/components/ui/form/fcltReg/bdstReg/bdstReg.tsx +++ b/apps/wr/components/ui/form/fcltReg/bdstReg/bdstReg.tsx @@ -1,83 +1,63 @@ import { useWrStore } from "@/app/store"; +import { numSize, txtSize } from "@/app/store/common/code/basicCode"; import { selectClass } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; import { formH1Class } from "@/components/lib/class/comClass"; import { - formatCommaNumber, - getCity, - handleCommaNumberChange, - safeDate, -} from "@/components/lib/func/commom"; -import type { objSearchType } from "@/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal"; -import { + DatePicker, FormField, FormUi, - DatePicker, SelectBox, type SelectDataType, } from "@common/business_components"; -import { Button, Checkbox, Input } from "@common/components"; -import { Check } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; -import { useFieldArray } from "react-hook-form"; +import { Input } from "@common/components"; +import { useEffect, useState } from "react"; -export default function BdstReg({ form }: { form: any }) { +export default function BdstReg({ form, type }: { form: any; type?: string }) { const { code } = useWrStore((state) => { return state; }); - const { fields, append, remove } = useFieldArray({ - name: "chgBuilding", - control: form?.control, // Provide a fallback to avoid errors - }); + const { userSiSggCd } = code + const [useCityCd, setUserCityCd] = useState<SelectDataType[]>([]) + const [useSggCd, setUserSggCd] = useState<SelectDataType[]>([]) - const plusHist = () => { - append({ - bdstNm: "", - daddr: "", - bdstUsgCd: "", - }); - }; -// 잠깐 주석처리 - // const removeLastItem = () => { - // if (fields.length > 0) { - // remove(fields.length - 1); - // } - // }; - const removeSelected = () => { - const idxsToRemove = fields - .map((item, idx) => selectedIds.includes(item.id) ? idx : -1) - .filter(index => index > -1) - .sort((prevIndex, nextIndex) => nextIndex - prevIndex); - - remove(idxsToRemove); - setSelectedIds([]); - }; - - - const [selectSgg, setSelectSgg] = useState<SelectDataType[] | undefined>(); - const [objSearch, setObjSearch] = useState<objSearchType>({ - sgg: "", - city: "", - }); - - const getSelCity = (val: string) => { - const data = getCity(val, code.sggCd); - setSelectSgg(data); - const sgg = form.getValues("sggCd"); - - setObjSearch({ city: val, sgg: "" }); - }; - - const getSgg = (val: string | number) => { - const sgg = form.getValues("sggCd"); - setObjSearch({ ...objSearch, sgg: val != "" ? String(val) : sgg }); - }; + // const [selectSgg, setSelectSgg] = useState<SelectDataType[] | undefined>(); + // const [objSearch, setObjSearch] = useState<objSearchType>({ + // sgg: "", + // city: "", + // }); useEffect(() => { - const city = form.getValues("ctpvCd"); - const sgg = form.getValues("sggCd"); - setObjSearch({ city: city, sgg: sgg }); - }, [selectSgg]); + if (userSiSggCd.length === 2 && code.cityCd.length > 0) { + const city = code.cityCd.filter((item) => item.val === userSiSggCd) + const sgg = code.sggCd.filter((item) => item.val.slice(0, 2) === userSiSggCd) + sgg.unshift({ text: '전체', val: '0' }) + setUserCityCd(city) // Fix: Ensure city is of type SelectDataType[] + setUserSggCd(sgg) + if (city[0]?.val) { + // 여기 중요 + queueMicrotask(() => { + form.setValue("ctpvCd", Number(city[0]?.val), { shouldDirty: true, shouldTouch: true, shouldValidate: true }); + }); + } + } else if (userSiSggCd.length === 5 && code.cityCd.length > 0 && code.sggCd.length > 0) { + const city = code.cityCd.filter((item) => item.val === userSiSggCd.slice(0, 2)) + const sgg = code.sggCd.filter((item) => item.val === userSiSggCd) + setUserSggCd(sgg) + setUserCityCd(city) + form.setValue('ctpvCd', Number(city[0]?.val)) + form.setValue('sggCd', Number(sgg[0]?.val)) + + } + }, [form, userSiSggCd, code.cityCd, code.sggCd]) + + + + // useEffect(() => { + // const city = form.getValues("ctpvCd"); + // const sgg = form.getValues("sggCd"); + // setObjSearch({ city: city, sgg: sgg }); + // }, [selectSgg]); useEffect(() => { const subscription = form.form?.watch((value: any) => { @@ -87,21 +67,15 @@ export default function BdstReg({ form }: { form: any }) { return () => subscription?.unsubscribe(); }, [form.form]); - const [selectedIds, setSelectedIds] = useState<string[]>([]); - const toggleSelect = (id: string) => { - setSelectedIds(prev => - prev.includes(id) - ? prev.filter(x => x !== id) - : [...prev, id] - ); - }; + + useEffect(() => { + console.log('form.formState ', form.watch("archArea")) + }, [form.formState]) return ( <> - <h1 className={formH1Class}> - 건축물 시설 정보 - </h1> + <h1 className={formH1Class}> {type !== "HTB" ? "건축물 기본 정보" : '시설 기본 정보'}</h1> <div className="grid grid-cols-6 gap-2 mt-4"> <div className="col-span-3"> <FormField @@ -113,12 +87,11 @@ export default function BdstReg({ form }: { form: any }) { className="col-span-3" label={""} selectClass={selectClass} - selectArray={code.cityCd} + selectArray={useCityCd} onSelectValue={(val) => { - getSelCity(String(val)); field.onChange(Number(val)); }} - value={field.value} + value={typeof field.value !== "undefined" ? field.value : useCityCd[0]?.val} /> </FormUi> )} @@ -134,9 +107,8 @@ export default function BdstReg({ form }: { form: any }) { className="col-span-3" label={""} selectClass={`w-full min-h-[2.9rem] ${selectClass}`} - selectArray={selectSgg ? selectSgg : code.sggCd || []} + selectArray={useSggCd} onSelectValue={(val) => { - getSgg(val); field.onChange(Number(val)); }} value={field.value} @@ -152,12 +124,16 @@ export default function BdstReg({ form }: { form: any }) { control={form.control} name="bdstNm" render={({ field }) => ( - <FormUi label="건축물명"> + <FormUi label={type !== "HTB" ? "건축물명" : '시설명'}> <Input className={selectClass} - placeholder="1,000,000" + placeholder={type !== "HTB" ? "건축물 명 입력하세요" : '시설명 입력하세요'} type="text" - {...field} + onChange={(e) => { + field.onChange(e.target.value) + }} + value={field.value || ''} + maxLength={txtSize} /> </FormUi> )} @@ -168,173 +144,114 @@ export default function BdstReg({ form }: { form: any }) { control={form.control} name="daddr" render={({ field }) => ( - <FormUi label="위치"> + <FormUi label={type !== "HTB" ? "건축물 주소" : '시설 주소'} > <Input className={selectClass} - placeholder="1,000,000" + placeholder="위치를 입력해주세요" type="search" - {...field} - /> - </FormUi> - )} - /> - </div> - <div className="col-span-2"> - <FormField - control={form.control} - name="reuseBuilding.0.bdstUsgCd" - render={({ field }) => ( - <FormUi label="건축물용도"> - <SelectBox - className={""} - selectClass={selectClass} - label={""} - selectArray={code.bdstUsgCd} - onSelectValue={(val) => { - field.onChange(Number(val)); + onChange={(e) => { + field.onChange(e.target.value) }} - value={field.value} + value={field.value || ''} + maxLength={50} /> </FormUi> )} /> </div> + <div className="col-span-2"> <FormField control={form.control} name="frstArchPrmsnDt" render={({ field }) => ( - <FormUi label="최초건축허가일"> + <FormUi label={type !== "HTB" ? "최초건축허가일" : '준공년도'} > <DatePicker mode={"single"} field={{ - value: field.value || new Date(), + value: field.value ?? new Date(), onChange: (date: Date | undefined) => field.onChange(date), }} - selectClass={'w-full'} + // modifiers={{ + // disabled: { after: new Date() } + // }} + selectClass={"w-full"} ></DatePicker> </FormUi> )} /> </div> - <div className="col-span-2"> - <FormField - control={form.control} - name="archArea" - render={({ field }) => ( - <FormUi label="건축면적(m³)"> - <Input - className={selectClass} - placeholder="1,000,000" - type="number" - onChange={(e) => { - handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - /> - </FormUi> - )} - /> - </div> - <div className="col-span-2"> - <FormField - control={form.control} - name="reuseBuilding.0.lawTrgtYn" - render={({ field }) => ( - <FormUi label="법정의무대상여부"> - <SelectBox - className={""} - selectClass={selectClass} - label={""} - selectArray={code.yn} - onSelectValue={(val) => { - field.onChange(val); - }} - value={field.value} - /> - </FormUi> - )} - /> - </div> </div> - <h1 className={formH1Class}> - 중축, 개축, 재축 - </h1> - <div className="flex justify-end gap-2 mt-4"> - <Button type="button" className="" size={"sm"} onClick={plusHist}> - 이력추가 - </Button> - <Button - type="button" - className="!bg-red-500 text-white" - size={"sm"} - onClick={removeSelected} - > - 삭제 - </Button> - </div> - {fields.map((field, index) => ( - <div key={field.id} className="grid grid-cols-[auto,1fr,1fr,1fr] gap-2 mt-4 items-center"> - <div className="col-span-1 flex justify-center mr-4"> - <Checkbox - checked={selectedIds.includes(field.id)} - onCheckedChange={() => toggleSelect(field.id)} - /> - </div> - <div className=""> - <FormField - control={form.control} - name={`chgBuilding.${index}.bdstChgNm`} - render={({ field: f }) => ( - <FormUi label="구분"> - <SelectBox - selectClass={selectClass} - selectArray={code.chBuCd} - onSelectValue={val => f.onChange(val)} - value={f.value ?? code.chBuCd[0]?.val} className={""} label={""} /> - </FormUi> - )} - /> - </div> - <div className=""> - <FormField - control={form.control} - name={`chgBuilding.${index}.archChgPrmsnDt`} - render={({ field: f }) => ( - <FormUi label="허가일"> - <DatePicker - mode="single" - field={{ - value: safeDate(f.value) || new Date(), - onChange: (date) => f.onChange(date), - }} - selectClass="w-full" - /> - </FormUi> - )} - /> - </div> - <div className=""> - <FormField - control={form.control} - name={`chgBuilding.${index}.archChgArea`} - render={({ field: f }) => ( - <FormUi label="면적"> - <Input - placeholder="-" - type="text" - onChange={e => handleCommaNumberChange(e, f.onChange)} - value={formatCommaNumber(f.value)} - className={selectClass} - /> - </FormUi> - )} - /> - </div> - </div> - ))} - <div className="border-b border-gray-300 my-4"> </div> + {type !== "HTB" ? ( + <> + <div className="grid grid-cols-6 gap-2 mt-4 "> + <div className="col-span-2"> + <FormField + control={form.control} + name="reuseBuilding.0.bdstUsgCd" + render={({ field }) => ( + <FormUi label="건축물용도"> + <SelectBox + className={""} + selectClass={selectClass} + label={""} + selectArray={code.bdstUsgCd} + onSelectValue={(val) => { + field.onChange(Number(val)); + }} + value={field.value ?? code.bdstUsgCd[0]?.val} + /> + </FormUi> + )} + /> + </div> + + <div className="col-span-2"> + <FormField + control={form.control} + name="archArea" + render={({ field }) => ( + <FormUi label="건축면적(m³)"> + <Input + className={selectClass} + placeholder="면적을 입력하세요" + type="text" + onChange={(e) => { + field.onChange(e.target.value) + }} + value={field.value || ''} + maxLength={numSize} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-2"> + <FormField + control={form.control} + name="reuseBuilding.0.lawTrgtYn" + render={({ field }) => ( + <FormUi label="법정의무대상여부"> + <SelectBox + className={""} + selectClass={selectClass} + label={""} + selectArray={code.yn} + onSelectValue={(val) => { + field.onChange(val); + }} + value={field.value ?? code.yn[0]?.val} + /> + </FormUi> + )} + /> + </div> + </div> + </> + ) : ( + <></> + )} </> ); } diff --git a/apps/wr/components/ui/form/fcltReg/htdflReg/htdflRegForm.tsx b/apps/wr/components/ui/form/fcltReg/htdflReg/htdflRegForm.tsx index 70e62eb7f..23eb3c1b4 100644 --- a/apps/wr/components/ui/form/fcltReg/htdflReg/htdflRegForm.tsx +++ b/apps/wr/components/ui/form/fcltReg/htdflReg/htdflRegForm.tsx @@ -1,39 +1,48 @@ "use client"; +import { useWrStore } from "@/app/store"; +import { numSize, txtSize } from "@/app/store/common/code/basicCode"; +import { + hdtflZod, + type customHtdflBdst, +} from "@/app/store/ruseFcltMng/fcltReg/htdflReg"; +import { selectClass } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; +import { formH1Class } from "@/components/lib/class/comClass"; +import { + confirmToast, + formatCommaNumber, + handleCommaNumberChange, + handleOnlyNumberChange, +} from "@/components/lib/func/commom"; import { Form, FormField, FormUi, SelectBox, } from "@common/business_components/ui"; -import { Button, Input } from "@common/components/ui"; +import { Button, Input, Label, ScrollArea } from "@common/components/ui"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useCallback, useEffect, useState } from "react"; import { useForm } from "react-hook-form"; +import toast from "react-hot-toast"; +import { useTranslation } from "react-i18next"; import { z } from "zod"; -import { useWrStore } from "@/app/store"; -import { selectClass } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; -import { - hdtflZod, - type customHtdflBdst, -} from "@/app/store/ruseFcltMng/fcltReg/htdflReg"; -import { useEffect } from "react"; import BdstReg from "../bdstReg/bdstReg"; -import { - formatCommaNumber, - handleCommaNumberChange, -} from "@/components/lib/func/commom"; -import { formH1Class } from "@/components/lib/class/comClass"; export default function HtdflRegForm({ htdflForm, isModal, - readonly = false + readonly = false, + setIsModal, }: { htdflForm: customHtdflBdst; isModal: boolean; readonly?: boolean; + setIsModal?: (modal: boolean) => void; }) { const { code, htdflBdstUpdate, htdflBdstReg } = useWrStore((state) => state); + const [frontPHdata, setFrontPHdata] = useState(""); + const [backPHdata, setBackPHdata] = useState(""); const defaultValues: z.infer<typeof hdtflZod> = hdtflZod.parse({}); @@ -41,16 +50,31 @@ export default function HtdflRegForm({ resolver: zodResolver(hdtflZod), defaultValues: defaultValues, }); + const [regCtpvCd, regSggcd] = form.watch(["ctpvCd", "sggCd"]); + const bdstNm = form.watch("bdstNm"); + const isPowerPlant = bdstNm?.includes("발전소"); + const { t } = useTranslation("base"); const onSubmit = (values: z.infer<typeof hdtflZod>) => { if (isModal) { - htdflBdstReg(values, '41'); + const data = regCtpvCd ? regCtpvCd : regSggcd ? regSggcd : "noCd"; + if (data === "noCd") { + toast.error("시도/시군구 코드를 선택해야합니다."); + return; + } + const handleConfirm = async () => { + htdflBdstReg(values, data); + if (setIsModal) setIsModal(false); + }; + confirmToast(t("isInsertMsg"), handleConfirm); } else { - htdflBdstUpdate(values, '41'); + const handleConfirm = async () => { + htdflBdstUpdate(values, "0"); + }; + confirmToast(t("isUpdateMsg"), handleConfirm); } }; - const cleanHtdflData = (raw: any): z.infer<typeof hdtflZod> => { return { ...raw, @@ -75,14 +99,156 @@ export default function HtdflRegForm({ })), }; }; + const isSetFrontPh = useCallback( + (frontPHdata: string) => { + setFrontPHdata(frontPHdata); + if (!frontPHdata) { + form.setValue(`reuseBuilding.0.htdflObj.ph`, ""); + return; + } + const PHdata = form.getValues(`reuseBuilding.0.htdflObj.ph`); + let backPH; + if (PHdata && String(PHdata).includes("~")) { + backPH = PHdata.split("~")[1]; + } else { + backPH = String(Number(frontPHdata) + 1); + } + const MatchData = `${frontPHdata}~${backPH}`; + form.setValue(`reuseBuilding.0.htdflObj.ph`, MatchData); + }, + [form] + ); + const isSetBackPh = useCallback( + (backPHdata: string) => { + setBackPHdata(backPHdata); + if (!backPHdata) { + form.setValue(`reuseBuilding.0.htdflObj.ph`, ""); + return; + } + const PHdata = form.getValues(`reuseBuilding.0.htdflObj.ph`); + let frontPH; + if (PHdata && String(PHdata).includes("~")) { + frontPH = PHdata.split("~")[0]; + } else { + frontPH = String(Math.max(Number(backPHdata) - 1, 0)); + } + const MatchData = `${frontPH}~${backPHdata}`; + form.setValue(`reuseBuilding.0.htdflObj.ph`, MatchData); + }, + [form] + ); + + const [divStyle, setDivStyle] = useState("h-[53vh]"); + useEffect(() => { + if (isModal === true) { + setDivStyle("h-[53vh] max-h-[37vh]"); + } else if (readonly === true) { + setDivStyle("h-[62vh]"); + } else { + setDivStyle("h-[57vh]") + } + }, [isModal, readonly]); useEffect(() => { console.log("form errors:", form.formState.errors); }, [form.formState.errors]); + // form.reset은 비동기로 동작할 수 있어, useEffect 하나로 관리하여 의도치 않은 초기화를 방지합니다. useEffect(() => { - if (hdtflZod) form.reset(cleanHtdflData(htdflForm)); - }, [htdflForm]); + // Case 1: 수정 또는 '복사하여 등록' 등 데이터가 주입되는 경우 + if (htdflForm && Object.keys(htdflForm).length > 0) { + + const htdflPH = htdflForm.reuseBuilding[0]?.htdflObj.ph + if(htdflPH && String(htdflPH).includes("~")){ + const [frontPH, backPH] = htdflPH.split("~"); + if (frontPH) { + setFrontPHdata(frontPH); + } + if (backPH) { + setBackPHdata(backPH); + } + } else { + setFrontPHdata(""); + setBackPHdata(""); + } + + + form.reset(cleanHtdflData(htdflForm)); + }else{ + form.reset(); + } + // Case 2: 데이터가 없는 순수 신규 등록 모드인 경우 + if (isModal) { + const firstYnValue = code.yn?.[0]?.val; + const firstHtdPowerMthdCd = code.htdPowerMthdCd?.[0]?.val; + const firstHtdUtztnUsgCd = code.htdUtztnUsgCd?.[0]?.val; + + if (firstHtdPowerMthdCd) { + form.setValue( + "reuseBuilding.0.htdflObj.etgtType", + firstHtdPowerMthdCd, + { shouldDirty: true } + ); + } + if (firstYnValue) { + form.setValue( + "reuseBuilding.0.htdflObj.plwtYn", + firstYnValue as "Y" | "N", + { shouldDirty: true } + ); + form.setValue( + "reuseBuilding.0.htdflObj.prcsProcsYn", + firstYnValue as "Y" | "N", + { shouldDirty: true } + ); + form.setValue( + "reuseBuilding.0.htdflObj.fmYn", + firstYnValue as "Y" | "N", + { shouldDirty: true } + ); + } + if (firstHtdUtztnUsgCd) { + form.setValue("reuseBuilding.0.htdflObj.utztnUsg", firstHtdUtztnUsgCd, { + shouldDirty: true, + }); + } + } + }, [htdflForm, code, form]); + + useEffect(() => { + if (!isPowerPlant) { + form.setValue( + "reuseBuilding.0.htdflObj.etgtType", + "해당없음", // 빈 문자열로 초기화 + { shouldDirty: true } // dirty 상태로 남길지 여부는 취사선택 + ); + } + }, [isPowerPlant, form]); + + +// useEffect(() => { +// const watchPH = form.getValues(`items.${indexMng}.fcBizs.${indexBiz}.ph`); +// const checkPkAndCycle = +// form.getValues(`items.${indexMng}.pbnstRuseFcltId`) + +// String(form.getValues(`items.${indexMng}.fcBizs.${indexBiz}.cycl`)); + +// console.log("checkPkAndCycle ", checkPkAndCycle); +// if (chekcPk !== checkPkAndCycle) { +// if (watchPH && watchPH !== undefined) { +// const [frontPH, backPH] = watchPH.split("~"); +// if (frontPH) { +// setFrontPHdata(frontPH); +// } +// if (backPH) { +// setBackPHdata(backPH); +// } +// } else { +// setFrontPHdata("0"); +// setBackPHdata("0"); +// } +// setCheakPk(checkPkAndCycle); +// } +// }, [form.getValues(`items.${indexMng}.fcBizs.${indexBiz}.ph`), chekcPk]); return ( <div className="!mx-[1.6rem] !p-0r"> @@ -90,543 +256,657 @@ export default function HtdflRegForm({ <form onSubmit={form.handleSubmit(onSubmit)}> {/* <div className="flex justify-between items-center mx-3"> */} <div> + <h1 className="text-2xl font-bold text-gray-900 mt-3 ml-1"> + {readonly && `(시설명 : ${htdflForm?.bdstNm})`} + </h1> <div className="grid grid-cols-12 flex items-center card-header"> - <h1 className="text-2xl font-medium text-label col-span-10">온배수 시설 정보</h1> + <h1 className="text-2xl font-medium text-label col-span-10"> + 온배수 시설 등록 + </h1> {!readonly && ( - <Button type="submit" className="col-start-12 col-span-1" size={"sm"}> + <Button + type="submit" + className="col-start-12 col-span-1" + size={"sm"} + > 저장 </Button> )} </div> </div> + <ScrollArea className={divStyle}> + {isModal && htdflForm != undefined ? ( + <BdstReg form={form} type="HTB" /> + ) : ( + <></> + )} + {isModal ? ( + <div className=" my-4"> + <h1 className={formH1Class}>온배수 시설 정보 </h1> + </div> + ) : ( + <></> + )} - {isModal && htdflForm != undefined ? <BdstReg form={form} /> : <></>} - {isModal ? ( - <h1 className="text-xl col-span-10 mt-4"> - 온배수 시설 정보{" "} - </h1> - ) : ( - <></> - )} + <div className="grid grid-cols-6 gap-2 mt-4"> + <div className="col-span-2"> + <FormField + control={form.control} + name="reuseBuilding.0.htdflObj.etgtType" + render={({ field }) => ( + <FormUi label="발전유형"> + <SelectBox + className={""} + label={""} + selectClass={selectClass} + selectArray={code.htdPowerMthdCd} + onSelectValue={readonly ? () => {} : field.onChange} + // value={field.value ?? ""} + value={ + field.value || + (code?.htdPowerMthdCd[0] && + code?.htdPowerMthdCd[0].val) + } + readOnly={readonly || !isPowerPlant} + /> + </FormUi> + )} + /> + </div> - <div className="grid grid-cols-6 gap-2 mt-4"> - <div className="col-span-2"> - <FormField - control={form.control} - name="reuseBuilding.0.htdflObj.etgtType" - render={({ field }) => ( - <FormUi label="발전유형(코드정비후 수정)"> - <Input - className={selectClass} - placeholder="1,000,000" - type="text" - value={field.value ?? ""} - onChange={(e) => { - if (!readonly) field.onChange(e) - }} - readOnly={readonly} - /> - </FormUi> - )} - /> + <div className="col-span-2"> + <FormField + control={form.control} + name="reuseBuilding.0.htdflObj.fclCpct" // 이름 이상함. + render={({ field }) => ( + <FormUi label="설비용량(KW)"> + <Input + className={selectClass} + placeholder="1,000,000" + type="text" + onChange={(e) => { + if (!readonly) + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={readonly} + maxLength={numSize} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-2"> + <FormField + control={form.control} + name="reuseBuilding.0.htdflObj.plwtYn" + render={({ field }) => ( + <FormUi label="담수화 여부"> + <SelectBox + className={""} + label={""} + selectClass={selectClass} + selectArray={code.yn} + onSelectValue={readonly ? () => {} : field.onChange} + value={field.value ?? ""} + readOnly={readonly} + /> + </FormUi> + )} + /> + </div> + </div> + <div className=" my-4"> + <h1 className={formH1Class}>온배수 시설 운영현황</h1> </div> - <div className="col-span-2"> - <FormField - control={form.control} - name="reuseBuilding.0.htdflObj.fclCpct" // 이름 이상함. - render={({ field }) => ( - <FormUi label="설비용량"> - <Input - className={selectClass} - placeholder="1,000,000" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={readonly} - /> - </FormUi> - )} - /> - </div> - <div className="col-span-2"> - <FormField - control={form.control} - name="reuseBuilding.0.htdflObj.plwtYn" - render={({ field }) => ( - <FormUi label="담수화 여부"> - <SelectBox - className={""} - label={""} - selectClass={selectClass} - selectArray={code.yn} - onSelectValue={readonly ? () => { } : field.onChange} - value={field.value ?? code.yn[0]?.val} - readOnly={readonly} - /> - </FormUi> - )} - /> - </div> - </div> - <div className=" my-4"> - <h1 className={formH1Class}> - 온배수 시설 운영현황 - </h1> - </div> - - <div className="grid grid-cols-10 gap-1 mt-4"> - <div className="col-span-5"> - <FormField - control={form.control} - name="reuseBuilding.0.htdflObj.orgwtWtritSpt" - render={({ field }) => ( - <FormUi label="원수취수 지점(코드정비후 수정)"> - <Input - className={selectClass} - placeholder="1,000,000" - type="text" - value={field.value ?? 0} - onChange={(e) => { - if (!readonly) field.onChange(Number(e.target.value)); - }} - readOnly={readonly} - /> - </FormUi> - )} - /> - </div> - <div className="col-span-5"> - <FormField - control={form.control} - name="reuseBuilding.0.htdflObj.orgwtOcrnProcs" - render={({ field }) => ( - <FormUi label="원수발생 공정"> - <Input - className={selectClass} - placeholder="1,000,000" - type="" - value={field.value ?? ""} - onChange={(e) => { - if (!readonly) field.onChange - }} - readOnly={readonly} - /> - </FormUi> - )} - /> - </div> - <div className="col-span-5"> - <FormField - control={form.control} - name="reuseBuilding.0.htdflObj.wtritEntrcTp" - render={({ field }) => ( - <FormUi label="취수구 온도"> - <Input - className={selectClass} - placeholder="1,000,000" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={readonly} - /> - </FormUi> - )} - /> - </div> - <div className="col-span-5"> - <FormField - control={form.control} - name="reuseBuilding.0.htdflObj.amtgnr" - render={({ field }) => ( - <FormUi label="발생량(m³/년)"> - <Input - className={selectClass} - placeholder="1,000,000" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={readonly} - /> - </FormUi> - )} - /> - </div> - <div className="col-span-5"> - <FormField - control={form.control} - name="reuseBuilding.0.htdflObj.prcsProcsYn" - render={({ field }) => ( - <FormUi label="처리공정"> - <SelectBox - className={""} - label={""} - selectClass={selectClass} - selectArray={code.yn} - onSelectValue={readonly ? () => { } : field.onChange} - value={field.value ?? code.yn[0]?.val} - readOnly={readonly} - /> - </FormUi> - )} - /> - </div> - <div className="col-span-5"> - <FormField - control={form.control} - name="reuseBuilding.0.htdflObj.prcsFcltCpct" - render={({ field }) => ( - <FormUi label="처리시설 용량(m³/년)"> - <Input - className={selectClass} - placeholder="1,000,000" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={readonly} - /> - </FormUi> - )} - /> - </div> - <div className="col-span-5"> - <FormField - control={form.control} - name="reuseBuilding.0.htdflObj.fmYn" - render={({ field }) => ( - <FormUi label="유량계유무"> - <SelectBox - className={""} - selectClass={selectClass} - label={""} - selectArray={code.yn} - onSelectValue={readonly ? () => { } : field.onChange} - value={field.value ?? code.yn[0]?.val} - readOnly={readonly} - /> - </FormUi> - )} - /> - </div> - <div className="col-span-5"> - <FormField - control={form.control} - name="reuseBuilding.0.htdflObj.utztnCpct" - render={({ field }) => ( - <FormUi label="이용량(m³/년)"> - <Input - className={selectClass} - placeholder="1,000,000" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={readonly} - /> - </FormUi> - )} - /> - </div> - <div className="col-span-5"> - <FormField - control={form.control} - name="reuseBuilding.0.htdflObj.utztnUsg" - render={({ field }) => ( - <FormUi label="이용용도"> - <Input - className={selectClass} - placeholder="1,000,000" - type="text" - value={field.value ?? ""} - onChange={() => { - if (!readonly) field.onChange - }} - readOnly={readonly} - /> - </FormUi> - )} - /> - </div> - </div> - - <div className=" my-2"> - <h1 className={formH1Class}> - 요구수질 - </h1> - </div> - - <div className="grid grid-cols-12 gap-4"> - <div className="col-span-6"> - <FormField - control={form.control} - name="reuseBuilding.0.htdflObj.grssClfg" - render={({ field }) => ( - <FormUi label="총대장균군"> - <Input - className={selectClass} - placeholder="10" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={readonly} - /> - </FormUi> - )} - /> + {/* <div className="grid grid-cols-10 gap-1 mt-4"> */} + <div className="grid grid-cols-12 gap-4"> + <div className="col-span-6"> + <FormField + control={form.control} + name="reuseBuilding.0.htdflObj.orgwtWtritSpt" + render={({ field }) => ( + <FormUi label="원수취수 지점"> + <Input + className={selectClass} + placeholder="" + type="text" + value={field.value ?? ""} + onChange={(e) => { + if (!readonly) field.onChange(e.target.value); + }} + readOnly={readonly} + maxLength={txtSize} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-6"> + <FormField + control={form.control} + name="reuseBuilding.0.htdflObj.orgwtOcrnProcs" + render={({ field }) => ( + <FormUi label="원수발생 공정"> + <Input + className={selectClass} + placeholder="" + type="text" + value={field.value ?? ""} + onChange={(e) => { + if (!readonly) field.onChange(e.target.value); + }} + readOnly={readonly} + maxLength={txtSize} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-6"> + <FormField + control={form.control} + name="reuseBuilding.0.htdflObj.wtritEntrcTp" + render={({ field }) => ( + <FormUi label="취수구 온도(℃)"> + <Input + className={selectClass} + placeholder="1,000,000" + type="text" + onChange={(e) => { + if (!readonly) + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={readonly} + maxLength={numSize} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-6"> + <FormField + control={form.control} + name="reuseBuilding.0.htdflObj.emsnExitTp" + render={({ field }) => ( + <FormUi label="배출구 온도(℃)"> + <Input + className={selectClass} + placeholder="10" + type="text" + onChange={(e) => { + if (!readonly) + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={readonly} + maxLength={numSize} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-6"> + <FormField + control={form.control} + name="reuseBuilding.0.htdflObj.amtgnr" + render={({ field }) => ( + <FormUi label="발생량(m³/일)"> + <Input + className={selectClass} + placeholder="1,000,000" + type="text" + onChange={(e) => { + if (!readonly) + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={readonly} + maxLength={numSize} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-6"> + <FormField + control={form.control} + name="reuseBuilding.0.htdflObj.prcsProcsYn" + render={({ field }) => ( + <FormUi label="처리공정 유무"> + <SelectBox + className={""} + label={""} + selectClass={selectClass} + selectArray={code.yn} + onSelectValue={readonly ? () => {} : field.onChange} + value={field.value ?? ""} + readOnly={readonly} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-6"> + <FormField + control={form.control} + name="reuseBuilding.0.htdflObj.prcsFcltCpct" + render={({ field }) => ( + <FormUi label="처리시설 용량(m³/일)"> + <Input + className={selectClass} + placeholder="1,000,000" + type="text" + onChange={(e) => { + if (!readonly) + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={readonly} + maxLength={numSize} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-6"> + <FormField + control={form.control} + name="reuseBuilding.0.htdflObj.fmYn" + render={({ field }) => ( + <FormUi label="유량계유무"> + <SelectBox + className={""} + selectClass={selectClass} + label={""} + selectArray={code.yn} + onSelectValue={readonly ? () => {} : field.onChange} + value={field.value ?? ""} + readOnly={readonly} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-6"> + <FormField + control={form.control} + name="reuseBuilding.0.htdflObj.utztnCpct" + render={({ field }) => ( + <FormUi label="이용량(m³/일)"> + <Input + className={selectClass} + placeholder="1,000,000" + type="text" + onChange={(e) => { + if (!readonly) + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={readonly} + maxLength={numSize} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-6"> + <FormField + control={form.control} + name="reuseBuilding.0.htdflObj.utztnUsg" + render={({ field }) => ( + <FormUi label="이용용도"> + <SelectBox + className={""} + selectClass={selectClass} + label={""} + selectArray={code.htdUtztnUsgCd} + onSelectValue={readonly ? () => {} : field.onChange} + value={field.value ?? ""} + readOnly={readonly} + /> + </FormUi> + )} + /> + </div> </div> - <div className="col-span-6"> - <FormField - control={form.control} - name="reuseBuilding.0.htdflObj.tn" - render={({ field }) => ( - <FormUi label="총질소"> - <Input - className={selectClass} - placeholder="10" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={readonly} - /> - </FormUi> - )} - /> - </div> - </div> - - <div className="grid grid-cols-12 gap-4"> - <div className="col-span-6"> - <FormField - control={form.control} - name="reuseBuilding.0.htdflObj.combResdch" - render={({ field }) => ( - <FormUi label="결합잔류염소"> - <Input - className={selectClass} - placeholder="10" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={readonly} - /> - </FormUi> - )} - /> + <div className=" my-2"> + <h1 className={formH1Class}>수질</h1> </div> - <div className="col-span-6 "> - <FormField - control={form.control} - name="reuseBuilding.0.htdflObj.tph" - render={({ field }) => ( - <FormUi label="총인"> - <Input - className={selectClass} - placeholder="10" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={readonly} - /> - </FormUi> - )} - /> - </div> - </div> + <div className="grid grid-cols-12 gap-4"> + <div className="col-span-6"> + <FormField + control={form.control} + name="reuseBuilding.0.htdflObj.grssClfg" + render={({ field }) => ( + <FormUi label="총대장균군(개/mg)"> + <div className="flex items-center gap-2"> + <div className="relative flex-1 min-w-0"> + <Input + className={selectClass} + placeholder="10" + type="text" + onChange={(e) => { + if (!readonly) + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={readonly} + maxLength={6} + /> + <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + 이하 + </span> + </div> + </div> + </FormUi> + )} + /> + </div> - <div className="grid grid-cols-12 gap-4"> - <div className="col-span-6"> - <FormField - control={form.control} - name="reuseBuilding.0.htdflObj.trbt" - render={({ field }) => ( - <FormUi label="탁도"> - <Input - className={selectClass} - placeholder="10" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={readonly} - /> - </FormUi> - )} - /> + <div className="col-span-6"> + <FormField + control={form.control} + name="reuseBuilding.0.htdflObj.tn" + render={({ field }) => ( + <FormUi label="총질소(T-N)(mg/L)"> + <div className="flex items-center gap-2"> + <div className="relative flex-1 min-w-0"> + <Input + className={selectClass} + placeholder="" + type="text" + onChange={(e) => { + if (!readonly) + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={readonly} + maxLength={6} + />{" "} + <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + 이하 + </span> + </div> + </div> + </FormUi> + )} + /> + </div> </div> - <div className="col-span-6"> - <FormField - control={form.control} - name="reuseBuilding.0.htdflObj.ph" - render={({ field }) => ( - <FormUi label="수소이온농도"> - <Input - className={selectClass} - placeholder="10" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={readonly} - /> - </FormUi> - )} - /> - </div> - </div> + <div className="grid grid-cols-12 gap-4"> + <div className="col-span-6"> + <FormField + control={form.control} + name="reuseBuilding.0.htdflObj.combResdch" + render={({ field }) => ( + <FormUi label="결합잔류염소(mh/L)"> + <Input + className={selectClass} + placeholder="10" + type="text" + onChange={(e) => { + if (!readonly) + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={readonly} + maxLength={numSize} + /> + </FormUi> + )} + /> + </div> - <div className="grid grid-cols-12 gap-4"> - <div className="col-span-6"> - <FormField - control={form.control} - name="reuseBuilding.0.htdflObj.bod" - render={({ field }) => ( - <FormUi label="생물학적 산소요구량"> - <Input - className={selectClass} - placeholder="10" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={readonly} - /> - </FormUi> - )} - /> + <div className="col-span-6 "> + <FormField + control={form.control} + name="reuseBuilding.0.htdflObj.tph" + render={({ field }) => ( + <FormUi label="총인(T-P)(mg/L)"> + <div className="flex items-center gap-2"> + <div className="relative flex-1 min-w-0"> + <Input + className={selectClass} + placeholder="10" + type="text" + onChange={(e) => { + if (!readonly) + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={readonly} + maxLength={6} + /> + <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + 이하 + </span> + </div> + </div> + </FormUi> + )} + /> + </div> </div> - <div className="col-span-6"> - <FormField - control={form.control} - name="reuseBuilding.0.htdflObj.clIonQnt" - render={({ field }) => ( - <FormUi label="염화물"> - <Input - className={selectClass} - placeholder="10" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={readonly} - /> - </FormUi> - )} - /> - </div> - </div> - - <div className="grid grid-cols-12 gap-4"> - <div className="col-span-6"> - <FormField - control={form.control} - name="reuseBuilding.0.htdflObj.smll" - render={({ field }) => ( - <FormUi label="냄새"> - <Input - className={selectClass} - placeholder="10" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={readonly} - /> - </FormUi> - )} - /> + <div className="grid grid-cols-12 gap-4"> + <div className="col-span-6"> + <FormField + control={form.control} + name="reuseBuilding.0.htdflObj.trbt" + render={({ field }) => ( + <FormUi label="탁도(NTU)"> + <div className="flex items-center gap-2"> + <div className="relative flex-1 min-w-0"> + <Input + className={selectClass} + placeholder="10" + type="text" + onChange={(e) => { + handleOnlyNumberChange(e, field.onChange); + }} + value={ + field.value !== undefined && field.value !== 0 + ? `${field.value}` + : "" + } + readOnly={readonly} + maxLength={6} + /> + <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + 이상 + </span> + </div> + </div> + </FormUi> + )} + /> + </div> + <div className="col-span-6"> + <FormField + control={form.control} + name="reuseBuilding.0.htdflObj.crmty" + render={({ field }) => ( + <FormUi label="색도(도)"> + <div className="flex items-center gap-2"> + <div className="relative flex-1 min-w-0"> + <Input + className={selectClass} + placeholder="10" + type="text" + onChange={(e) => { + if (!readonly) + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={readonly} + maxLength={numSize} + /> + <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + 이상 + </span> + </div> + </div> + </FormUi> + )} + /> + </div> </div> - <div className="col-span-6"> - <FormField - control={form.control} - name="reuseBuilding.0.htdflObj.cdtnv" - render={({ field }) => ( - <FormUi label="전기전도도"> - <Input - className={selectClass} - placeholder="10" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={readonly} - /> - </FormUi> - )} - /> - </div> - </div> + <div className="grid grid-cols-12 gap-4"> + <div className="col-span-6"> + <FormField + control={form.control} + name="reuseBuilding.0.htdflObj.bod" + render={({ field }) => ( + <FormUi label="생물학적 산소요구량(mg/L)"> + <div className="flex items-center gap-2"> + <div className="relative flex-1 min-w-0"> + <Input + className={selectClass} + placeholder="10" + type="text" + onChange={(e) => { + if (!readonly) + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={readonly} + maxLength={numSize} + /> + <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + 이하 + </span> + </div> + </div> + </FormUi> + )} + /> + </div> - <div className="grid grid-cols-12 gap-4"> - <div className="col-span-6"> - <FormField - control={form.control} - name="reuseBuilding.0.htdflObj.crmty" - render={({ field }) => ( - <FormUi label="색도"> - <Input - className={selectClass} - placeholder="10" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={readonly} - /> - </FormUi> - )} - /> + <div className="col-span-6"> + <FormField + control={form.control} + name="reuseBuilding.0.htdflObj.clIonQnt" + render={({ field }) => ( + <FormUi label="염화물(mg/L)"> + <div className="flex items-center gap-2"> + <div className="relative flex-1 min-w-0"> + <Input + className={selectClass} + placeholder="10" + type="text" + onChange={(e) => { + if (!readonly) + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={readonly} + maxLength={7} + /> + <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + 이하 + </span> + </div> + </div> + </FormUi> + )} + /> + </div> </div> - <div className="col-span-6"> - <FormField - control={form.control} - name="reuseBuilding.0.htdflObj.emsnExitTp" - render={({ field }) => ( - <FormUi label="배출구온도"> - <Input - className={selectClass} - placeholder="10" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={readonly} - /> - </FormUi> - )} - /> + <div className="grid grid-cols-12 gap-4"> + <div className="col-span-6"> + <FormField + control={form.control} + name="reuseBuilding.0.htdflObj.smll" + render={({ field }) => ( + <FormUi label="냄새"> + <SelectBox + className={"!text-2xl"} + label={""} + selectClass={undefined} + selectArray={[ + { val: "불쾌하지않을것", text: "불쾌하지않을것" }, + { val: "없음", text: "없음" }, + ]} + readOnly={readonly} + onSelectValue={(val) => { + field.onChange(val); + }} + value={field.value || undefined} + ></SelectBox> + </FormUi> + )} + /> + </div> + <div className="col-span-6"> + <FormField + control={form.control} + name="reuseBuilding.0.htdflObj.cdtnv" + render={({ field }) => ( + <FormUi label="전기전도도(μs/cm)"> + <div className="flex items-center gap-2"> + <div className="relative flex-1 min-w-0"> + <Input + className={selectClass} + placeholder="10" + type="text" + onChange={(e) => { + if (!readonly) + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={readonly} + maxLength={numSize} + /> + <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + 이하 + </span> + </div> + </div> + </FormUi> + )} + /> + </div> </div> - </div> + <div className="grid grid-cols-12 gap-4"> + <div className="col-span-6"> + <Label className="text-xl">수소이온농도(PH)</Label> + <div className="flex flex-row items-center gap-2"> + <Input + className="!text-xl w-1/2" + placeholder="" + type="text" + onChange={(e) => + //setFrontPHdata(e.target.value) + isSetFrontPh(e.target.value) + } + value={frontPHdata ?? ""} + readOnly={readonly} + maxLength={3} + /> + <span className="text-xl">~</span> + <Input + className="!text-xl w-1/2" + placeholder="" + type="text" + onChange={(e) => { + isSetBackPh(e.target.value); + }} + value={backPHdata ?? ""} + readOnly={readonly} + maxLength={3} + /> + </div> + </div> + </div> + </ScrollArea> </form> </Form> </div> diff --git a/apps/wr/components/ui/form/fcltReg/pbnstReg/buseReg/bugRegForm.tsx b/apps/wr/components/ui/form/fcltReg/pbnstReg/buseReg/bugRegForm.tsx index df63b3520..2589422c8 100644 --- a/apps/wr/components/ui/form/fcltReg/pbnstReg/buseReg/bugRegForm.tsx +++ b/apps/wr/components/ui/form/fcltReg/pbnstReg/buseReg/bugRegForm.tsx @@ -6,14 +6,12 @@ import { businessSttnInfo, concentratedWaterInfo, ruseFcltinfo, - ruseWaterQulityList, wasteWaterFcltINfo, } from "@/app/store/ruseFcltMng/fcltReg/pbnstReg/headList"; import { fcBizSchema, type fcBizType, type PbnstRegArrrayType, - type ppInstlType, } from "@/app/store/ruseFcltMng/fcltReg/pbnstReg/type"; import { selectClass } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; import { @@ -22,26 +20,31 @@ import { formTableName, } from "@/components/lib/class/comClass"; import FcOrgwtsRegModal from "@/components/ui/modal/fcltReg/fcOrgwtsRegModal/fcOrgwtsRegModal"; -import RuseMngTable from "@/components/ui/table/pbnstReg/ruseMngTable/ruseMngTable"; import { SelectBox } from "@common/business_components"; import { DatePicker, FormUi, UiModal } from "@common/business_components/ui"; import { Form, FormField } from "@common/business_components/ui/form"; -import { Button, DataTable, Input, Label } from "@common/components"; +import { Button, Input, Label } from "@common/components"; import { useEffect, useRef, useState } from "react"; -import { useFieldArray, useForm, type UseFormReturn } from "react-hook-form"; +import { useFieldArray, type UseFormReturn } from "react-hook-form"; import MngRegFormDetail from "../mngReg/mngRegFormDetail/mngRegFormDetail"; -import { safeDate } from "@/components/lib/func/commom"; +import { + formatCommaNumber, + handleCommaNumberChange, + safeDate, +} from "@/components/lib/func/commom"; import SearchInputText from "@/components/ui/searchInput/searchInput"; import SdmInsertForm from "@common/business_components/ui/sdmInsertForm"; import type { DemandCompType } from "@common/business_components/ui/sdmInsertForm/form"; import RuseMngTableForm from "@/components/ui/table/pbnstReg/ruseMngTableForm/ruseMngTableForm"; import type { SelectDataType } from "@common/business_components"; +import { BuseWaterQulity } from "./buseWaterQulity/buseWaterQulity"; export function PbnstBuseForm({ indexMng = 0, indexBiz = 0, setIndexBiz, form, + readonly = false, }: { formType: string; firstBizData?: fcBizType[]; @@ -49,28 +52,25 @@ export function PbnstBuseForm({ indexMng: number; indexBiz: number; setIndexBiz: (index: number) => void; + readonly?: boolean; }) { - const ref = useRef<ppInstlType[] | null>(null); const { code, pbnst, demandCompList, - getpbnstInsertIndex, - appendRuseFclt, getDemandCompList, + getFcOrgwts, regDemandCompany, } = useWrStore((state) => state); - const { pbnstBizCycle, pbnstRes } = pbnst; + const { fcOrgwtsNmList } = pbnst; const { fields: fcBizs, append: appendBiz } = useFieldArray({ control: form.control, name: `items.${indexMng}.fcBizs`, }); - - const [bizCycle, setBizCycle] = useState("1"); - const [ppiSelectList, setPpiSelectList] = - useState<[{ name: string; val: SelectDataType[] }]>(); + const [ppiSelectList, setPpiSelectList] = useState< + { name: string; val: SelectDataType[] }[] | undefined + >(); const [ppiYn, setPpiYn] = useState("Y"); - const [selectBizCycle, setSelectBizCycle] = useState< { text: string; @@ -79,25 +79,66 @@ export function PbnstBuseForm({ >([{ text: "없음", val: 0 }]); const [isFcOrgwts, setIsFcOrgwts] = useState<boolean>(false); const [sumCpct, setSumCpct] = useState<number>(0); - const [getDmdList, setGetFmdList] = useState<number>(0); const [isDmdModal, setIsDmdModal] = useState<{ isMoal: boolean; data: DemandCompType; }>({ isMoal: false, data: {} as DemandCompType }); useEffect(() => { - const total = + const etc = isNaN( + Number(form.getValues(`items.${indexMng}.fcBizs.${indexBiz}.etc`)) + ); + let total = (form.getValues(`items.${indexMng}.fcBizs.${indexBiz}.gvsbs`) ?? 0) + (form.getValues(`items.${indexMng}.fcBizs.${indexBiz}.lcep`) ?? 0) + (form.getValues(`items.${indexMng}.fcBizs.${indexBiz}.cusrAlotm`) ?? 0); + if (!etc) { + total += Number( + form.getValues(`items.${indexMng}.fcBizs.${indexBiz}.etc`) + ); + } + form.setValue(`items.${indexMng}.fcBizs.${indexBiz}.totalMoney`, total); }, [ form.getValues(`items.${indexMng}.fcBizs.${indexBiz}.gvsbs`), form.getValues(`items.${indexMng}.fcBizs.${indexBiz}.lcep`), form.getValues(`items.${indexMng}.fcBizs.${indexBiz}.cusrAlotm`), + form.getValues(`items.${indexMng}.fcBizs.${indexBiz}.etc`), ]); + const fcOrgwts = form.watch(`items.${indexMng}.fcBizs.${indexBiz}.fcOrgwts`); + + useEffect(() => { + if (fcOrgwts && fcOrgwts?.length > 0) { + const swtrmtFcltIdList = fcOrgwts?.map((item) => item.swtrmtFcltId); + + if (swtrmtFcltIdList.length > 0) { + getFcOrgwts( + swtrmtFcltIdList.filter((item): item is string => item != null), + fcOrgwts?.length + ); + } + } + }, [fcOrgwts]); + + useEffect(() => { + if (fcOrgwtsNmList && fcOrgwtsNmList.length > 0) { + fcOrgwts?.forEach((item, index) => { + const orgwtNm = fcOrgwtsNmList.find( + (orgwt) => orgwt.bscFctCd === item.swtrmtFcltId + )?.bscFctNm; + + if (orgwtNm) { + form.setValue( + `items.${indexMng}.fcBizs.${indexBiz}.fcOrgwts.${index}.ruseUtztnFcltNm`, + orgwtNm + ); + } + }); + } + }, [fcOrgwtsNmList, fcOrgwts]); + // fcBizs 데이터가 변경될 때마다 차수 선택 목록을 업데이트합니다. useEffect(() => { const cycleLength = fcBizs.length; @@ -124,7 +165,7 @@ export function PbnstBuseForm({ // 🧩 새로운 fcBizs 기본 객체 생성 (필수값만 채우고 나머지는 default로) const newItem = fcBizSchema.parse({ cycl: pbnstBizCycle + 1, - pbnstRuseFcltFk: formRuse.pbnstRuseFcltPk, + pbnstRuseFcltId: formRuse.pbnstRuseFcltId, fcOrgwts: undefined, pplInstls: undefined, fcDemands: undefined, @@ -132,28 +173,29 @@ export function PbnstBuseForm({ appendBiz(newItem); setIndexBiz(pbnstBizCycle); // 새로 추가된 차수로 포커스 이동 } - useEffect(() => { - //console.log("form ", form.watch()); - }, [form.formState]); - - const { - fields: fcDemands, - append: appendDemand, - remove: removeDemand, - } = useFieldArray({ + const { fields: fcDemands, append: appendDemand } = useFieldArray({ control: form.control, name: `items.${indexMng}.fcBizs.${indexBiz}.fcDemands`, }); - const { fields: fcOrgwtFiled } = useFieldArray({ - control: form.control, - name: `items.${indexMng}.fcBizs.${indexBiz}.fcOrgwts`, - }); - const { - fields: pplInstls, - append: appendPpl, - remove: removePpl, - } = useFieldArray({ + const demandsContainerRef = useRef<HTMLDivElement>(null); + const prevDemandsLengthRef = useRef(fcDemands.length); + + // 수요처 추가 시 새로 추가된 항목으로 스크롤하는 효과 + useEffect(() => { + // 수요처 항목이 추가되었는지 확인 (길이가 이전보다 길어졌는지) + if (fcDemands.length > prevDemandsLengthRef.current) { + // 컨테이너의 마지막 자식 요소(새로 추가된 수요처 폼)로 부드럽게 스크롤 + demandsContainerRef.current?.lastElementChild?.scrollIntoView({ + behavior: "smooth", + block: "end", + }); + } + // 다음 렌더링을 위해 현재 길이를 ref에 저장 + prevDemandsLengthRef.current = fcDemands.length; + }, [fcDemands.length]); + + const { append: appendPpl, remove: removePpl } = useFieldArray({ control: form.control, name: `items.${indexMng}.fcBizs.${indexBiz}.pplInstls`, }); @@ -166,11 +208,11 @@ export function PbnstBuseForm({ const plusHist = () => { if ( - form.getValues(`items.${indexMng}.fcBizs.${indexBiz}.pbnstRuseFcltBizPk`) + form.getValues(`items.${indexMng}.fcBizs.${indexBiz}.pbnstRuseFcltBizSn`) ) { appendPpl({ - ruseFcltBizFk: form.getValues( - `items.${indexMng}.fcBizs.${indexBiz}.pbnstRuseFcltBizPk` + pbnstRuseFcltBizSn: form.getValues( + `items.${indexMng}.fcBizs.${indexBiz}.pbnstRuseFcltBizSn` ), choose: false, ppMqlt: "", @@ -196,14 +238,14 @@ export function PbnstBuseForm({ splyEndDay: new Date(), splyCpct: 0, splyFmYn: "Y", - pbnstRuseFcltBizPk: form.getValues( - `items.${indexMng}.fcBizs.${indexBiz}.pbnstRuseFcltBizPk` + pbnstRuseFcltBizSn: form.getValues( + `items.${indexMng}.fcBizs.${indexBiz}.pbnstRuseFcltBizSn` ), - pbnstRuseFcltFk: + pbnstRuseFcltId: form.getValues( - `items.${indexMng}.fcBizs.${indexBiz}.pbnstRuseFcltFk` + `items.${indexMng}.fcBizs.${indexBiz}.pbnstRuseFcltId` ) ?? "", - dmdInfoPk: "", + dmdInfoId: "", //telno: "", }); }; @@ -229,7 +271,7 @@ export function PbnstBuseForm({ if (code.pplLenght) { ppl.push({ name: "pplQulity", val: code.pplQulity }); } - if (ppl.length > 0) { + if (ppl && ppl.length > 0) { setPpiSelectList(ppl); } else { setPpiSelectList(undefined); @@ -250,25 +292,12 @@ export function PbnstBuseForm({ setSumCpct(sum); }, [form.formState, indexMng, indexBiz]); - // // 차수별 정보(fcBizs)가 하나도 없을 경우 플레이스홀더 UI를 보여줍니다. - // if (fcBizs.length === 0) { - // return ( - // <div className="!mx-[1.6rem] !p-0r flex flex-col items-center justify-center h-64 text-gray-500 border rounded-md"> - // <p className="text-lg">등록된 차수별 정보가 없습니다.</p> - // <p className="text-sm mb-4">아래 버튼을 눌러 신규 차수 정보를 등록해주세요.</p> - // <Button - // onClick={addRuseFcltObj} - // type="button" - // size={commonButonSize} - // className="mt-2" - // > - // 신규 차수 등록 - // </Button> - // </div> - // ); - // } + useEffect(() => { + console.log("form.formState ", form.formState); + }, [form.formState]); const pbnstBiz = form.watch(`items.${indexMng}.fcBizs`); + return ( <> {pbnstBiz && pbnstBiz?.length > 0 ? ( @@ -302,6 +331,7 @@ export function PbnstBuseForm({ onClick={addRuseFcltObj} type="button" size={commonButonSize} + disabled={readonly} > 추가 </Button> @@ -316,16 +346,18 @@ export function PbnstBuseForm({ sonClass="col-span-4" indexMng={indexMng} indexBiz={indexBiz} + readonly={readonly} /> <MngRegFormDetail key={`buziMoney-${indexMng}-${indexBiz}`} formList={businessMoneyInfo} - h1Title={"사업비"} + h1Title={"사업비(원)"} form={form} moClass="grid grid-cols-12 gap-4" sonClass="col-span-4" indexMng={indexMng} indexBiz={indexBiz} + readonly={readonly} /> <MngRegFormDetail @@ -337,6 +369,7 @@ export function PbnstBuseForm({ sonClass="col-span-4" indexMng={indexMng} indexBiz={indexBiz} + readonly={readonly} /> <MngRegFormDetail key={`concertedWater-${indexMng}-${indexBiz}`} @@ -347,6 +380,7 @@ export function PbnstBuseForm({ sonClass="col-span-4" indexMng={indexMng} indexBiz={indexBiz} + readonly={readonly} /> <MngRegFormDetail key={`wasteFcltinfo-${indexMng}-${indexBiz}`} @@ -357,6 +391,7 @@ export function PbnstBuseForm({ sonClass="col-span-4" indexMng={indexMng} indexBiz={indexBiz} + readonly={readonly} /> <h1 className={formH1Class}>재이용 원수취수 현황</h1> @@ -367,21 +402,26 @@ export function PbnstBuseForm({ onClick={() => setIsFcOrgwts(true)} size={commonButonSize} className="" + disabled={readonly} > 추가등록 </Button> </div> - <RuseMngTable + <RuseMngTableForm key={`fcOrgwts-${indexMng}-${indexBiz}`} // 이게 핵심! tableData={ form.getValues( `items.${indexMng}.fcBizs.${indexBiz}.fcOrgwts` - ) ?? [] + ) || [] + // fcOrgwtWatch||[] } + form={form} headList={pbnst.pbnstOrgwtHeadList} upHeadList={[]} indexMng={indexMng} + formNm={"fcOrgwts"} indexBiz={indexBiz} + readonly={readonly} /> </div> <h1 className={formH1Class}>재이용 공급관로 설치 현황</h1> @@ -394,11 +434,12 @@ export function PbnstBuseForm({ className="w-32" selectClass="w-full" onSelectValue={(val) => setPpiYn(String(val))} + readOnly={readonly} /> <Label className="text-right">합계</Label> <Input type="text" - value={sumCpct} + value={formatCommaNumber(sumCpct)} readOnly={true} className="w-32 border-gray-300 rounded pr-8 " /> @@ -409,6 +450,7 @@ export function PbnstBuseForm({ size={commonButonSize} className="!text-xl" onClick={plusHist} + disabled={readonly} > 추가등록 </Button> @@ -417,6 +459,7 @@ export function PbnstBuseForm({ size={commonButonSize} className="!text-xl !bg-red-500 !text-white ml-1" onClick={removeSelected} + disabled={readonly} > 삭제 </Button> @@ -432,38 +475,43 @@ export function PbnstBuseForm({ form.getValues( `items.${indexMng}.fcBizs.${indexBiz}.pplInstls` ) ?? [] + // pplInstlsWatch||[] } headList={pbnst.pplInstlsHeadLisat} code={ppiSelectList ?? []} - //ref={ref} indexMng={indexMng} indexBiz={indexBiz} form={form} formNm={"pplInstls"} upHeadList={[]} - onTableSet={(val) => { - const pplSum = form.getValues( - `items.${indexMng}.fcBizs.${indexBiz}.pplInstls` - ); - if (!pplSum) return; - let sum = 0; - pplSum.forEach((item) => { - sum += Number(item.cpct) || 0; - }); - setSumCpct(sum); - }} + readonly={readonly} /> </div> - <MngRegFormDetail + {/* <MngRegFormDetail formList={ruseWaterQulityList} - h1Title={"하수처리수 재이용시설 보증수질"} + h1Title={`하수처리수 재이용시설 보증수질 `} + subTitle="※재처리시설이 있는 경우만 작성" form={form} moClass="grid grid-cols-12 gap-4" sonClass="col-span-4" indexMng={indexMng} indexBiz={indexBiz} + /> */} + <div> + + <BuseWaterQulity + h1Title={`하수처리수 재이용시설 보증수질 `} + subTitle="※재처리시설이 있는 경우만 작성" + form={form} + moClass="grid grid-cols-12 gap-4" + sonClass="col-span-4" + indexMng={indexMng} + indexBiz={indexBiz} + readonly={readonly} /> + </div> + <div key={"hasu"}> <h1 className={formH1Class}> {"하수처리수 재이용수 수요처"} @@ -476,8 +524,9 @@ export function PbnstBuseForm({ }} size={commonButonSize} className="mt-6 mr-3" + disabled={readonly} > - 수요항목 추가 + 수요처 추가 </Button> <Button type="button" @@ -489,183 +538,188 @@ export function PbnstBuseForm({ }} size={commonButonSize} className="mt-6" + disabled={readonly} > - 수요처 추가 + 수요처 신규등록 </Button> </div> - {( - form.getValues( - `items.${indexMng}.fcBizs.${indexBiz}.fcDemands` - ) || [] - ).map((field, index) => ( - <div - key={`fcDemands-${indexMng}-${indexBiz}`} // 이게 핵심! - className="grid grid-cols-12 gap-4 mt-3" - > - <div key={index + "keySon1"} className="col-span-4"> - <FormField - control={form.control} - name={`items.${indexMng}.fcBizs.${indexBiz}.fcDemands.${index}.dmdCoNm`} - render={({ field: f }) => ( - <FormUi label="수요처명"> - <SearchInputText - label={"수요처"} - initValue={f.value} - setValue={(val) => { - f.onChange(val); - if (val && val.length > 2) { - getDemandCompList(val, 3); + <div ref={demandsContainerRef}> + {fcDemands.map((field, index) => ( + <div + key={field.id} // useFieldArray의 고유 id를 key로 사용 + className="grid grid-cols-12 gap-4 mt-3 border-b border-gray-400 pb-3" + > + <div key={index + "keySon1"} className="col-span-4"> + <FormField + control={form.control} + name={`items.${indexMng}.fcBizs.${indexBiz}.fcDemands.${index}.dmdCoNm`} + render={({ field: f }) => ( + <FormUi label="수요처명"> + <SearchInputText + label={"수요처"} + initValue={f.value} + setValue={(val) => { + f.onChange(val); + if (val && val.length > 2) { + getDemandCompList(val, 3); + } + }} + setRtnData={(valList: any) => { + if (valList) { + form.setValue( + `items.${indexMng}.fcBizs.${indexBiz}.fcDemands.${index}.daddr`, + valList.daddr + ); + form.setValue( + `items.${indexMng}.fcBizs.${indexBiz}.fcDemands.${index}.useUsgCd`, + valList.useUsgCd + ); + form.setValue( + `items.${indexMng}.fcBizs.${indexBiz}.fcDemands.${index}.splyCpct`, + valList.splyCpct + ); + form.setValue( + `items.${indexMng}.fcBizs.${indexBiz}.fcDemands.${index}.dmdInfoId`, + valList.dmdInfoId?.toString() + ); + } + }} + searchList={demandCompList?.map((item) => { + return { + ...item, + searchNm: item.dmdCoNm, + searchPk: item.dmdInfoId, + }; + })} + readonly={readonly} + ></SearchInputText> + </FormUi> + )} + /> + </div> + <div key={index + "keySon2"} className="col-span-4"> + <FormField + control={form.control} + name={`items.${indexMng}.fcBizs.${indexBiz}.fcDemands.${index}.daddr`} + render={({ field: f }) => ( + <FormUi label="위치(주소)"> + <Input + placeholder="-" + type="text" + readOnly + onChange={(e) => f.onChange(e.target.value)} + value={f.value} + className={selectClass} + /> + </FormUi> + )} + /> + </div> + <div key={index + "keySon3"} className="col-span-4"> + <FormField + control={form.control} + name={`items.${indexMng}.fcBizs.${indexBiz}.fcDemands.${index}.useUsgCd`} + render={({ field: f }) => ( + <FormUi label="사용용도"> + <SelectBox + label={""} + selectClass={""} + className="w-full" + selectArray={code.dmdUszCd} + onSelectValue={f.onChange} + value={f.value ?? code.dmdUszCd[0]?.val} + readOnly={readonly} + /> + </FormUi> + )} + /> + </div> + <div key={index + "keySon4"} className="col-span-3"> + <FormField + control={form.control} + name={`items.${indexMng}.fcBizs.${indexBiz}.fcDemands.${index}.splyBgngDay`} + render={({ field: f }) => ( + <FormUi label="공급시작일"> + <DatePicker + mode={"single"} + field={{ + value: safeDate(f.value) || new Date(), + onChange: (date: Date | undefined) => + f.onChange(date), + }} + selectClass={"rounded-lg !w-full"} + readOnly={readonly} + /> + </FormUi> + )} + /> + </div> + <div key={index + "keySon5"} className="col-span-3"> + <FormField + control={form.control} + name={`items.${indexMng}.fcBizs.${indexBiz}.fcDemands.${index}.splyEndDay`} + render={({ field: f }) => ( + <FormUi label="공급종료일"> + <DatePicker + mode={"single"} + onSelect={(date: Date | undefined) => + f.onChange(date) } - }} - setRtnData={(valList: any) => { - if (valList) { - form.setValue( - `items.${indexMng}.fcBizs.${indexBiz}.fcDemands.${index}.daddr`, - valList.daddr - ); - form.setValue( - `items.${indexMng}.fcBizs.${indexBiz}.fcDemands.${index}.useUsgCd`, - valList.useUsgCd - ); - form.setValue( - `items.${indexMng}.fcBizs.${indexBiz}.fcDemands.${index}.splyCpct`, - valList.splyCpct - ); - form.setValue( - `items.${indexMng}.fcBizs.${indexBiz}.fcDemands.${index}.dmdInfoPk`, - valList.dmdInfoPk?.toString() - ); + field={{ + value: safeDate(f.value) || new Date(), + onChange: (date: Date | undefined) => + f.onChange(date), + }} + selectClass={"rounded-lg !w-full"} + readOnly={readonly} + /> + </FormUi> + )} + /> + </div> + <div key={index + "keySon6"} className="col-span-3"> + <FormField + control={form.control} + name={`items.${indexMng}.fcBizs.${indexBiz}.fcDemands.${index}.splyCpct`} + render={({ field: f }) => ( + <FormUi label="(계약)공급량(m³/일)"> + <Input + placeholder="" + type="text" + onChange={(e) => { + handleCommaNumberChange(e, f.onChange); + }} + value={formatCommaNumber(f.value)} + readOnly={readonly} + /> + </FormUi> + )} + /> + </div> + <div key={index + "keySon7"} className="col-span-3"> + <FormField + control={form.control} + name={`items.${indexMng}.fcBizs.${indexBiz}.fcDemands.${index}.splyFmYn`} + render={({ field: f }) => ( + <FormUi label="공급 유량계 유무"> + <SelectBox + label={""} + selectClass={""} + className="w-full" + selectArray={code.yn} + onSelectValue={(val: string | number) => + f.onChange(val) } - }} - searchList={demandCompList?.map((item) => { - return { - ...item, - searchNm: item.dmdCoNm, - searchPk: item.dmdInfoPk, - }; - })} - ></SearchInputText> - </FormUi> - )} - /> + value={f.value ?? code.yn[0]?.val} + readOnly={readonly} + /> + </FormUi> + )} + /> + </div> </div> - <div key={index + "keySon2"} className="col-span-4"> - <FormField - control={form.control} - name={`items.${indexMng}.fcBizs.${indexBiz}.fcDemands.${index}.daddr`} - render={({ field: f }) => ( - <FormUi label="위치(주소)"> - <Input - placeholder="-" - type="text" - readOnly - onChange={(e) => f.onChange(e.target.value)} - value={f.value} - className={selectClass} - /> - </FormUi> - )} - /> - </div> - <div key={index + "keySon3"} className="col-span-4"> - <FormField - control={form.control} - name={`items.${indexMng}.fcBizs.${indexBiz}.fcDemands.${index}.useUsgCd`} - render={({ field: f }) => ( - <FormUi label="사용용도"> - <SelectBox - label={""} - selectClass={""} - className="w-full" - selectArray={code.dmdUszCd} - onSelectValue={f.onChange} - value={f.value ?? code.dmdUszCd[0]?.val} - /> - </FormUi> - )} - /> - </div> - <div key={index + "keySon4"} className="col-span-3"> - <FormField - control={form.control} - name={`items.${indexMng}.fcBizs.${indexBiz}.fcDemands.${index}.splyBgngDay`} - render={({ field: f }) => ( - <FormUi label="공급시작일"> - <DatePicker - mode={"single"} - field={{ - value: safeDate(f.value) || new Date(), - onChange: (date: Date | undefined) => - f.onChange(date), - }} - selectClass={"rounded-lg !w-full"} - /> - </FormUi> - )} - /> - </div> - <div key={index + "keySon5"} className="col-span-3"> - <FormField - control={form.control} - name={`items.${indexMng}.fcBizs.${indexBiz}.fcDemands.${index}.splyEndDay`} - render={({ field: f }) => ( - <FormUi label="공급종료일"> - <DatePicker - mode={"single"} - onSelect={(date: Date | undefined) => - f.onChange(date) - } - field={{ - value: safeDate(f.value) || new Date(), - onChange: (date: Date | undefined) => - f.onChange(date), - }} - selectClass={"rounded-lg !w-full"} - /> - </FormUi> - )} - /> - </div> - <div key={index + "keySon6"} className="col-span-3"> - <FormField - control={form.control} - name={`items.${indexMng}.fcBizs.${indexBiz}.fcDemands.${index}.splyCpct`} - render={({ field: f }) => ( - <FormUi label="(계약)공급량(m3/일)"> - <Input - placeholder="" - type="number" - value={f.value ?? 0} - onChange={(e) => { - f.onChange(Number(e.target.value)); - }} - /> - </FormUi> - )} - /> - </div> - <div key={index + "keySon7"} className="col-span-3"> - <FormField - control={form.control} - name={`items.${indexMng}.fcBizs.${indexBiz}.fcDemands.${index}.splyFmYn`} - render={({ field: f }) => ( - <FormUi label="공급 유량계 유무"> - <SelectBox - label={""} - selectClass={""} - className="w-full" - selectArray={code.yn} - onSelectValue={(val: string | number) => - f.onChange(val) - } - value={f.value ?? code.yn[0]?.val} - /> - </FormUi> - )} - /> - </div> - </div> - ))} + ))} + </div> </div> </form> </Form> @@ -674,9 +728,14 @@ export function PbnstBuseForm({ <UiModal open={isFcOrgwts} onOpenChange={(open: boolean) => setIsFcOrgwts(open)} - bodyClass={"w-[50vw] max-w-none h-[90vh]"} + bodyClass={"w-[70vw] max-w-none max-h-[90vh] overflow-y-auto"} body={ <FcOrgwtsRegModal + initTableData={ + form.getValues( + `items.${indexMng}.fcBizs.${indexBiz}.fcOrgwts` + ) ?? [] + } closeModal={() => { setIsFcOrgwts(false); }} diff --git a/apps/wr/components/ui/form/fcltReg/pbnstReg/buseReg/buseWaterQulity/buseWaterQulity.tsx b/apps/wr/components/ui/form/fcltReg/pbnstReg/buseReg/buseWaterQulity/buseWaterQulity.tsx new file mode 100644 index 000000000..121957b8e --- /dev/null +++ b/apps/wr/components/ui/form/fcltReg/pbnstReg/buseReg/buseWaterQulity/buseWaterQulity.tsx @@ -0,0 +1,495 @@ +import type { PbnstRegArrrayType } from "@/app/store/ruseFcltMng/fcltReg/pbnstReg/type"; +import { formH1Class } from "@/components/lib/class/comClass"; +import { FormField, FormUi, SelectBox } from "@common/business_components/ui"; +import { Input, Label } from "@common/components/ui"; +import { useCallback, useEffect, useState } from "react"; +import type { UseFormReturn } from "react-hook-form"; + +export function BuseWaterQulity({ + form, + h1Title, + subTitle, + moClass, + sonClass, + indexMng, + indexBiz, + readonly, +}: { + form: UseFormReturn<{ items: PbnstRegArrrayType }>; + h1Title: string; + subTitle: string; + moClass: string; + sonClass: string; + indexMng: number; + indexBiz: number; + readonly?: boolean; +}) { + const [frontPHdata, setFrontPHdata] = useState("0"); + const [backPHdata, setBackPHdata] = useState("0"); + const [chekcPk, setCheakPk] = useState(""); + + // const watchPH = useWatch({ + // name: `items.${indexMng}.fcBizs.${indexBiz}.ph`, + // control: form.control, // useForm에서 받은 control 전달 + // }); + + useEffect(() => { + const watchPH = form.getValues(`items.${indexMng}.fcBizs.${indexBiz}.ph`); + const checkPkAndCycle = + form.getValues(`items.${indexMng}.pbnstRuseFcltId`) + + String(form.getValues(`items.${indexMng}.fcBizs.${indexBiz}.cycl`)); + + console.log("checkPkAndCycle ", checkPkAndCycle); + if (chekcPk !== checkPkAndCycle) { + if (watchPH && watchPH !== undefined) { + const [frontPH, backPH] = watchPH.split("~"); + if (frontPH) { + setFrontPHdata(frontPH); + } + if (backPH) { + setBackPHdata(backPH); + } + } else { + setFrontPHdata("0"); + setBackPHdata("0"); + } + setCheakPk(checkPkAndCycle); + } + }, [form.getValues(`items.${indexMng}.fcBizs.${indexBiz}.ph`), chekcPk]); + + const isSetFrontPh = useCallback( + (frontPHdata: string) => { + if (frontPHdata && frontPHdata.length > 0) { + const PHdata = form.getValues( + `items.${indexMng}.fcBizs.${indexBiz}.ph` + ); + if (PHdata) { + const backPH = PHdata.split("~")[1]; + const MatchData = frontPHdata + "~" + backPH; + form.setValue(`items.${indexMng}.fcBizs.${indexBiz}.ph`, MatchData); + setFrontPHdata(frontPHdata); + } else { + const MatchData = frontPHdata + "~" + "0"; + form.setValue(`items.${indexMng}.fcBizs.${indexBiz}.ph`, MatchData); + setFrontPHdata(frontPHdata); + } + } + }, + [frontPHdata, form, indexMng, indexBiz] + ); + + const isSetBackPh = useCallback( + (backPHdata: string) => { + if (backPHdata && backPHdata.length > 0) { + const PHdata = form.getValues( + `items.${indexMng}.fcBizs.${indexBiz}.ph` + ); + + if (PHdata) { + const frontPH = PHdata.split("~")[0]; + const MatchData = frontPH + "~" + backPHdata; + + console.log(" backPHdata ", MatchData); + form.setValue(`items.${indexMng}.fcBizs.${indexBiz}.ph`, MatchData); + setBackPHdata(backPHdata); + } else { + const MatchData = "0" + "~" + backPHdata; + + form.setValue(`items.${indexMng}.fcBizs.${indexBiz}.ph`, MatchData); + setBackPHdata(backPHdata); + } + } + }, + [backPHdata, form, indexMng, indexBiz] + ); + + return ( + <> + {h1Title ? <h1 className={formH1Class}>{h1Title}</h1> : <></>} + {subTitle ? <h3 className={"items-center"}>{subTitle}</h3> : <></>} + + <div className={moClass}> + <div className={sonClass}> + <FormField + control={form.control} + name={`items.${indexMng}.fcBizs.${indexBiz}.bod`} + key={`items.${indexMng}.fcBizs.${indexBiz}.bod-key`} + render={({ field }) => ( + <FormUi label="BOD(mg/L)"> + <div className="flex items-center gap-2"> + <div className="relative flex-1 min-w-0"> + <Input + type="text" + className="w-full pr-16 border py-2 text-right rounded-md" + placeholder="" + onChange={(e) => { + const inputValue = e.target.value.replace(/[^0-9.]/g, ""); // 숫자만 저장 + field.onChange(inputValue); + }} + value={ + field.value !== undefined && field.value !== "" + ? `${field.value}` + : "" + } + readOnly={readonly} + maxLength={6} + /> + <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + 이하 + </span> + </div> + </div> + </FormUi> + )} + /> + </div> + <div className={sonClass}> + { + //TODO 불검춣확인필요 + } + <FormField + control={form.control} + name={`items.${indexMng}.fcBizs.${indexBiz}.grssClfg`} + key={`items.${indexMng}.fcBizs.${indexBiz}.grssClfg-key`} + render={({ field }) => ( + <FormUi label="총대장균군수(개/mg)"> + <div className="flex items-center gap-2"> + <div className="relative flex-1 min-w-0"> + <Input + type="text" + className="w-full pr-16 border py-2 text-right rounded-md" + placeholder="" + onChange={(e) => { + const inputValue = e.target.value.replace(/[^0-9.]/g, ""); // 숫자만 저장 + field.onChange(inputValue); + }} + value={ + field.value !== undefined && field.value !== "" + ? `${field.value}` + : "" + } + readOnly={readonly} + maxLength={6} + /> + {/* TODO: 총대장균군수 확인 필요*/} + <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + 이하 + </span> + </div> + </div> + </FormUi> + )} + /> + </div> + <div className={sonClass}> + <FormField + control={form.control} + name={`items.${indexMng}.fcBizs.${indexBiz}.combResdch`} + key={`items.${indexMng}.fcBizs.${indexBiz}.combResdch-key`} + render={({ field }) => ( + <FormUi label="결합잔류염소(mh/L)"> + <div className="flex items-center gap-2"> + <div className="relative flex-1 min-w-0"> + <Input + type="text" + className="w-full pr-16 border py-2 text-right rounded-md" + placeholder="" + onChange={(e) => { + const inputValue = e.target.value.replace(/[^0-9.]/g, ""); // 숫자만 저장 + field.onChange(inputValue); + }} + value={ + field.value !== undefined && field.value !== "" + ? `${field.value}` + : "" + } + readOnly={readonly} + maxLength={6} + /> + <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + 이상 + </span> + </div> + </div> + </FormUi> + )} + /> + </div> + <div className={sonClass}> + <FormField + control={form.control} + name={`items.${indexMng}.fcBizs.${indexBiz}.trbt`} + key={`items.${indexMng}.fcBizs.${indexBiz}.trbt-key`} + render={({ field }) => ( + <FormUi label="탁도(NTU)"> + <div className="flex items-center gap-2"> + <div className="relative flex-1 min-w-0"> + <Input + type="text" + className="w-full pr-16 border py-2 text-right rounded-md" + placeholder="" + onChange={(e) => { + const inputValue = e.target.value.replace(/[^0-9.]/g, ""); // 숫자만 저장 + field.onChange(inputValue); + }} + value={ + field.value !== undefined && field.value !== "" + ? `${field.value}` + : "" + } + readOnly={readonly} + maxLength={6} + /> + <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + 이상 + </span> + </div> + </div> + </FormUi> + )} + /> + </div> + <div className={sonClass}> + { + //TODO : 확인필요 + } + <FormField + control={form.control} + name={`items.${indexMng}.fcBizs.${indexBiz}.smll`} + key={`items.${indexMng}.fcBizs.${indexBiz}.smll-key`} + render={({ field }) => ( + <FormUi label="냄새"> + {/* <Input + className="!text-2xl" + placeholder="" + type="text" + onChange={(e) => { + field.onChange(String(e.target.value)); + }} + value={field.value ?? ""} + readOnly={readonly === "sttn" ? true : false} + maxLength={txtSize} + /> */} + <SelectBox + className={"!text-2xl"} + label={""} + selectClass={undefined} + selectArray={[ + { val: "불쾌하지않을것", text: "불쾌하지않을것" }, + { val: "없음", text: "없음" }, + ]} + readOnly={readonly} + onSelectValue={(val) => { + field.onChange(val); + }} + value={field.value || undefined} + ></SelectBox> + </FormUi> + )} + /> + </div> + <div className={sonClass}> + <FormField + control={form.control} + name={`items.${indexMng}.fcBizs.${indexBiz}.crmty`} + key={`items.${indexMng}.fcBizs.${indexBiz}.crmty-key`} + render={({ field }) => ( + <FormUi label="색도(도)"> + <div className="flex items-center gap-2"> + <div className="relative flex-1 min-w-0"> + <Input + type="text" + className="w-full pr-16 border py-2 text-right rounded-md" + placeholder="" + onChange={(e) => { + const inputValue = e.target.value.replace(/[^0-9.]/g, ""); // 숫자만 저장 + field.onChange(inputValue); + }} + value={ + field.value !== undefined && field.value !== "" + ? `${field.value}` + : "" + } + readOnly={readonly} + maxLength={6} + /> + <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + 이하 + </span> + </div> + </div> + </FormUi> + )} + /> + </div> + <div className={sonClass}> + <FormField + control={form.control} + name={`items.${indexMng}.fcBizs.${indexBiz}.tn`} + key={`items.${indexMng}.fcBizs.${indexBiz}.tn-key`} + render={({ field }) => ( + <FormUi label="총질소(T-N)(mg/L)"> + <div className="flex items-center gap-2"> + <div className="relative flex-1 min-w-0"> + <Input + type="text" + className="w-full pr-16 border py-2 text-right rounded-md" + placeholder="" + onChange={(e) => { + const inputValue = e.target.value.replace(/[^0-9.]/g, ""); // 숫자만 저장 + field.onChange(inputValue); + }} + value={ + field.value !== undefined && field.value !== "" + ? `${field.value}` + : "" + } + readOnly={readonly} + maxLength={6} + /> + <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + 이하 + </span> + </div> + </div> + </FormUi> + )} + /> + </div> + + <div className={sonClass}> + <FormField + control={form.control} + name={`items.${indexMng}.fcBizs.${indexBiz}.tph`} + key={`items.${indexMng}.fcBizs.${indexBiz}.tph-key`} + render={({ field }) => ( + <FormUi label="총인(T-P)(mg/L)"> + <div className="flex items-center gap-2"> + <div className="relative flex-1 min-w-0"> + <Input + type="text" + className="w-full pr-16 border py-2 text-right rounded-md" + placeholder="" + onChange={(e) => { + const inputValue = e.target.value.replace(/[^0-9.]/g, ""); // 숫자만 저장 + field.onChange(inputValue); + }} + value={ + field.value !== undefined && field.value !== "" + ? `${field.value}` + : "" + } + readOnly={readonly} + maxLength={6} + /> + <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + 이하 + </span> + </div> + </div> + </FormUi> + )} + /> + </div> + <div className={sonClass}> + <FormField + control={form.control} + name={`items.${indexMng}.fcBizs.${indexBiz}.cdtnv`} + key={`items.${indexMng}.fcBizs.${indexBiz}.cdtnv-key`} + render={({ field }) => ( + <FormUi label="전기전도도(μs/cm)"> + <div className="flex items-center gap-2"> + <div className="relative flex-1 min-w-0"> + <Input + type="text" + className="w-full pr-16 border py-2 text-right rounded-md" + placeholder="" + onChange={(e) => { + const inputValue = e.target.value.replace(/[^0-9.]/g, ""); // 숫자만 저장 + field.onChange(inputValue); + }} + value={ + field.value !== undefined && field.value !== "" + ? `${field.value}` + : "" + } + readOnly={readonly} + maxLength={7} + /> + <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + 이하 + </span> + </div> + </div> + </FormUi> + )} + /> + </div> + + <div className={sonClass}> + <FormField + control={form.control} + name={`items.${indexMng}.fcBizs.${indexBiz}.clIonQnt`} + key={`items.${indexMng}.fcBizs.${indexBiz}.clIonQnt-key`} + render={({ field }) => ( + <FormUi label="염화물(mg/L)"> + <div className="flex items-center gap-2"> + <div className="relative flex-1 min-w-0"> + <Input + type="text" + className="w-full pr-16 border py-2 text-right rounded-md" + placeholder="" + onChange={(e) => { + const inputValue = e.target.value.replace(/[^0-9.]/g, ""); // 숫자만 저장 + field.onChange(inputValue); + }} + value={ + field.value !== undefined && field.value !== "" + ? `${field.value}` + : "" + } + readOnly={readonly} + maxLength={7} + /> + <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + 이하 + </span> + </div> + </div> + </FormUi> + )} + /> + </div> + <div className="col-span-8"> + <Label className="text-xl">수소이온농도(PH)</Label> + <div className="flex flex-row items-center gap-2"> + <Input + className="!text-xl w-28" + placeholder="" + type="text" + onChange={(e) => + //setFrontPHdata(e.target.value) + isSetFrontPh(e.target.value) + } + value={frontPHdata ?? ""} + readOnly={readonly} + maxLength={3} + /> + <span className="text-xl">~</span> + <Input + className="!text-xl w-28" + placeholder="" + type="text" + onChange={(e) => { + isSetBackPh(e.target.value); + }} + value={backPHdata ?? ""} + readOnly={readonly} + maxLength={3} + /> + </div> + </div> + </div> + </> + ); +} diff --git a/apps/wr/components/ui/form/fcltReg/pbnstReg/mngReg/mngRegForm.tsx b/apps/wr/components/ui/form/fcltReg/pbnstReg/mngReg/mngRegForm.tsx index d07d24a23..68d7634a0 100644 --- a/apps/wr/components/ui/form/fcltReg/pbnstReg/mngReg/mngRegForm.tsx +++ b/apps/wr/components/ui/form/fcltReg/pbnstReg/mngReg/mngRegForm.tsx @@ -9,9 +9,8 @@ import { } from "@common/business_components/ui"; import { Form, FormField } from "@common/business_components/ui/form"; import { Button, Input } from "@common/components"; -import { useEffect, useState } from "react"; -import { useFieldArray, useWatch, type UseFormReturn } from "react-hook-form"; -import { z } from "zod"; +import { useEffect, useRef, useState } from "react"; +import { useFieldArray, type UseFormReturn } from "react-hook-form"; import { commonButonSize, formH1Class, @@ -19,8 +18,6 @@ import { } from "@/components/lib/class/comClass"; import { TusClientUpload } from "@/components/lib/useTus_wr/useTusWr"; import { - pbnstRegZod, - type customPbnst, type fcFileType, type PbnstRegArrrayType, type selectLabel, @@ -29,6 +26,11 @@ import { TEST_UUID } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; import { Label } from "@common/business_components/ui/form/lib/label"; import React from "react"; import RuseMngTableForm from "@/components/ui/table/pbnstReg/ruseMngTableForm/ruseMngTableForm"; +import { + handleCommaNumberChange, + formatCommaNumber, +} from "@/components/lib/func/commom"; +import { numSize, txtSize } from "@/app/store/common/code/basicCode"; export type FilesType = { atflCd: string; file: File; @@ -39,22 +41,18 @@ export function PbnstMngForm({ formType, indexMng, setIndexMng, + readonly, }: { form: UseFormReturn<{ items: PbnstRegArrrayType }>; formType: string; - firstFcltData?: customPbnst; indexMng: number; setIndexMng: (index: number) => void; + readonly: boolean; }) { - const { - code, - pbnst, - pbnstInsert, - fileDelete, - fileDownLoad, - } = useWrStore((state) => state); - - const { pbnstRes } = pbnst; + const { code, pbnst, fileDelete, fileDownLoad } = useWrStore( + (state) => state + ); + const fileInitRef = useRef<{ index: number; key: string } | null>(null); const [regFileList, setRegFileList] = useState<fcFileType[] | null>(null); const [planFiles, setPlanFiles] = useState<FilesType[] | null>(null); const [relationfiles, setRelationFiles] = useState<FilesType[] | null>(null); @@ -62,25 +60,26 @@ export function PbnstMngForm({ const [mngMtdCode, setMngMtdCode] = useState<{ name: string; val: SelectDataType[] }[]>(); + const { fields: fcOpersField, - append, + prepend, remove, } = useFieldArray({ control: form.control, name: `items.${indexMng}.fcOpers`, }); - const watchedItem = useWatch({ + const { fields: mngRegForm } = useFieldArray({ control: form.control, - name: `items.${indexMng}`, + name: `items`, }); const plusHist = () => { - append({ + prepend({ choose: false, - pbnstRuseFcltFk: - form.getValues(`items.${indexMng}.pbnstRuseFcltPk`) ?? "", + pbnstRuseFcltId: + form.getValues(`items.${indexMng}.pbnstRuseFcltId`) ?? "", rgtr: "", mdfr: "", operMthd: "", @@ -88,6 +87,7 @@ export function PbnstMngForm({ bgngDt: new Date(), endDt: new Date(), }); + //trigger(`items.${indexMng}.fcOpers`); }; const removeSelected = () => { @@ -121,6 +121,7 @@ export function PbnstMngForm({ atflCd: tusFile.atflCd.toString(), file: item, }); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions type === "relation" ? setRelationFiles(setReFiles) : setPlanFiles(setReFiles); @@ -147,6 +148,7 @@ export function PbnstMngForm({ } }); } + // eslint-disable-next-line @typescript-eslint/no-unused-expressions type === "relation" ? setRelationFiles(setReFiles) : setPlanFiles(setReFiles); @@ -171,70 +173,69 @@ export function PbnstMngForm({ }; useEffect(() => { - if (indexMng >= 0) { - const defaultFiles = watchedItem?.fcFiles; - if (defaultFiles && defaultFiles.length > 0) { - const relationFileList = defaultFiles.filter( - (item) => item.fileKnd === "계통도" - ); + const currentItem = form.getValues(`items.${indexMng}`); + const currentKey = JSON.stringify(currentItem); + const isNewItem = + !fileInitRef.current || + fileInitRef.current.index !== indexMng || + fileInitRef.current.key !== currentKey; - const planFileList = defaultFiles.filter( - (item) => item.fileKnd === "평면도" - ); - if (relationFileList.length > 0) - setRelationFiles( - relationFileList.map((item) => ({ - atflCd: item.fileTkn, - file: new File([] as BlobPart[], item.filePath), - })) - ); - else { - setRelationFiles(null); - } + if (!isNewItem) return; // - if (planFileList.length > 0) - setPlanFiles( - planFileList.map((item) => ({ - atflCd: item.fileTkn, - file: new File([] as BlobPart[], item.filePath), - })) - ); - else { - setPlanFiles(null); - } + const defaultFiles = currentItem?.fcFiles || []; + + const relationFileList = defaultFiles.filter( + (item) => item.fileKnd === "계통도" + ); + const planFileList = defaultFiles.filter( + (item) => item.fileKnd === "평면도" + ); + + setRelationFiles( + relationFileList.length > 0 + ? relationFileList.map((item) => ({ + atflCd: item.fileTkn, + file: new File([], item.filePath), + })) + : null + ); + + setPlanFiles( + planFileList.length > 0 + ? planFileList.map((item) => ({ + atflCd: item.fileTkn, + file: new File([], item.filePath), + })) + : null + ); + // ✅ 다음 루프에선 다시 안 하도록 저장 + fileInitRef.current = { + index: indexMng, + key: currentKey, + }; + }, [indexMng, form.getValues(`items.${indexMng}`)]); + + useEffect( + () => { + //폼이 들어오면 폼에 대한 이름 셀렉트박스를만들기 위해 + if (mngRegForm && mngRegForm.length >= 0) { + const fcltNmList = mngRegForm?.map((item, index) => ({ + text: item.ruseUtztnFcltNm, + val: String(index), + })); + if (fcltNmList) setFcltNm(fcltNmList); } else { - setRelationFiles(null); - setPlanFiles(null); + setFcltNm(null); } - } - }, [watchedItem, indexMng]); + }, //pbnst.pbnstRes + [mngRegForm] + ); useEffect(() => { - // pbnstMngData(pbnst.pbnstMngRegData); - }, [pbnst.pbnstMngRegData]); - - useEffect(() => { - if (pbnst.pbnstRes && pbnst.pbnstRes?.length >= 0) { - - const fcltNmList = pbnst.pbnstRes?.map((item, index) => ({ - text: item.ruseUtztnFcltNm, - val: String(index), - })); - - - console.log(' fcltNmList ', fcltNmList) - if (fcltNmList) setFcltNm(fcltNmList); - }else{ - setFcltNm(null); + if (regFileList && regFileList?.length > 0) { + form.setValue(`items.${indexMng}.fcFiles`, regFileList); } - }, [pbnst.pbnstRes]); - - // 2. Define a submit handler. - function onSubmit(values: z.infer<typeof pbnstRegZod>) { - values.fcOpers = []; - values.fcFiles = regFileList || []; - pbnstInsert(values); - } + }, [regFileList, indexMng]); useEffect(() => { if (relationfiles) { @@ -253,16 +254,6 @@ export function PbnstMngForm({ } }, [code]); - - useEffect(() =>{ - // console.log(' fcltNm ', watchedItem?.ruseUtztnFcltNm) - },[watchedItem]) - - - useEffect(() => { - console.log(" form indexMng ", indexMng); - }, [indexMng]); - return ( <div className=" !mx-[1.6rem] !p-0r"> <div> @@ -285,7 +276,7 @@ export function PbnstMngForm({ setIndexMng(Number(val)); }} value={String(indexMng)} - className={""} + className={"ml-4 mt-2"} selectClass={undefined} /> </FormUi> @@ -297,16 +288,15 @@ export function PbnstMngForm({ render={({ field }) => ( <FormUi label=""> <Input - - - className="!text-2xl" + className="!text-2xl ml-4 mt-1" placeholder="" type="text" onChange={(e) => { field.onChange(String(e.target.value)); }} value={field.value ?? ""} - readOnly={formType === "sttn" ? true : false} + readOnly={readonly} + maxLength={txtSize} /> </FormUi> )} @@ -316,49 +306,42 @@ export function PbnstMngForm({ </div> <div> + {/* <div className="h-[200px] overflow-y-auto"> */} + <div className="flex justify-between items-center mx-3 mb-3"> + <span className={formTableName}>재이용시설 운영정보</span> + {formType === "sttn" ? ( + <></> + ) : ( + <div> + <Button + type="button" + size={commonButonSize} + className="!text-xl" + onClick={plusHist} + disabled={readonly} + > + 추가등록 + </Button> + <Button + type="button" + size={commonButonSize} + className="!text-xl !bg-red-500 !text-white ml-1" + onClick={removeSelected} + disabled={readonly} + > + 삭제 + </Button> + </div> + )} + </div> <div className="h-[200px] overflow-y-auto"> - <div className="flex justify-between items-center mx-3 mb-3"> - <span className={formTableName}>재이용시설 운영정보</span> - {formType === "sttn" ? ( - <></> - ) : ( - <div> - <Button - type="button" - size={commonButonSize} - className="!text-xl" - onClick={plusHist} - > - 추가등록 - </Button> - <Button - type="button" - size={commonButonSize} - className="!text-xl !bg-red-500 !text-white ml-1" - onClick={removeSelected} - > - 삭제 - </Button> - </div> - )} - </div> <RuseMngTableForm + key={`ruse-table-${indexMng}-${fcOpersField.length}`} // 🔥 length까지 넣어야 동기화됨 tableData={form.getValues(`items.${indexMng}.fcOpers`) || []} - headList={ - formType === "sttn" - ? pbnst.pbnstMngUpHeadList.map((item) => { - return { - ...item, - readonly: true, - }; - }) - : pbnst.pbnstMngUpHeadList - } + headList={pbnst.pbnstMngUpHeadList} upHeadList={pbnst.pbnstMngNm} code={mngMtdCode || []} - onTableSet={(val) => { - //setFcOpresData(val); - }} + readonly={readonly} indexMng={indexMng} form={form} formNm={"fcOpers"} @@ -379,12 +362,13 @@ export function PbnstMngForm({ <Input key={`items.${indexMng}.fcltCpct`} placeholder="" - type="number" + maxLength={numSize} //입력제한 + type="text" onChange={(e) => { - field.onChange(Number(e.target.value)); + handleCommaNumberChange(e, field.onChange); }} - value={field.value ?? 0} - readOnly={formType === "sttn" ? true : false} + value={formatCommaNumber(field.value)} + readOnly={readonly} /> </FormUi> )} @@ -401,12 +385,13 @@ export function PbnstMngForm({ <Input key={`items.${indexMng}.ruseCpct`} placeholder="" - type="number" + maxLength={numSize} //입력제한 + type="text" onChange={(e) => { - field.onChange(Number(e.target.value)); + handleCommaNumberChange(e, field.onChange); }} - value={field.value ?? 0} - readOnly={formType === "sttn" ? true : false} + value={formatCommaNumber(field.value)} + readOnly={readonly} /> </FormUi> )} @@ -422,12 +407,13 @@ export function PbnstMngForm({ <Input key={`items.${indexMng}.splyExtraCpct`} placeholder="" - type="number" + maxLength={numSize} //입력제한 + type="text" onChange={(e) => { - field.onChange(Number(e.target.value)); + handleCommaNumberChange(e, field.onChange); }} - value={field.value ?? 0} - readOnly={formType === "sttn" ? true : false} + value={formatCommaNumber(field.value)} + readOnly={readonly} /> </FormUi> )} @@ -439,7 +425,7 @@ export function PbnstMngForm({ </div> <div className="grid grid-cols-12 gap-4 my-4"> <div className="col-span-6"> - {formType === "sttn" ? ( + {readonly ? ( relationfiles && relationfiles?.length > 0 ? ( <> <Label className="text-3xl">계통도</Label> @@ -476,7 +462,7 @@ export function PbnstMngForm({ )} </div> <div className="col-span-6"> - {formType === "sttn" ? ( + {readonly ? ( planFiles && planFiles?.length > 0 ? ( <> <Label className="text-3xl">평면도</Label> diff --git a/apps/wr/components/ui/form/fcltReg/pbnstReg/mngReg/mngRegFormDetail/mngRegFormDetail.tsx b/apps/wr/components/ui/form/fcltReg/pbnstReg/mngReg/mngRegFormDetail/mngRegFormDetail.tsx index 971888e10..2885c07a5 100644 --- a/apps/wr/components/ui/form/fcltReg/pbnstReg/mngReg/mngRegFormDetail/mngRegFormDetail.tsx +++ b/apps/wr/components/ui/form/fcltReg/pbnstReg/mngReg/mngRegFormDetail/mngRegFormDetail.tsx @@ -1,48 +1,52 @@ "use client"; import { useWrStore } from "@/app/store"; +import { numSize, txtSize } from "@/app/store/common/code/basicCode"; import type { formDetailType } from "@/app/store/ruseFcltMng/fcltReg/pbnstReg/headList"; import { formH1Class } from "@/components/lib/class/comClass"; -import { safeDate } from "@/components/lib/func/commom"; +import { formatCommaNumber, handleCommaNumberChange, safeDate } from "@/components/lib/func/commom"; import { DatePicker, FormField, FormUi, - SearchInputSelect, SelectBox, - type SelectDataType, } from "@common/business_components/ui"; import { Input } from "@common/components/ui"; -import { useState } from "react"; export default function MngRegFormDetail({ formList, h1Title, + subTitle, form, moClass, sonClass, indexBiz, indexMng, + readonly }: { formList: formDetailType[]; h1Title?: string; + subTitle?: string form: any; moClass?: string; indexBiz?: number; indexMng?: number; sonClass?: string; + readonly?:boolean }) { const { code } = useWrStore((state) => state); return ( <> {h1Title ? <h1 className={formH1Class}>{h1Title}</h1> : <></>} + {subTitle ? <h3 className={'items-center'}>{subTitle}</h3> : <></>} + <div className={moClass}> {formList?.map((formHead, index) => { const fieldId = typeof indexMng === "number" && - indexMng >= 0 && - typeof indexBiz === "number" && - indexBiz >= 0 + indexMng >= 0 && + typeof indexBiz === "number" && + indexBiz >= 0 ? `items.${indexMng}.fcBizs.${indexBiz}.${formHead.id}` : typeof indexMng === "number" && indexMng >= 0 ? `items.${indexMng}.${formHead.id}` @@ -55,9 +59,7 @@ export default function MngRegFormDetail({ name={fieldId} render={({ field }) => ( <FormUi label={formHead.lable}> - {formHead.type === "select" ? ( - <Input placeholder="" type="text" {...field} /> - ) : formHead.type === "date" ? ( + {formHead.type === "date" ? ( <DatePicker mode={"single"} className={"rounded-lg !w-full"} @@ -67,35 +69,64 @@ export default function MngRegFormDetail({ field.onChange(date), }} selectClass={"w-full"} + readOnly={readonly + //|| formHead.option === "disabled" ? true : false + + } + /> ) : formHead.type === "number" ? ( <Input placeholder="" - type="number" - value={field.value ?? 0} + type="text" onChange={(e) => { - field.onChange(Number(e.target.value)); + handleCommaNumberChange( + e, + field.onChange + ); }} - readOnly={formHead.option === "readOnly"?true:false} - /> - ) : formHead.type === "selectYn" ? ( - <SelectBox - label={""} - selectClass={""} - className="w-full" - selectArray={code.yn} - onSelectValue={(val: string | number) => - field.onChange(val) + value={formatCommaNumber(field.value)} + readOnly={readonly + // || formHead.option === "readOnly" ? true : false } - value={field.value ?? "Y"} + disabled={formHead.option === "disabled" ? true : false} + maxLength={numSize} /> + ) : formHead.type === "select" ? ( + (() => { + const potentialOptions = formHead.selectData + ? code[formHead.selectData as keyof typeof code] + : []; + + return ( + <SelectBox + label={""} + selectClass={""} + className="w-full" + selectArray={ + Array.isArray(potentialOptions) + ? potentialOptions + : [] + } + onSelectValue={field.onChange} + value={field.value?.trim() ?? (Array.isArray(potentialOptions) && potentialOptions[0]?.val)} + readOnly={readonly + // || formHead.option === "readOnly" + } + /> + ); + })() ) : ( <Input placeholder="" type="text" value={field.value ?? ""} onChange={field.onChange} - readOnly={formHead.option === "readOnly"?true:false} + readOnly={readonly + // || formHead.option === "readOnly" ? true : false + } + disabled={formHead.option === "disabled" ? true : false} + maxLength={txtSize} /> )} </FormUi> diff --git a/apps/wr/components/ui/form/fcltReg/rainReg/rainRegForm.tsx b/apps/wr/components/ui/form/fcltReg/rainReg/rainRegForm.tsx index 9ec4fc5c0..2da752028 100644 --- a/apps/wr/components/ui/form/fcltReg/rainReg/rainRegForm.tsx +++ b/apps/wr/components/ui/form/fcltReg/rainReg/rainRegForm.tsx @@ -4,40 +4,47 @@ import { useWrStore } from "@/app/store"; import { rainZod, selectClass, - type customRainBdst + type customRainBdst, } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; import { formH1Class, formH2Class } from "@/components/lib/class/comClass"; import { + confirmToast, formatCommaNumber, - handleCommaNumberChange, + handleMinusNumberChange, } from "@/components/lib/func/commom"; import { DatePicker, FormUi } from "@common/business_components"; import { Form, FormField, SelectBox } from "@common/business_components/ui"; -import { Button, Input } from "@common/components/ui"; +import { Button, Input, ScrollArea } from "@common/components/ui"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; import BdstReg from "../bdstReg/bdstReg"; +import toast from "react-hot-toast"; +import BdstAdd from "../bdstReg/bdstAdd"; +import { useTranslation } from "react-i18next"; +import { numSize, txtSize } from "@/app/store/common/code/basicCode"; export default function RainRegForm({ rainForm, isModal, - readonly = false + readonly = false, + setIsModal, }: { rainForm: customRainBdst; isModal: boolean; + setIsModal?: (modal: boolean) => void; readonly?: boolean; }) { - const { code, rainBdstReg, rainBdstUpdate, rain } = useWrStore( + const { code, rainBdstReg, rainBdstUpdate } = useWrStore( (state) => state ); const { rainFcltCd } = code; - const defaultValues: z.infer<typeof rainZod> = rainZod.parse({}); const form = useForm<z.infer<typeof rainZod>>({ resolver: zodResolver(rainZod), - defaultValues: defaultValues, + // defaultValues: defaultValues, }); + const { t } = useTranslation('base'); const [grntchBdstCertYn, instlCstSprtYn, filtrTrtmfctYn] = form.watch([ "reuseBuilding.0.grntchBdstCertYn", @@ -45,25 +52,45 @@ export default function RainRegForm({ "reuseBuilding.0.rainyObj.filtrTrtmfctYn", ]); + const [divStyle, setDivStyle] = useState("h-[53vh]"); + useEffect(() => { + if (isModal === true) { + setDivStyle("h-[53vh] max-h-[37vh]") + } else if (readonly === true) { + setDivStyle("h-[62vh]") + } else { + setDivStyle("h-[57vh]") + } + }, [isModal, readonly]); + const cleanRainData = (raw: any): z.infer<typeof rainZod> => { + const buildings = + raw?.reuseBuilding && raw.reuseBuilding.length > 0 + ? raw.reuseBuilding + : [{}]; return { ...raw, - frstArchPrmsnDt: new Date(raw?.frstArchPrmsnDt || null), + frstArchPrmsnDt: raw?.frstArchPrmsnDt + ? new Date(raw.frstArchPrmsnDt) + : new Date(), // raw?.frstArchPrmsnDt가 유효하지 않으면 현재 날짜로 초기화 archChgPrmsnDt: raw?.archChgPrmsnDt ? new Date(raw?.archChgPrmsnDt) : null, - reuseBuilding: (raw?.reuseBuilding || []).map((item: any) => ({ + reuseBuilding: buildings.map((item: any) => ({ ...item, - instlCmptnDt: new Date(item?.instlCmptnDt), + instlCmptnDt: item?.instlCmptnDt + ? new Date(item.instlCmptnDt) + : new Date(), // item?.instlCmptnDt가 유효하지 않으면 현재 날짜로 초기화 operMngCd: item?.operMngCd ?? "", // null → "" grntchBdstCertYr: typeof item?.grntchBdstCertYr === "string" ? item?.grntchBdstCertYr : "", // null → "" - // Ensure enum fields are null if empty string to trigger Zod default + instlCstSprtYn: item?.instlCstSprtYn ?? "Y", // null → "Y" + grntchBdstCertYn: item?.grntchBdstCertYn ?? "Y", rainyObj: { ...item?.rainyObj, - filtrTrtmfctYn: item?.rainyObj?.filtrTrtmfctYn === "" ? null : item?.rainyObj?.filtrTrtmfctYn, + filtrTrtmfctYn: item?.rainyObj?.filtrTrtmfctYn ?? "Y", }, })), }; @@ -80,14 +107,21 @@ export default function RainRegForm({ form.setValue("reuseBuilding.0.rainyObj.filtrTrtmfctCpct", 0); form.setValue("reuseBuilding.0.rainyObj.filtrTrtmfctPrs", ""); } + + if (grntchBdstCertYn === "D") { + form.setValue("reuseBuilding.0.grntchBdstCertYr", "0000"); + form.setValue("reuseBuilding.0.grntchBdstCertNo", "확인불가"); + form.setValue("reuseBuilding.0.grntchBdstCertGrd", "확인불가"); + } else if (instlCstSprtYn === "D") { + form.setValue("reuseBuilding.0.instlCstSprtAmt", -1); + } else if (filtrTrtmfctYn === "D") { + form.setValue("reuseBuilding.0.rainyObj.filtrTrtmfctCpct", -1); + form.setValue("reuseBuilding.0.rainyObj.filtrTrtmfctPrs", "확인불가"); + } }, [grntchBdstCertYn, instlCstSprtYn, filtrTrtmfctYn]); - // form 값이 변경될 때마다 로그 출력 - useEffect(() => { - const subscription = form.watch((value) => { - }); - return () => subscription.unsubscribe(); - }, [form]); + const [regCtpvCd, regSggcd] = form.watch(["ctpvCd", "sggCd"]); + useEffect(() => { console.log("form errors:", form.formState.errors); @@ -95,9 +129,23 @@ export default function RainRegForm({ const onSubmit = (values: z.infer<typeof rainZod>) => { if (isModal) { - rainBdstReg(values, "41"); + const data = regCtpvCd ? regCtpvCd : regSggcd ? regSggcd : "noCd"; + + if (data === "noCd") { + toast.error("시도/시군구 코드를 선택해야합니다."); + return; + } + + const handleConfirm = async () => { + rainBdstReg(values, data); + if (setIsModal) setIsModal(false); + }; + confirmToast(t('isInsertMsg'), handleConfirm); } else { - rainBdstUpdate(values, "41"); + const handleConfirm = async () => { + rainBdstUpdate(values, "41"); + }; + confirmToast(t('isUpdateMsg'), handleConfirm); } }; @@ -110,320 +158,460 @@ export default function RainRegForm({ <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)}> <div> + <h1 className="text-2xl font-bold text-gray-900 mt-3 ml-1"> + {readonly && `(시설명 : ${rainForm?.bdstNm})`} + </h1> <div className="grid grid-cols-12 flex items-center card-header"> <h1 className="text-2xl font-medium text-label col-span-10"> - {isModal ? "건축물 운영정보" : "빗물이용시설 정보"} + {isModal ? "건축물 정보" : "빗물이용시설 정보"} </h1> {!readonly && ( - <Button type="submit" className="col-start-12 col-span-1" size={"sm"}> + <Button + type="submit" + className="col-start-12 col-span-1" + size={"sm"} + > 저장 </Button> )} </div> </div> - {isModal && rainForm != undefined ? <BdstReg form={form} /> : <></>} - {isModal ? ( - <h1 className="pl-3 text-xl col-span-10 mt-4"> - 빗물이용시설 정보{" "} - </h1> - ) : ( - <></> - )} - <h1 className={formH1Class}> - 기본정보 - </h1> + <ScrollArea className={divStyle}> + {isModal && rainForm != undefined ? <BdstReg form={form} /> : <></>} + {isModal ? ( + <h1 className="pl-3 text-xl col-span-10 mt-4"> + 빗물이용시설 정보{" "} + </h1> + ) : ( + <></> + )} + <h1 className={formH1Class}>기본정보</h1> + <div className="grid grid-cols-6 gap-2 mt-4"> + <div className="col-span-3"> + <FormField + control={form.control} + name="reuseBuilding.0.instlCmptnDt" + render={({ field }) => ( + <FormUi label="설치완료일"> + <DatePicker + mode="single" + field={{ + value: + field.value && !isNaN(new Date(field.value).getTime()) + ? new Date(field.value) + : new Date(), + onChange: (date: Date | undefined) => + field.onChange(date), + }} + selectClass={"w-full"} + readOnly={readonly} + /> + </FormUi> + )} + /> + </div> - <div className="grid grid-cols-4 gap-2 mt-4"> - <div className="col-span-2"> - <FormField - control={form.control} - name="reuseBuilding.0.instlCmptnDt" - render={({ field }) => ( - <FormUi label="설치완료일"> - <DatePicker - mode="single" - field={{ - value: - field.value && !isNaN(new Date(field.value).getTime()) - ? new Date(field.value) - : new Date(), // Provide a default Date value - onChange: (date) => field.onChange(date), - - }} - selectClass={"w-full"} - readOnly={readonly} - /> - </FormUi> - )} - /> + <div className="col-span-3"> + <FormField + control={form.control} + name={`reuseBuilding.0.instlCst`} + render={({ field }) => { + const value = field.value; + const isUnknown = value === null || value === -1; // 또는 Number(value) === -1 + return ( + <FormUi label="설치비(원)"> + <Input + placeholder="1,000,000" + type="text" + className={selectClass} + readOnly={readonly} + value={ + isUnknown ? "확인불가" : formatCommaNumber(value) + } + onFocus={() => { + // 입력 시 자동으로 확인불가 해제 + if (isUnknown && !readonly) { + field.onChange(""); // 초기화 + } + }} + onChange={(e) => { + if (readonly) return; + handleMinusNumberChange(e, field.onChange); + }} + maxLength={numSize} + /> + </FormUi> + ); + }} + /> + </div> + </div> + <div className=""> + <h1 className={formH1Class}>빗물이용시설 정보</h1> + </div> + <div> + <h2 className={formH2Class}>집수면 및 저류조</h2> + </div> + <div className="grid grid-cols-12 gap-4 mt-4 "> + <div className="col-span-4"> + <FormField + control={form.control} + name="reuseBuilding.0.rainyObj.dtfcltCpct" + render={({ field }) => { + const isUnknown = field.value === null || field.value === -1; // 또는 Number(value) === -1 + return ( + <FormUi label="저류조 용량(m³)"> + <Input + className={selectClass} + placeholder="1,000,000" + type="text" + value={ + isUnknown + ? "확인불가" + : formatCommaNumber(field.value) + } + onFocus={() => { + // 입력 시 자동으로 확인불가 해제 + if (isUnknown && !readonly) { + field.onChange(""); // 초기화 + } + }} + onChange={(e) => { + if (readonly) return; + handleMinusNumberChange(e, field.onChange); + }} + readOnly={readonly} + maxLength={numSize} + /> + </FormUi> + ); + }} + /> + </div> + <div className="col-span-4"> + <FormField + control={form.control} + name="reuseBuilding.0.rainyObj.pntfColKnd" + render={({ field }) => ( + <FormUi label="집수면 종류"> + <SelectBox + className={""} + selectClass={selectClass} + label={""} + selectArray={rainFcltCd} + onSelectValue={readonly ? () => { } : field.onChange} + value={field.value ?? rainFcltCd[0]?.val} + readOnly={readonly} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-4"> + <FormField + control={form.control} + name="reuseBuilding.0.rainyObj.pntfColArea" + render={({ field }) => { + const value = field.value; + const isUnknown = value === null || value === -1; // 또는 Number(value) === -1 + return ( + // <div className="flex items-center gap-2"> + <FormUi label="집수면 면적(m²)"> + <Input + className={selectClass} + placeholder="1,000,000" + type="text" + value={ + isUnknown ? "확인불가" : formatCommaNumber(value) + } + onFocus={() => { + // 입력 시 자동으로 확인불가 해제 + if (isUnknown && !readonly) { + field.onChange(""); // 초기화 + } + }} + onChange={(e) => { + if (readonly) return; + handleMinusNumberChange(e, field.onChange); + }} + readOnly={readonly} + maxLength={numSize} + /> + </FormUi> + // </div> + ); + }} + /> + </div> </div> - <div className="col-span-2"> - <FormField - control={form.control} - name="reuseBuilding.0.instlCst" - render={({ field }) => ( - <FormUi label="설치비"> - <Input - className={selectClass} - placeholder="1,000,000" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={readonly} - /> - </FormUi> - )} - /> + <div> + <h2 className={formH2Class}>여과 등 처리시설</h2> </div> - </div> - <div className=""> - <h1 className={formH1Class}> - 빗물이용시설 정보 - </h1> - </div> - <div> - <h2 className={formH2Class}>집수면</h2> - </div> - <div className="grid grid-cols-10 gap-4 mt-4 "> - <div className="col-span-5"> - <FormField - control={form.control} - name="reuseBuilding.0.rainyObj.pntfColKnd" - render={({ field }) => ( - <FormUi label="종류"> - <SelectBox - className={""} - selectClass={selectClass} - label={""} - selectArray={rainFcltCd} - onSelectValue={readonly ? () => { } : field.onChange} - value={field.value ?? rainFcltCd[0]?.val} - readOnly={readonly} - /> - </FormUi> - )} - /> - </div> - <div className="col-span-5"> - <FormField - control={form.control} - name="reuseBuilding.0.rainyObj.pntfColArea" - render={({ field }) => ( - <FormUi label="면적(m³)"> - <Input - className={selectClass} - placeholder="1,000,000" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={readonly} - /> - </FormUi> - )} - /> - </div> - </div> + <div className="grid grid-cols-12 gap-2 mt-4"> + <div className="col-span-4"> + <FormField + control={form.control} + name="reuseBuilding.0.rainyObj.filtrTrtmfctYn" + render={({ field }) => ( + <FormUi label="유무"> + <SelectBox + className={""} + selectClass={selectClass} + label={""} + selectArray={code.bdstFilterYn} + onSelectValue={readonly ? () => { } : field.onChange} + value={field.value ?? code.yn[0]?.val} + readOnly={readonly} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-4"> + <FormField + control={form.control} + name="reuseBuilding.0.rainyObj.filtrTrtmfctCpct" + render={({ field }) => { + const isUnknown = field.value === null || field.value === -1; // 또는 Number(value) === -1 + return ( + <FormUi label="용량(m³/일)"> + <Input + className={selectClass} + placeholder="1,000,000" + type="text" + value={ + isUnknown + ? "확인불가" + : formatCommaNumber(field.value) + } + onFocus={() => { + // 입력 시 자동으로 확인불가 해제 + if (isUnknown && !readonly) { + field.onChange(""); // 초기화 + } + }} + onChange={(e) => { + if (readonly) return; + handleMinusNumberChange(e, field.onChange); + }} + readOnly={readonly} + disabled={filtrTrtmfctYn !== "Y"} + maxLength={numSize} + /> + </FormUi> + ); + }} + /> + </div> + <div className="col-span-4"> + <FormField + control={form.control} + name="reuseBuilding.0.rainyObj.filtrTrtmfctPrs" + render={({ field }) => { + // const value = field.value; + // const isUnknown = value === null || value === "-"; // 또는 Number(value) === -1 - <div> - <h2 className={formH2Class}>여과 등 처리시설</h2> - </div> - <div className="grid grid-cols-12 gap-2 mt-4"> - <div className="col-span-4"> - <FormField - control={form.control} - name="reuseBuilding.0.rainyObj.filtrTrtmfctYn" - render={({ field }) => ( - <FormUi label="유무"> - <SelectBox - className={""} - selectClass={selectClass} - label={""} - selectArray={code.yn} - onSelectValue={readonly ? () => { } : field.onChange} - value={field.value || code.yn[0]?.val} - readOnly={readonly} - /> - </FormUi> - )} - /> + return ( + // <div className="flex items-center gap-2"> + <FormUi label="처리공정"> + <Input + className={" h-full !text-xl col-span-4"} + placeholder="처리공정 입력" + type="text" + value={field.value ?? ""} + onChange={field.onChange} + readOnly={readonly} + disabled={filtrTrtmfctYn !== "Y"} + maxLength={txtSize} + /> + </FormUi> + // </div> + ); + }} + /> + </div> </div> - <div className="col-span-4"> - <FormField - control={form.control} - name="reuseBuilding.0.rainyObj.filtrTrtmfctCpct" - render={({ field }) => ( - <FormUi label="용량(m³/일)"> - <Input - className={selectClass} - placeholder="1,000,000" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={filtrTrtmfctYn !== "Y"} - /> - </FormUi> - )} - /> + <div className=" my-2"> + <h1 className={formH1Class}>인센티브 정보</h1> </div> - <div className="col-span-4"> - <FormField - control={form.control} - name="reuseBuilding.0.rainyObj.filtrTrtmfctPrs" - render={({ field }) => ( - <FormUi label="처리공정"> - <Input - className={selectClass} - placeholder="" - type="text" - value={field.value ?? ""} - onChange={field.onChange} - readOnly={filtrTrtmfctYn !== "Y"} - /> - </FormUi> - )} - /> + <div> + <h2 className={formH2Class}>설치비 지원</h2> </div> - </div> - <div className=" my-2"> - <h1 className={formH1Class}> - 인센티브 정보 - </h1> - </div> - <div> - <h2 className={formH2Class}>설치비 지원</h2> - </div> - <div className="grid grid-cols-10 gap-2 mt-4"> - <div className="col-span-5"> - <FormField - control={form.control} - name="reuseBuilding.0.instlCstSprtYn" - render={({ field }) => ( - <FormUi label="지원 여부"> - <SelectBox - className={""} - selectClass={selectClass} - label={""} - selectArray={code.yn} - onSelectValue={readonly ? () => { } : field.onChange} - value={field.value ?? code.yn[0]?.val} - readOnly={readonly} - /> - </FormUi> - )} - /> + <div className="grid grid-cols-10 gap-2 mt-4"> + <div className="col-span-5"> + <FormField + control={form.control} + name="reuseBuilding.0.instlCstSprtYn" + render={({ field }) => ( + <FormUi label="지원 여부"> + <SelectBox + className={""} + selectClass={selectClass} + label={""} + selectArray={code.bdstIncenYn} + onSelectValue={readonly ? () => { } : field.onChange} + value={field.value ?? code.yn[0]?.val} + readOnly={readonly} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-5"> + <FormField + control={form.control} + name="reuseBuilding.0.instlCstSprtAmt" + render={({ field }) => { + const value = field.value; + const isUnknown = value === null || value === -1; // 또는 Number(value) === -1 + return ( + <FormUi label="지원금액(원)"> + <Input + className={selectClass} + placeholder="" + type="text" + readOnly={readonly || isUnknown} + value={ + isUnknown ? "확인불가" : formatCommaNumber(value) + } + onFocus={() => { + // 입력 시 자동으로 확인불가 해제 + if (isUnknown && !readonly) { + field.onChange(""); // 초기화 + } + }} + onChange={(e) => { + if (readonly) return; + handleMinusNumberChange(e, field.onChange); + }} + disabled={instlCstSprtYn !== "Y"} + maxLength={numSize} + /> + </FormUi> + ); + }} + /> + </div> </div> - <div className="col-span-5"> - <FormField - control={form.control} - name="reuseBuilding.0.instlCstSprtAmt" - render={({ field }) => ( - <FormUi label="지원금액(원)"> - <Input - className={selectClass} - placeholder="1,000,000" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={instlCstSprtYn !== "Y"} - /> - </FormUi> - )} - /> + <div> + <h2 className={formH2Class}>녹색건축물 인증</h2> </div> - </div> + <div className="grid grid-cols-6 gap-2 mt-4"> + <div className="col-span-3"> + <FormField + control={form.control} + name="reuseBuilding.0.grntchBdstCertYn" + render={({ field }) => ( + <FormUi label="인증여부"> + <SelectBox + className={""} + selectClass={selectClass} + label={""} + selectArray={code.bdstIncenYn} + onSelectValue={readonly ? () => { } : field.onChange} + value={field.value ?? code.yn[0]?.val} + readOnly={readonly} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-3"> + <FormField + control={form.control} + name="reuseBuilding.0.grntchBdstCertYr" + render={({ field }) => { + const isUnknown = + field.value === null || field.value === "0000"; // 또는 Number(value) === -1 + return ( + <FormUi label="인증년도"> + <Input + className={selectClass} + placeholder="2024" + type="" + onFocus={() => { + // 입력 시 자동으로 확인불가 해제 + if (isUnknown && !readonly) { + field.onChange(""); // 초기화 + } + }} + value={isUnknown ? "확인불가" : (field.value ?? "")} + onChange={(e) => { + if (readonly) return; + if (e.target.value === "확인불가") { + field.onChange("0000"); + } else { + field.onChange(e.target.value); + } + }} + disabled={grntchBdstCertYn !== "Y"} + max={4} + /> + </FormUi> + ); + }} + /> + </div> + <div className="col-span-3 "> + <FormField + control={form.control} + name="reuseBuilding.0.grntchBdstCertNo" + render={({ field }) => { + const isUnknown = + field.value === null || field.value === "0000"; // 또는 Number(value) === -1 + return ( + <FormUi label="인증번호"> + <Input + className={selectClass} + placeholder="ab1234678" + type="" + onFocus={() => { + // 입력 시 자동으로 확인불가 해제 + if (isUnknown && !readonly) { + field.onChange(""); // 초기화 + } + }} + value={isUnknown ? "확인불가" : (field.value ?? "")} + onChange={(e) => { + if (readonly) return; - <div> - <h2 className={formH2Class}>녹색건축물 인증</h2> - </div> - <div className="grid grid-cols-6 gap-2 mt-4"> - <div className="col-span-3"> - <FormField - control={form.control} - name="reuseBuilding.0.grntchBdstCertYn" - render={({ field }) => ( - <FormUi label="인증여부"> - <SelectBox - className={""} - selectClass={selectClass} - label={""} - selectArray={code.yn} - onSelectValue={readonly ? () => { } : field.onChange} - value={field.value ?? code.yn[0]?.val} - /> - </FormUi> - )} - /> + if (e.target.value === "확인불가") { + field.onChange(""); + } else { + field.onChange(e.target.value); + } + }} + disabled={grntchBdstCertYn !== "Y"} + maxLength={txtSize} + /> + </FormUi> + ); + }} + /> + </div> + <div className="col-span-3"> + <FormField + control={form.control} + name="reuseBuilding.0.grntchBdstCertGrd" + render={({ field }) => ( + <FormUi label="인증등급"> + <SelectBox + className={""} + selectClass={selectClass} + label={""} + selectArray={code.bdstGrntchClass} + onSelectValue={readonly ? () => { } : field.onChange} + value={field.value ?? code.bdstGrntchClass[0]?.val} + readOnly={readonly} + /> + </FormUi> + )} + /> + </div> </div> - <div className="col-span-3"> - <FormField - control={form.control} - name="reuseBuilding.0.grntchBdstCertYr" - render={({ field }) => ( - <FormUi label="인증년도"> - <Input - className={selectClass} - placeholder="2024" - type="" - value={field.value ?? ""} - onChange={(e) => { - if (!readonly) field.onChange(e); - }} - readOnly={grntchBdstCertYn !== "Y"} - /> - </FormUi> - )} - /> - </div> - <div className="col-span-3 "> - <FormField - control={form.control} - name="reuseBuilding.0.grntchBdstCertNo" - render={({ field }) => ( - <FormUi label="인증번호"> - <Input - className={selectClass} - placeholder="ab1234678" - type="" - value={field.value ?? ""} - onChange={(e) => { - if (!readonly) field.onChange(e); - }} - readOnly={grntchBdstCertYn !== "Y"} - /> - </FormUi> - )} - /> - </div> - <div className="col-span-3"> - <FormField - control={form.control} - name="reuseBuilding.0.grntchBdstCertGrd" - render={({ field }) => ( - <FormUi label="인증등급"> - <Input - className={selectClass} - placeholder="A" - type="" - value={field.value ?? ""} - onChange={(e) => { - if (!readonly) field.onChange(e); - }} - readOnly={grntchBdstCertYn !== "Y"} - /> - </FormUi> - )} - /> - </div> - </div> + <BdstAdd form={form} readonly={readonly}></BdstAdd> + </ScrollArea> </form> </Form> </div> diff --git a/apps/wr/components/ui/form/fcltReg/rswtrReg/rswtrRegForm.tsx b/apps/wr/components/ui/form/fcltReg/rswtrReg/rswtrRegForm.tsx index a4f29477a..59ce18d93 100644 --- a/apps/wr/components/ui/form/fcltReg/rswtrReg/rswtrRegForm.tsx +++ b/apps/wr/components/ui/form/fcltReg/rswtrReg/rswtrRegForm.tsx @@ -8,8 +8,9 @@ import { } from "@/app/store/ruseFcltMng/fcltReg/rswtReg"; import { formH1Class, formH2Class } from "@/components/lib/class/comClass"; import { + confirmToast, formatCommaNumber, - handleCommaNumberChange, + handleMinusNumberChange } from "@/components/lib/func/commom"; import { DatePicker, @@ -18,31 +19,45 @@ import { FormUi, SelectBox, } from "@common/business_components/ui"; -import { Button, Input } from "@common/components/ui"; +import { Button, Input, ScrollArea } from "@common/components/ui"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; +import BdstAdd from "../bdstReg/bdstAdd"; import BdstReg from "../bdstReg/bdstReg"; +import { useTranslation } from "react-i18next"; +import { numSize, txtSize } from "@/app/store/common/code/basicCode"; export default function RswtrRegForm({ rswtForm, isModal, - readonly = false + setIsModal, + readonly = false, }: { rswtForm: customRswtBdst; isModal: boolean; + setIsModal?: (isModal: boolean) => void; readonly?: boolean; }) { - const { rswt, code, rswtBdstUpdate, rswtBdstReg } = useWrStore( + const { code, rswtBdstUpdate, rswtBdstReg } = useWrStore( (state) => state ); - - const defaultValues: z.infer<typeof rswtZod> = rswtZod.parse({}); + const [divStyle, setDivStyle] = useState("h-[53vh]"); + useEffect(() => { + if (isModal === true) { + setDivStyle("h-[53vh] max-h-[37vh]") + } else if (readonly === true) { + setDivStyle("h-[62vh]") + } else { + setDivStyle("h-[57vh]") + } + }, [isModal, readonly]); + // const defaultValues: z.infer<typeof rswtZod> = rswtZod.parse({}); const form = useForm<z.infer<typeof rswtZod>>({ resolver: zodResolver(rswtZod), - defaultValues: defaultValues, + // defaultValues: defaultValues, }); const [grntchBdstCertYn, instlCstSprtYn, filtrTrtmfctProcs] = form.watch([ @@ -51,38 +66,59 @@ export default function RswtrRegForm({ "reuseBuilding.0.rswtrObj.filtrTrtmfctProcs", ]); + const { t } = useTranslation('base'); const onSubmit = (values: z.infer<typeof rswtZod>) => { if (isModal) { - rswtBdstReg(values, "41"); + const handleConfirm = async () => { + rswtBdstReg(values, "41"); + if (setIsModal) setIsModal(false); + }; + confirmToast(t('isInsertMsg'), handleConfirm); + } else { - rswtBdstUpdate(values, "41"); + const handleConfirm = async () => { + rswtBdstUpdate(values, "41"); + }; + confirmToast(t('isUpdateMsg'), handleConfirm); } }; + + const filtrTrtmfctCpct = form.watch('reuseBuilding.0.rswtrObj.filtrTrtmfctCpct') const cleanRswtData = (raw: any): z.infer<typeof rswtZod> => { + + console.log('raw ', raw) return { ...raw, - frstArchPrmsnDt: new Date(raw?.frstArchPrmsnDt || null), + frstArchPrmsnDt: raw?.frstArchPrmsnDt + ? new Date(raw.frstArchPrmsnDt) + : new Date(), // raw?.frstArchPrmsnDt가 유효하지 않으면 현재 날짜로 초기화 archChgPrmsnDt: raw?.archChgPrmsnDt ? new Date(raw?.archChgPrmsnDt) : null, reuseBuilding: (raw?.reuseBuilding || []).map((item: any) => ({ ...item, - instlCmptnDt: new Date(item?.instlCmptnDt), - operMngCd: item?.operMngCd ?? "", // null → "" - grntchBdstCertYr: - typeof item?.grntchBdstCertYr === "string" - ? item?.grntchBdstCertYr - : "", // null → "" + instlCmptnDt: item?.instlCmptnDt + ? new Date(item.instlCmptnDt) + : new Date(), // item?.instlCmptnDt가 유효하지 않으면 현재 날짜로 초기화 + operMngCd: item?.operMngCd || "", // null → "" + instlCstSprtYn: item?.instlCstSprtYn || code.yn[0]?.val, // null → "Y" + grntchBdstCertYn: item?.grntchBdstCertYn || code.yn[0]?.val, rswtrObj: { ...item?.rswtrObj, + filtrTrtmfctYn: item?.rainyObj?.filtrTrtmfctYn || code.yn[0]?.val, + filtrTrtmfctCpct: item?.rswtrObj?.filtrTrtmfctCpct ?? null, + filtrTrtmfctProcs: item?.rswtrObj?.filtrTrtmfctProcs || code.rswtPrscCdMthdCd[0]?.val, + orgwtKndCd: item?.rswtrObj?.orgwtKndCd || code.orgwtKndCd[0]?.val, }, })), }; }; - - useEffect(()=>{ - console.log("rswtForm:", rswtForm) - }, [rswtForm]) + // useEffect(() =>{ + // console.log(' formform ', form.watch()) + // },[form.formState]) + useEffect(() => { + console.log(' filtrTrtmfctCpct ', filtrTrtmfctCpct) + }, [filtrTrtmfctCpct]) useEffect(() => { console.log("form errors:", form.formState.errors); @@ -95,6 +131,16 @@ export default function RswtrRegForm({ } else if (instlCstSprtYn === "N") { form.setValue("reuseBuilding.0.instlCstSprtAmt", 0); } + + + if (grntchBdstCertYn === "D") { + form.setValue("reuseBuilding.0.grntchBdstCertYr", "0000"); + form.setValue("reuseBuilding.0.grntchBdstCertNo", "확인불가"); + form.setValue("reuseBuilding.0.grntchBdstCertGrd", "확인불가"); + } else if (instlCstSprtYn === "D") { + form.setValue("reuseBuilding.0.instlCstSprtAmt", -1); + } + }, [grntchBdstCertYn, instlCstSprtYn, filtrTrtmfctProcs, form]); useEffect(() => { @@ -107,314 +153,355 @@ export default function RswtrRegForm({ <form onSubmit={form.handleSubmit(onSubmit)}> {/* <div className="flex justify-between items-center mx-3"> */} <div> + <h1 className="text-2xl font-bold text-gray-900 mt-3 ml-1"> + {readonly && `(시설명 : ${rswtForm?.bdstNm})`} + </h1> <div className="grid grid-cols-12 flex items-center card-header"> <h1 className="text-2xl font-medium text-label col-span-10"> 건축물 운영정보 </h1> {!readonly && ( - <Button type="submit" className="col-start-12 col-span-1" size={"sm"}> + <Button + type="submit" + className="col-start-12 col-span-1" + size={"sm"} + > 저장 </Button> )} </div> </div> - - {isModal && rswtForm != undefined ? <BdstReg form={form} /> : <></>} - {isModal ? ( - <h1 className="pl-3 text-xl col-span-10 mt-4 "> - 중수도 시설 정보{" "} - </h1> - ) : ( - <></> - )} - <h1 className={formH1Class}>기본정보</h1> - <div className="grid grid-cols-4 gap-2 mt-4"> - <div className="col-span-2"> - <FormField - control={form.control} - name="reuseBuilding.0.instlCmptnDt" - render={({ field }) => ( - <FormUi label="설치완료일"> - <DatePicker - mode="single" - field={{ - value: - field.value && !isNaN(new Date(field.value).getTime()) - ? new Date(field.value) - : new Date(), // Provide a default Date value - onChange: (date) => field.onChange(date), - }} - selectClass={"w-full"} - readOnly={readonly} - /> - </FormUi> - )} - /> + <ScrollArea className={divStyle}> + {isModal && rswtForm != undefined ? <BdstReg form={form} /> : <></>} + {isModal ? ( + <h1 className="pl-3 text-xl col-span-10 mt-4 "> + 중수도 시설 정보{" "} + </h1> + ) : ( + <></> + )} + <h1 className={formH1Class}>기본정보</h1> + <div className="grid grid-cols-4 gap-2 mt-4"> + <div className="col-span-2"> + <FormField + control={form.control} + name="reuseBuilding.0.instlCmptnDt" + render={({ field }) => ( + <FormUi label="설치완료일"> + <DatePicker + mode="single" + field={{ + value: + field.value && !isNaN(new Date(field.value).getTime()) + ? new Date(field.value) + : new Date(), // Provide a default Date value + onChange: (date) => field.onChange(date), + }} + selectClass={"w-full"} + readOnly={readonly} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-2"> + <FormField + control={form.control} + name="reuseBuilding.0.instlCst" + render={({ field }) => { + const value = field.value; + const isUnknown = value === null || value === -1; // 또는 Number(value) === -1 + return ( + <FormUi label="설치비(원)"> + <Input + className={selectClass} + placeholder="1,000,000" + type="text" + value={ + isUnknown ? "확인불가" : formatCommaNumber(value) + } + onFocus={() => { + // 입력 시 자동으로 확인불가 해제 + if (isUnknown && !readonly) { + field.onChange(""); // 초기화 + } + }} + onChange={(e) => { + if (readonly) return; + handleMinusNumberChange(e, field.onChange); + }} + readOnly={readonly} + maxLength={numSize} + /> + </FormUi> + ); + }} + /> + </div> + </div> + <div className=" my-4"> + <h1 className={formH1Class}>중수도 시설 정보</h1> </div> - <div className="col-span-2"> - <FormField - control={form.control} - name="reuseBuilding.0.instlCst" - render={({ field }) => ( - <FormUi label="설치비"> - <Input - className={selectClass} - placeholder="1,000,000" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={readonly} - /> - </FormUi> - )} - /> - </div> - </div> - <div className="grid grid-cols-4 gap-2 mt-4"> - <div className="col-span-2"> - <FormField - control={form.control} - name="reuseBuilding.0.rswtrObj.wtrUsqty" - render={({ field }) => ( - <FormUi label="물사용량(m³)"> - <Input - className={selectClass} - placeholder="1,000,000" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={readonly} - /> - </FormUi> - )} - /> + <div className="grid grid-cols-12 gap-1 mt-4"> + <div className="col-span-4"> + <FormField + control={form.control} + name="reuseBuilding.0.rswtrObj.filtrTrtmfctCpct" + render={({ field }) => { + const value = field.value; + const isUnknown = value === null || value === -1; // 또는 Number(value) === -1 + + return ( + <FormUi label="처리시설 용량(m³)"> + <Input + className={selectClass} + placeholder="" + type="text" + value={ + isUnknown ? "확인불가" : formatCommaNumber(value) + } + onFocus={() => { + // 입력 시 자동으로 확인불가 해제 + if (isUnknown && !readonly) { + + console.log('value') + field.onChange(""); // 초기화 + } + }} + onChange={(e) => { + if (readonly) return; + handleMinusNumberChange(e, field.onChange); + }} + readOnly={readonly} + maxLength={numSize} + /> + </FormUi> + ); + }} + /> + </div> + <div className="col-span-4"> + <FormField + control={form.control} + name="reuseBuilding.0.rswtrObj.orgwtKndCd" + render={({ field }) => ( + <FormUi label="원수 종류"> + <SelectBox + className={""} + label={""} + selectClass={selectClass} + selectArray={code.orgwtKndCd} + onSelectValue={readonly ? () => { } : field.onChange} + value={field.value || code.orgwtKndCd[0]?.val} + readOnly={readonly} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-4"> + <FormField + control={form.control} + name="reuseBuilding.0.rswtrObj.filtrTrtmfctProcs" + render={({ field }) => ( + <FormUi label="주요처리공정"> + <SelectBox + className={""} + selectClass={selectClass} + label={""} + selectArray={code.rswtPrscCdMthdCd} + onSelectValue={readonly ? () => { } : field.onChange} + value={field.value || code.rswtPrscCdMthdCd[0]?.val} + readOnly={readonly} + /> + </FormUi> + )} + /> + </div> </div> - <div className="col-span-2"> - <FormField - control={form.control} - name="reuseBuilding.0.rswtrObj.oblgTrgtWtrUsqty" - render={({ field }) => ( - <FormUi label="의무대상 물사용량(m³)"> - <Input - className={selectClass} - placeholder="1,000,000" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={readonly} - /> - </FormUi> - )} - /> + <div className=" my-2"> + <h1 className={formH1Class}>인센티브 정보</h1> + </div> + <div> + <h2 className={formH2Class}>설치비(원) 지원</h2> + </div> + <div className="grid grid-cols-2 gap-2 mt-4"> + <div className="col-span-1"> + <FormField + control={form.control} + name="reuseBuilding.0.instlCstSprtYn" + render={({ field }) => ( + <FormUi label="설치비(원) 지원여부"> + <SelectBox + className={""} + selectClass={selectClass} + label={""} + selectArray={code.bdstIncenYn} + onSelectValue={readonly ? () => { } : field.onChange} + value={field.value || code.bdstIncenYn[0]?.val} + readOnly={readonly} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-1"> + <FormField + control={form.control} + name="reuseBuilding.0.instlCstSprtAmt" + render={({ field }) => { + const value = field.value; + const isUnknown = value === null || value === -1; // 또는 Number(value) === -1 + return ( + <FormUi label="지원금액(원)"> + <Input + className={selectClass} + placeholder="" + type="text" + readOnly={readonly || isUnknown} + value={ + isUnknown ? "확인불가" : formatCommaNumber(value) + } + onFocus={() => { + // 입력 시 자동으로 확인불가 해제 + if (isUnknown && !readonly) { + field.onChange(""); // 초기화 + } + }} + onChange={(e) => { + if (readonly) return; + handleMinusNumberChange(e, field.onChange); + }} + disabled={instlCstSprtYn === "N" || instlCstSprtYn === "D"} + maxLength={numSize} + /> + </FormUi> + ); + }} + /> + </div> </div> - </div> - <div className=" my-4"> - <h1 className={formH1Class}>중수도 시설 정보</h1> - </div> - <div className="grid grid-cols-12 gap-1 mt-4"> - <div className="col-span-4"> - <FormField - control={form.control} - name="reuseBuilding.0.rswtrObj.filtrTrtmfctCpct" - render={({ field }) => ( - <FormUi label="처리시설 용량"> - <Input - className={selectClass} - placeholder="" - type="number" - value={field.value ?? 0} - onChange={(e) => { - if (!readonly) field.onChange(Number(e.target.value)); - }} - readOnly={readonly} - /> - </FormUi> - )} - /> + <div> + <h2 className={formH2Class}>녹색건축물 인증</h2> </div> - <div className="col-span-4"> - <FormField - control={form.control} - name="reuseBuilding.0.rswtrObj.orgwtKndCd" - render={({ field }) => ( - <FormUi label="원수 종류"> - <SelectBox - className={""} - label={""} - selectClass={selectClass} - selectArray={code.orgwtKndCd} - onSelectValue={readonly ? () => { } : field.onChange} - value={field.value ?? code.orgwtKndCd[0]?.val} - readOnly={readonly} - /> - </FormUi> - )} - /> - </div> - <div className="col-span-4"> - <FormField - control={form.control} - name="reuseBuilding.0.rswtrObj.filtrTrtmfctProcs" - render={({ field }) => ( - <FormUi label="주요처리공정"> - <Input - className={selectClass} - placeholder="" - type="" - value={field.value ?? ""} - onChange={(e) => { - if (!readonly) field.onChange - }} - readOnly={readonly} - /> - </FormUi> - )} - /> - </div> - </div> + <div className="grid grid-cols-6 gap-2 mt-4"> + <div className="col-span-3"> + <FormField + control={form.control} + name="reuseBuilding.0.grntchBdstCertYn" + render={({ field }) => ( + <FormUi label="인증여부"> + <SelectBox + className={""} + selectClass={selectClass} + label={""} + selectArray={code.bdstIncenYn} + onSelectValue={readonly ? () => { } : field.onChange} + value={field.value || code.yn[0]?.val} + readOnly={readonly} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-3"> + <FormField + control={form.control} + name="reuseBuilding.0.grntchBdstCertYr" + render={({ field }) => { + const isUnknown = field.value === null || field.value === "0000"; // 또는 Number(value) === -1 + return ( + <FormUi label="인증년도"> + <Input + className={selectClass} + placeholder="2024" + type="" + onFocus={() => { + // 입력 시 자동으로 확인불가 해제 + if (isUnknown && !readonly) { + field.onChange(""); // 초기화 + } + }} + value={ + isUnknown ? '확인불가' : field.value ?? ""} + onChange={(e) => { + if (readonly) return + if (e.target.value === '확인불가') { + field.onChange('0000'); + } else { + field.onChange(e.target.value); + } + }} + disabled={grntchBdstCertYn == "N" || grntchBdstCertYn == "D"} + max={4} + /> + </FormUi> + ) + }} + /> + </div> + <div className="col-span-3"> + <FormField + control={form.control} + name="reuseBuilding.0.grntchBdstCertNo" + render={({ field }) => { + const isUnknown = field.value === null || field.value === "0000"; // 또는 Number(value) === -1 + return ( + <FormUi label="인증번호"> + <Input + className={selectClass} + placeholder="ab1234678" + type="" + onFocus={() => { + // 입력 시 자동으로 확인불가 해제 + if (isUnknown && !readonly) { + field.onChange(""); // 초기화 + } + }} + value={ + isUnknown ? '확인불가' : field.value ?? ""} + onChange={(e) => { + if (readonly) return - <div className=" my-2"> - <h1 className={formH1Class}>인센티브 정보</h1> - </div> - <div> - <h2 className={formH2Class}>설치비 지원</h2> - </div> - <div className="grid grid-cols-2 gap-2 mt-4"> - <div className="col-span-1"> - <FormField - control={form.control} - name="reuseBuilding.0.instlCstSprtYn" - render={({ field }) => ( - <FormUi label="설치비 지원여부"> - <SelectBox - className={""} - selectClass={selectClass} - label={""} - selectArray={code.yn} - onSelectValue={readonly ? () => { } : field.onChange} - value={field.value ?? code.yn[0]?.val} - readOnly={readonly} - /> - </FormUi> - )} - /> + if (e.target.value === '확인불가') { + field.onChange(''); + } else { + field.onChange(e.target.value); + } + }} + disabled={grntchBdstCertYn == "N" || grntchBdstCertYn == "D"} + maxLength={txtSize} + /> + </FormUi> + ) + }} + /> + </div> + <div className="col-span-3"> + <FormField + control={form.control} + name="reuseBuilding.0.grntchBdstCertGrd" + render={({ field }) => ( + <FormUi label="인증등급"> + <SelectBox + className={""} + selectClass={selectClass} + label={""} + selectArray={code.bdstGrntchClass} + onSelectValue={readonly ? () => { } : field.onChange} + value={field.value || code.bdstGrntchClass[0]?.val} + readOnly={readonly} + /> + </FormUi> + )} + /> + </div> </div> - <div className="col-span-1"> - <FormField - control={form.control} - name="reuseBuilding.0.instlCstSprtAmt" - render={({ field }) => ( - <FormUi label="지원금액(원)"> - <Input - className={selectClass} - placeholder="1,000,000" - type="text" - onChange={(e) => { - if (!readonly) handleCommaNumberChange(e, field.onChange); - }} - value={formatCommaNumber(field.value)} - readOnly={instlCstSprtYn !== "Y"} - /> - </FormUi> - )} - /> - </div> - </div> - - <div> - <h2 className={formH2Class}>녹색건축물 인증</h2> - </div> - <div className="grid grid-cols-6 gap-2 mt-4"> - <div className="col-span-3"> - <FormField - control={form.control} - name="reuseBuilding.0.grntchBdstCertYn" - render={({ field }) => ( - <FormUi label="인증여부"> - <SelectBox - className={""} - selectClass={selectClass} - label={""} - selectArray={code.yn} - onSelectValue={readonly ? () => { } : field.onChange} - value={field.value ?? code.yn[0]?.val} - readOnly={readonly} - /> - </FormUi> - )} - /> - </div> - <div className="col-span-3"> - <FormField - control={form.control} - name="reuseBuilding.0.grntchBdstCertYr" - render={({ field }) => ( - <FormUi label="인증년도"> - <Input - className={selectClass} - placeholder="2024" - type="text" - {...field} - value={field.value ?? ""} - onChange={(e) => { - if (!readonly) field.onChange(e) - }} - readOnly={grntchBdstCertYn !== "Y"} - /> - </FormUi> - )} - /> - </div> - <div className="col-span-3"> - <FormField - control={form.control} - name="reuseBuilding.0.grntchBdstCertNo" - render={({ field }) => ( - <FormUi label="인증번호"> - <Input - className={selectClass} - placeholder="ab1234678" - type="" - value={field.value ?? ""} - onChange={(e) => { - if (!readonly) field.onChange(e) - }} - readOnly={grntchBdstCertYn !== "Y"} - /> - </FormUi> - )} - /> - </div> - <div className="col-span-3"> - <FormField - control={form.control} - name="reuseBuilding.0.grntchBdstCertGrd" - render={({ field }) => ( - <FormUi label="인증등급"> - <Input - className={selectClass} - placeholder="A" - type="" - value={field.value ?? ""} - onChange={(e) => { - if (!readonly) field.onChange(e) - }} - readOnly={grntchBdstCertYn !== "Y"} - /> - </FormUi> - )} - /> - </div> - </div> + <BdstAdd form={form} readonly={readonly}></BdstAdd> + </ScrollArea> </form> </Form> </div> diff --git a/apps/wr/components/ui/modal/chkgd/chkgdExcel/chkgdExcel.tsx b/apps/wr/components/ui/modal/chkgd/chkgdExcel/chkgdExcel.tsx index 15badc59d..46e6c5d30 100644 --- a/apps/wr/components/ui/modal/chkgd/chkgdExcel/chkgdExcel.tsx +++ b/apps/wr/components/ui/modal/chkgd/chkgdExcel/chkgdExcel.tsx @@ -1,17 +1,16 @@ import { useWrStore } from "@/app/store"; import { type rainChkZodType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn"; import { excelToRainChkData } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn/excel"; -import { chkgdRegExcel } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList"; +import { chkgdRegExcel } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList"; import type { rswtChkZodType } from "@/app/store/prfmncReg/chkgdnReg/rswrtChkgdn"; import { excelToRswtGuideData, excelToRswtPolution, } from "@/app/store/prfmncReg/chkgdnReg/rswrtChkgdn/excel"; -import { ToastFormUi } from "@/components/ui/toast/toastFromUi"; +import { showConfirmToast } from "@/components/lib/func/commom"; import { filePicked, filePickedArray, - FileUpload, Form, FormField, FormUi, @@ -20,23 +19,46 @@ import { } from "@common/business_components/ui"; import { Button, Input } from "@common/components/ui"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useForm } from "react-hook-form"; +import toast from "react-hot-toast"; import type { z } from "zod"; -export default function ChkgdExcel({ type }: { type: string }) { +export default function ChkgdExcel({ + type, + setIsRegExcel, + setYearChange, +}: { + type: string; + setIsRegExcel: (open: boolean) => void; + setYearChange: (year: string) => void; +}) { const { - rswtChkgdn, rainChkExcelDataUPload, rswtChkExceDataUpload, + setRainChkLoading, } = useWrStore((state) => { return state; }); - const [files, setFiles] = useState<File[] | null>(null); const [excelData, setExcelData] = useState< rswtChkZodType[] | rainChkZodType[] >(); + + const [file, setFile] = useState<File | null>(null); + + const clearFile = () => { + if (fileInputRef.current) { + fileInputRef.current.value = ""; // ✅ 파일 선택 비우기 + } + }; + const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const selectedFile = e.target.files?.[0]; + if (selectedFile) { + setFile(selectedFile); + } + }; const [excelRswtData, setExcelRswtData] = useState<rswtChkZodType[]>(); + const fileInputRef = useRef<HTMLInputElement>(null); const currentYear = new Date().getFullYear(); const startYear = 1970; @@ -49,7 +71,7 @@ export default function ChkgdExcel({ type }: { type: string }) { ); const defaultValues: z.infer<typeof chkgdRegExcel> = chkgdRegExcel.parse({ - chckYr: String(currentYear) + chckYr: String(currentYear), }); const form = useForm<z.infer<typeof chkgdRegExcel>>({ @@ -58,120 +80,138 @@ export default function ChkgdExcel({ type }: { type: string }) { }); useEffect(() => { - if (files) { - const file = files[0]; + if (file) { if (form.getValues("chckYr").length < 1) { - ToastFormUi({ - body: "파일업로드 이전에 등록년도를 작성해주세요 ", - title: "지도점검 등록", - }); - setFiles(null); + toast.error("파일업로드 이전에 등록년도를 작성해주세요 "); + setFile(null); return; } if (file) if (type === "RNB") { - filePicked(file, 0).then((res) => { - if (res !== undefined) - setExcelData(excelToRainChkData(res, form.getValues("chckYr"))); - }); + setRainChkLoading(true); + filePicked(file, 0) + .then(async (res) => { + const excelData = excelToRainChkData( + res, + form.getValues("chckYr") + ); + if (excelData.issues && excelData.issues.length > 0) { + excelData.issues.some((err, index) => { + showConfirmToast(err); + return index === 4; // true 반환 시 반복 종료 + }); + clearFile(); + } else { + setExcelData(excelData.rainChkgdData); + } + }) + .finally(() => { + setRainChkLoading(false); + }); } else if (type === "RST") { filePickedArray(file, 0).then((res) => { + let issues: string[] = []; + if (res && res?.length > 1) { - const excelArray = []; const guideData = excelToRswtGuideData( res[0], - form.getValues("chckYr") - ); - const polutionData = excelToRswtPolution( - res[1], - form.getValues("chckYr") + form.getValues("chckYr"), + issues ); + const polutionData = excelToRswtPolution(res[1], issues); + issues = [...guideData.issues, ...polutionData.issues]; - const keyGenerator = (item: rswtChkZodType) => - `${item.bdstRuseFcltFk}|${item.chckYmd}|${item.utztnUsgCd}`; - const totalData: rswtChkZodType[] = guideData.map((guide) => { - const fusion = polutionData.find( - (polution) => - guide.bdstRuseFcltFk === polution.bdstRuseFcltFk && - guide.chckYmd === polution.chckYmd && - guide.utztnUsgCd === polution.utztnUsgCd + if (issues && issues.length > 0) { + issues.some((err, index) => { + showConfirmToast(err); + return index === 6; // true 반환 시 반복 종료 + }); + clearFile(); + } else { + const keyGenerator = (item: rswtChkZodType) => + `${item.bdstRuseFcltSn}|${item.chckYmd}|${item.utztnUsgCd}`; + const totalData: rswtChkZodType[] = guideData.data.map( + (guide) => { + const fusion = polutionData.data.find( + (polution) => + guide.bdstRuseFcltSn === polution.bdstRuseFcltSn && + guide.chckYmd === polution.chckYmd && + guide.utztnUsgCd === polution.utztnUsgCd + ); + + if (fusion) { + return { + ...guide, + rswtrPollutions: fusion?.rswtrPollutions, + rswtrDetails: fusion?.rswtrDetails, + }; + } else { + return { + ...guide, + }; + } + } + ); + const guideKey = new Set(guideData.data.map(keyGenerator)); + const onlypolution = polutionData.data.filter( + (item) => !guideKey.has(keyGenerator(item)) ); - - if (fusion) { - return { - ...guide, - rswtrPollutions: fusion?.rswtrPollutions, - rswtrDetails:fusion?.rswtrDetails - }; - } else { - return { - ...guide, - }; - } - }); + const listTotalData: rswtChkZodType[] = [ + ...totalData, + ...onlypolution, + ]; - const guideKey = new Set(guideData.map(keyGenerator)); - - const onlypolution = polutionData.filter( - (item) => !guideKey.has(keyGenerator(item)) - ); - - - const listTotalData:rswtChkZodType[] =[ - ...totalData, - ...onlypolution - ] - - - setExcelRswtData(listTotalData); + setExcelRswtData(listTotalData); + } } }); } } - }, [files]); + }, [file]); - function findMatchingItems<T>( - arrayA: T[], - arrayB: T[], - keySelector: (item: T) => string, - condition?: (item: T) => boolean - ): T[] { - const bKeySet = new Set(arrayB.map(keySelector)); - return arrayA.filter( - (item) => - (!condition || condition(item)) && bKeySet.has(keySelector(item)) - ); - } - + // function findMatchingItems<T>( + // arrayA: T[], + // arrayB: T[], + // keySelector: (item: T) => string, + // condition?: (item: T) => boolean + // ): T[] { + // const bKeySet = new Set(arrayB.map(keySelector)); + // return arrayA.filter( + // (item) => + // (!condition || condition(item)) && bKeySet.has(keySelector(item)) + // ); + // } const onSubmit = (values: z.infer<typeof chkgdRegExcel>) => { if (values.chckYr === "") { - ToastFormUi({ body: "등록년도를 작성해주세요", title: "지도점검 등록" }); + toast.error("등록년도를 작성해주세요"); return; } if (type === "RNB") { if (excelData === undefined || excelData?.length === 0) { - ToastFormUi({ - body: "빈 파일 및 파일 오류 입니다.", - title: "지도점검 등록", - }); + toast.error("빈 파일 및 파일 오류 입니다."); + return; } else { - - rainChkExcelDataUPload(excelData as rainChkZodType[]); + rainChkExcelDataUPload( + excelData as rainChkZodType[], + form.getValues("chckYr") + ); + setIsRegExcel(false); + setYearChange(form.getValues("chckYr")); } } else if (type === "RST") { if (excelRswtData === undefined || excelRswtData?.length === 0) { - ToastFormUi({ - body: "빈 파일 및 파일 오류 입니다.", - title: "중수지도점검 등록", - }); + toast.error("빈 파일 및 파일 오류 입니다."); + return; } else { rswtChkExceDataUpload(excelRswtData as rswtChkZodType[]); + setIsRegExcel(false); + setYearChange(form.getValues("chckYr")); } } }; @@ -217,19 +257,23 @@ export default function ChkgdExcel({ type }: { type: string }) { </FormUi> )} /> - <FormField control={form.control} name="file" render={({ field }) => ( <FormUi label="점검결과 파일"> - <FileUpload - files={files || []} - setFiles={(files: File[] | null) => { - if (files) setFiles(files); - }} - fileTypes="xlsx" - ></FileUpload> + <div className="space-y-4 p-4"> + <Input + ref={fileInputRef} + type="file" + onChange={(file) => { + handleFileChange(file); + ////TODO : 폼 을 써야하는 지 확인 필요 + console.log(field.value); + }} + accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" + /> + </div> </FormUi> )} /> diff --git a/apps/wr/components/ui/modal/chkgd/chkgdModal/chkgdModal.tsx b/apps/wr/components/ui/modal/chkgd/chkgdModal/chkgdModal.tsx index 9572d1228..c528f5add 100644 --- a/apps/wr/components/ui/modal/chkgd/chkgdModal/chkgdModal.tsx +++ b/apps/wr/components/ui/modal/chkgd/chkgdModal/chkgdModal.tsx @@ -1,16 +1,14 @@ import { useWrStore } from "@/app/store"; -import { ScrollArea, Title } from "@common/business_components/ui"; -import { Button } from "@common/components/ui"; -import { useEffect, useState } from "react"; -import { headMakeprfmnc } from "@/app/ruseFcltMng/prfmncReg/chkgdnReg/TableHead"; -import { GuideDetailsTable } from "@/components/ui/table/chkgdTable/guideDetailTable/guideDetailTable"; +import type { TableUpperProps } from "@/app/store/common/type/header"; import type { splRuseWaterType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList"; import type { rswtrPolutionsZode } from "@/app/store/prfmncReg/chkgdnReg/rswrtChkgdn/headList"; +import { GuideDetailsTable } from "@/components/ui/table/chkgdTable/guideDetailTable/guideDetailTable"; +import { ScrollArea, Title } from "@common/business_components/ui"; +import { Button } from "@common/components/ui"; +import React, { useEffect, useState } from "react"; import type { z } from "zod"; -import React from "react"; -import type { TableUpperProps } from "@/app/store/common/type/header"; -export default function ChkgdModal({ type, obj }: { type: string; obj: any }) { +export default function ChkgdModal({ type, obj, setIsModal, readonly }: { type: string; obj: any, setIsModal: (open: boolean) => void, readonly: boolean }) { const { rainChkgdn, rswtChkgdn, @@ -39,7 +37,6 @@ export default function ChkgdModal({ type, obj }: { type: string; obj: any }) { "4": [] as z.infer<typeof rswtrPolutionsZode>[], }); - const { rainChk, ruseHeadList, splHeadList } = rainChkgdn; const [updateData, setUpdateData] = useState<splRuseWaterType[]>(); const initStats = (): z.infer<typeof rswtrPolutionsZode> => ({ @@ -97,7 +94,6 @@ export default function ChkgdModal({ type, obj }: { type: string; obj: any }) { target = { utztnUsgCd: code, ruseTotal: 0, splTotal: 0 }; acc.push(target); } - // 월별 값 누적 target[ruseMonthKey] = (target[ruseMonthKey] ?? 0) + ruseValue; target[splMonthKey] = (target[splMonthKey] ?? 0) + splValue; @@ -106,7 +102,17 @@ export default function ChkgdModal({ type, obj }: { type: string; obj: any }) { target.splTotal += splValue; return acc; }, []); - setGuidData(result); + + if (result) { + // '기타' 항목을 배열의 맨 뒤로 정렬합니다. + result.sort((a: splRuseWaterType, b: splRuseWaterType) => { + if (a.utztnUsgCd === "기타") return 1; // a를 b 뒤로 보냅니다. + if (b.utztnUsgCd === "기타") return -1; // b를 a 뒤로 보냅니다. + return 0; // 순서를 유지합니다. + }); + } + + setGuidData(result as splRuseWaterType[]); if (obj.rswtrPollutions) { const resultPolution: Record<string, any[]> = { @@ -144,21 +150,29 @@ export default function ChkgdModal({ type, obj }: { type: string; obj: any }) { const average = { utztnUsgCd: "평균", edit: false, - grssClfgUswtr: total.grssClfgUswtr / onlyData.length, - combResdch: total.combResdch / onlyData.length, - trbt: total.trbt / onlyData.length, - bod: total.bod / onlyData.length, - smll: total.smll / onlyData.length, - crmty: total.crmty / onlyData.length, - tph: total.tph / onlyData.length, - tn: total.tn / onlyData.length, - ph: total.ph / onlyData.length, - clIonQnt: total.clIonQnt / onlyData.length, + grssClfgUswtr: (total.grssClfgUswtr / onlyData.length).toFixed(1), + combResdch: (total.combResdch / onlyData.length).toFixed(1), + trbt: (total.trbt / onlyData.length).toFixed(1), + bod: (total.bod / onlyData.length).toFixed(1), + smll: (total.smll / onlyData.length).toFixed(1), + crmty: (total.crmty / onlyData.length).toFixed(1), + tph: (total.tph / onlyData.length).toFixed(1), + tn: (total.tn / onlyData.length).toFixed(1), + ph: (total.ph / onlyData.length).toFixed(1), + clIonQnt: (total.clIonQnt / onlyData.length).toFixed(1), }; if (!resultPolution[nodeKey]) { resultPolution[nodeKey] = []; } + + // '기타' 항목을 배열의 맨 뒤로 정렬합니다. + resultPolution[nodeKey].sort((a: any, b: any) => { + if (a.utztnUsgCd === "기타") return 1; // a를 b 뒤로 보냅니다. + if (b.utztnUsgCd === "기타") return -1; // b를 a 뒤로 보냅니다. + return 0; // 순서를 유지합니다. + }); + resultPolution[nodeKey].push(average); }); @@ -199,13 +213,16 @@ export default function ChkgdModal({ type, obj }: { type: string; obj: any }) { }); obj.guideDetails = realGuideDetail; if (type === "RNB") { + rainChkGuidUpdate(obj); + setIsModal(false) } else { const polution = Object.values(upPolutionData) .flat() .filter((item) => item.utztnUsgCd !== "평균"); obj.rswtrPollutions = polution; rwstPolAndGuideUpdate([obj]); + setIsModal(false) } }; @@ -221,40 +238,43 @@ export default function ChkgdModal({ type, obj }: { type: string; obj: any }) { <> <div> <div> - <Title - title={ - type === "RNB" - ? "빗물이용시설 " + "지도점검 결과등록" - : "중수도 이용시설 " + "지도점검 결과등록" - } - > - -
+
+ + {!rainChkSttn.isSttn ? ( + + ) : ( + <> + )} +

- {`(${obj.bdstNm})`} + {type === "RNB" + ? `시설명 : (${obj.bdstNm})` : `시설명 : (${obj.building.bdstNm})`}

- {!rainChkSttn.isSttn ? ( - - ) : ( - <> - )}

- {`연간보급수이용량`} + {`연간대체수이용량`}

{ setChGuidData(data); }} + readonly={readonly} />

@@ -263,11 +283,13 @@ export default function ChkgdModal({ type, obj }: { type: string; obj: any }) {

{ setChGuidData(data); }} + readonly={readonly} /> {type !== "RNB" && polutionData !== undefined ? ( @@ -285,10 +307,12 @@ export default function ChkgdModal({ type, obj }: { type: string; obj: any }) { { getPolutionData(data); }} + readonly={readonly} /> ); diff --git a/apps/wr/components/ui/modal/chkgd/pbnPrfExcelModal/pbnPrfExcelModal.tsx b/apps/wr/components/ui/modal/chkgd/pbnPrfExcelModal/pbnPrfExcelModal.tsx index 28dc882c3..427323c1e 100644 --- a/apps/wr/components/ui/modal/chkgd/pbnPrfExcelModal/pbnPrfExcelModal.tsx +++ b/apps/wr/components/ui/modal/chkgd/pbnPrfExcelModal/pbnPrfExcelModal.tsx @@ -1,29 +1,35 @@ -import { useWrStore } from "@/app/store"; import { excelToPbnPrfData } from "@/app/store/prfmncReg/pbnstPrfmncReg/excel"; import { pbnPrfRegExcelZod } from "@/app/store/prfmncReg/pbnstPrfmncReg/headList"; -import type { FcPrfType, PrfType } from "@/app/store/prfmncReg/pbnstPrfmncReg/type"; +import type { + FcPrfType, + PrfType, +} from "@/app/store/prfmncReg/pbnstPrfmncReg/type"; import { yearOptions } from "@/components/lib/func/commom"; -import { ToastFormUi } from "@/components/ui/toast/toastFromUi"; import { - FileUpload, FormField, FormUi, SelectBox, Form, Title, - type SelectDataType, filePicked, } from "@common/business_components/ui"; import { Button, Input } from "@common/components/ui"; import { zodResolver } from "@hookform/resolvers/zod"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; +import toast from "react-hot-toast"; import type { z } from "zod"; -export default function PbnPrfExcelModal({ data }: { data: FcPrfType }) { - const { prfmnRegData } = useWrStore((state) => state); - - const [files, setFiles] = useState(null); +export default function PbnPrfExcelModal({ + data, + setIsExcelOPen, + setModalExcelData, +}: { + data: FcPrfType; + setIsExcelOPen: (open: boolean) => void; + setModalExcelData: (excelData: PrfType) => void; +}) { + const [file, setFile] = useState(null); const [excelData, setExcelData] = useState(); const form = useForm>({ resolver: zodResolver(pbnPrfRegExcelZod), @@ -31,126 +37,152 @@ export default function PbnPrfExcelModal({ data }: { data: FcPrfType }) { ym: String(new Date().getFullYear()), }, }); - const onSubmit = (values: z.infer) => { - if (excelData) { - prfmnRegData(excelData as unknown as PrfType); + const handleFileChange = (e: React.ChangeEvent) => { + const selectedFile = e.target.files?.[0]; + if (selectedFile) { + setFile(selectedFile); } + }; + const onSubmit = (values: z.infer) => { + if (excelData) { + ////TODO: 궅이 서브밋을 써야하는 이유가 있는지 ? + console.log("values ", values); + // prfmnRegData(1,'41', excelData as unknown as PrfType); 활성화시 스토어 활성화 칠요 - + setModalExcelData(excelData as unknown as PrfType); + setIsExcelOPen(false); + } }; useEffect(() => { if (data && data.bscFctNm) - form.setValue("pbnstRuseFcltFk", data.pbnstRuseFcltPk); + form.setValue("pbnstRuseFcltId", data.pbnstRuseFcltId); form.setValue("bscFctNm", data.bscFctNm || ""); form.setValue("pbnstRuseFcltNm", data.ruseUtztnFcltNm); }, [data]); useEffect(() => { - console.log("excelData", excelData); - }, [excelData]); - - useEffect(() => { - if (files) { - const file = files[0]; + if (file) { + const isFile = file; if (form.getValues("ym").length < 1) { - ToastFormUi({ - body: "파일업로드 이전에 등록년도를 작성해주세요 ", - title: "지도점검 등록", - }); - setFiles(null); + toast.error("파일업로드 이전에 등록년도를 작성해주세요 "); + setFile(null); return; } - if (file) - filePicked(file, 0).then((res) => { + if (isFile) { + filePicked(isFile, 0).then((res) => { if (res !== undefined) - setExcelData(excelToPbnPrfData(res as unknown as JSON[], form.getValues("ym"),form.getValues('pbnstRuseFcltFk') ) as PrfType[]) + setExcelData( + excelToPbnPrfData( + res as unknown as JSON[], + form.getValues("ym"), + form.getValues("pbnstRuseFcltId") + ) as PrfType[] + ); }); + } } - }, [files]); + }, [file]); return ( <>
-
+
-
-
-

- {"점검정보 입력"} -

- -
+

+ {"점검정보 입력"} +

-
+
( field.onChange(val)} /> )} /> +
+
+ ( + + + + )} + /> +
+
+ ( + + + + )} + /> +
+
+
+ ( + + {/* { + if (files) setFiles(files); + }} + fileTypes="xlsx" + maxFiles={1} - ( - - - - )} - /> - ( - - - - )} - /> - ( - - { - if (files) setFiles(files); - }} - fileTypes="xlsx" - maxFiles={1} - > - - )} - /> + + > */} + +
+ { + console.log(" field. valus", field.value); + if (file) handleFileChange(file); + }} + accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" + /> +
+
+ )} + /> +
diff --git a/apps/wr/components/ui/modal/coMat/constCostFeeTable.tsx b/apps/wr/components/ui/modal/coMat/constCostFeeTable.tsx index efd47c69b..94f088d0e 100644 --- a/apps/wr/components/ui/modal/coMat/constCostFeeTable.tsx +++ b/apps/wr/components/ui/modal/coMat/constCostFeeTable.tsx @@ -5,25 +5,17 @@ import type { conCostFeeType, ContCostMap, } from "@/app/store/common/searchCode/type"; -import { yearOptions } from "@/components/lib/func/commom"; - -import { SelectBox, UiTable } from "@common/business_components/ui"; +import { UiTable } from "@common/business_components/ui"; import ListDiv from "@common/business_components/ui/list_div"; -import { Label, ScrollArea } from "@common/components/ui"; -import { useEffect, useRef, useState } from "react"; - -export function ConstCostFeeTable({ - setReturnData, -}: { - setReturnData: (data: any) => void; -}) { - const { coRvw, constCostRes, getConstCostFee } = useWrStore((state) => state); - const [year, setYear] = useState(String(new Date().getFullYear())); +import { Label, } from "@common/components/ui"; +import { useEffect, useState } from "react"; +export function ConstCostFeeTable() { + const { coRvw, constCostRes } = useWrStore((state) => state); const [constCostData, setConstCostData] = useState([]); - const getFctDescription = (fctCd: string) => { + const getFctDescription = (fctCd: string) => { const fctMap: { [key: string]: string } = { "001": "총인처리시설", "002": "염소소독", @@ -77,26 +69,19 @@ export function ConstCostFeeTable({ 공사비 산정표 보기
-
- - setYear(String(val))} - /> +
+ + {new Date().getFullYear() || ""}
- {}} - onPageChange={() => {}} - /> + { }} + /> ); diff --git a/apps/wr/components/ui/modal/coMat/drgtDetailModal.tsx b/apps/wr/components/ui/modal/coMat/drgtDetailModal.tsx index a6b05ca77..e36ce5589 100644 --- a/apps/wr/components/ui/modal/coMat/drgtDetailModal.tsx +++ b/apps/wr/components/ui/modal/coMat/drgtDetailModal.tsx @@ -1,48 +1,83 @@ "use client" import { btnClass } from "@/app/store/common/code" +import type { customDrgt } from "@/app/store/matDmd/drgtSttn" import { drgtDetailHeadListExport } from "@/app/store/matDmd/drgtSttn/headList" -import { ContentsDiv, Title, UiTable } from "@common/business_components/ui" +import { downloadExcel, Title, UiTable } from "@common/business_components/ui" import ListDiv from "@common/business_components/ui/list_div" -import { Button } from "@common/components/ui" +import { Button, ScrollArea } from "@common/components/ui" +import { useEffect, useState } from "react" + +export default function DrgtDetailModal({ + data, +}: { + data: customDrgt[]; +}) { + + // const [isSggNm, setIsSggNm] = useState('') + const [excelData, setExcelData] = useState(); + const excelClick = () => { + downloadExcel({ + data: excelData ?? [], + fileName: `우리동네가뭄 상세현황 - ${data[0]?.sggNm}`, + sheetNameArray: ["Sheet1"], + headerRange: "A1:C1" + }) + } + + useEffect(() => { + const resData = Array.isArray(data) ? data : [data]; + const exData = Array.isArray(resData) + ? resData.map((item: customDrgt) => { + return { + "년": item.year, + "월": item.month, + "가뭄상태": item.drgtState, // Access drgtState directly from the item + } + }) : []; + setExcelData(exData) + // if(data&&data[0].sggNm){ + ////TODO: data.sggNm [0[인지 data.sggNm 인지 사용하는지 확인필요 + // setIsSggNm(data[0].sggNm) + // } + }, [data]) -export default function DrgtDetailModal() { return ( <> - -
-
- - <div className="w-2/12 mx-5 flex flex-col items-center justify-center space-x-2 "> - <div className="flex flex-row space-x-2"> - <Button - size={"sm"} - className={`${btnClass} green`} - onClick={() => { }} - > - 다운로드 - </Button> - </div> - <div className="grid grid-cols-6 gap-4 "> - <div className="col-span-6"> - <ListDiv> - <UiTable - headName={[]} - tableData={[]} - headlist={drgtDetailHeadListExport} - pageSize={10} - total={10000} - cellClick={(index: number | undefined, headId: string) => { }} - onPageChange={() => { }} - ></UiTable> - </ListDiv> - </div> - </div> + <div className="grid grid-cols-4 gap-4 h-full"> + <div className="col-span-4"> + <div className="flex items-center justify-between"> + <Title title={`우리동네가뭄 상세내용 - ${data[0] && data[0].sggNm ? data[0].sggNm : ''}`} /> + <div className="flex flex-row space-x-2"> + <Button + size={"sm"} + className={`${btnClass} green mt-12`} + onClick={excelClick} + > + 다운로드 + </Button> + </div> + </div> + <hr className="mt-2 mb-2" /> + <div className="grid grid-cols-6 gap-4 "> + <div className="col-span-6"> + <ListDiv> + <ScrollArea className="h-[63vh]"> + <UiTable + headName={undefined} + tableData={data} + headlist={drgtDetailHeadListExport} + pageSize={0} + total={0} + cellClick={undefined} + onPageChange={() => { }} + ></UiTable> + </ScrollArea> + </ListDiv> </div> </div> </div> - - </ContentsDiv> + </div> </> ) } \ No newline at end of file diff --git a/apps/wr/components/ui/modal/coMat/rateFeeModal.tsx b/apps/wr/components/ui/modal/coMat/rateFeeModal.tsx index ee82420a8..f67a70b1a 100644 --- a/apps/wr/components/ui/modal/coMat/rateFeeModal.tsx +++ b/apps/wr/components/ui/modal/coMat/rateFeeModal.tsx @@ -2,20 +2,13 @@ import { useWrStore } from "@/app/store"; import type { rateFeeType } from "@/app/store/common/searchCode/type"; -import { yearOptions } from "@/components/lib/func/commom"; -import { SelectBox, UiTable } from "@common/business_components/ui"; +import { UiTable } from "@common/business_components/ui"; import ListDiv from "@common/business_components/ui/list_div"; -import { Input, Label, ScrollArea } from "@common/components/ui"; -import { Scroll } from "lucide-react"; +import { Label } from "@common/components/ui"; import { useEffect, useState } from "react"; -export function RateFeeTableModal({ - setReturnData, -}: { - setReturnData: (data: any) => void; -}) { - const { code, coRvw, rateFeeRes } = useWrStore((state) => state); - const [year, setYear] = useState<string>(String(new Date().getFullYear())); +export function RateFeeTableModal() { + const { coRvw, rateFeeRes } = useWrStore((state) => state); const [reateFeeTable, setRateFeeTable] = useState<rateFeeType[]>([]); const gubunLabelMap: Record<number, string> = { @@ -39,32 +32,32 @@ export function RateFeeTableModal({ }; const fetchData = async (rateFeeRes: rateFeeType[]) => { - const data = await rateFeeRes?.filter((item: rateFeeType) => { - return item.incExpCd !== "SS"; - }) - .reduce((acc: any, cur: rateFeeType) => { - const key = cur.stdCost; - let target = acc.find((item: any) => item.stdCost === key); - if (!target) { - target = { - stdCost: cur.stdCost, - BD: 0, - SE: 0, - CE: 0, - }; - acc.push(target); - } - target[cur.incExpCd] = cur.rt; - return acc; - }, []); - + const data = await rateFeeRes + ?.filter((item: rateFeeType) => { + return item.incExpCd !== "SS"; + }) + .reduce((acc: any, cur: rateFeeType) => { + const key = cur.stdCost; + let target = acc.find((item: any) => item.stdCost === key); + if (!target) { + target = { + stdCost: cur.stdCost, + BD: 0, + SE: 0, + CE: 0, + }; + acc.push(target); + } + target[cur.incExpCd] = cur.rt; + return acc; + }, []); const dataGubun = data?.map((item: any) => { return { ...item, gubun: gubunLabelMap[item.stdCost] || item.stdCost, }; - }) + }); return dataGubun?.sort((a: any, b: any) => a.stdCost - b.stdCost); }; @@ -87,27 +80,21 @@ export function RateFeeTableModal({ <hr className="col-span-6 border-gray-300" /> <div className="col-span-5 flex items-center"> - <Label className=" text-xl text-gray-600">기준년도</Label> - <Input - className="col-span-1 text-2xl text-right ml-2 w-32" - placeholder="" - type="text" - onChange={(e) => {}} - value={new Date().getFullYear() || ""} - /> + <Label className="mr-1 text-xl text-gray-600">기준년도</Label> + <span>{new Date().getFullYear() || ""}</span> </div> <div className="col-span-6 pr-6"> - <UiTable - className="min-w-full pr-12" - key={"rateTable" + reateFeeTable?.length} - headName={""} - tableData={reateFeeTable || []} - headlist={coRvw.rateFeeHeadList} - onPageChange={() => {}} - pageSize={0} - total={0} - cellClick={undefined} - /> + <UiTable + className="min-w-full pr-12" + key={"rateTable" + reateFeeTable?.length} + headName={""} + tableData={reateFeeTable || []} + headlist={coRvw.rateFeeHeadList} + onPageChange={() => {}} + pageSize={0} + total={0} + cellClick={undefined} + /> </div> </div> </ListDiv> diff --git a/apps/wr/components/ui/modal/coMat/ruseCaseMng/ruseCaseMngExcelModal.tsx b/apps/wr/components/ui/modal/coMat/ruseCaseMng/ruseCaseMngExcelModal.tsx new file mode 100644 index 000000000..57424996e --- /dev/null +++ b/apps/wr/components/ui/modal/coMat/ruseCaseMng/ruseCaseMngExcelModal.tsx @@ -0,0 +1,172 @@ +import { coRegExcel } from "@/app/store/matDmd/coSttn/headList"; +import type { ruseCaseMngExcelProps } from "@/app/store/matDmd/ruseCaseMng/index"; +import { reUseCaseNames } from "@/app/store/matDmd/ruseCaseMng/excel"; +import { filePicked, FileUpload, FormUi, Title } from "@common/business_components/ui"; +import { Form, FormField } from "@common/business_components/ui/form"; +import { Button } from "@common/components/ui"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import type { z } from "zod"; +import { useWrStore } from "@/app/store"; + +export default function RuseCaseMngExcel() { + const { insertRuseCaseMng } = useWrStore((state) => state); + const form = useForm<z.infer<typeof coRegExcel>>({ + resolver: zodResolver(coRegExcel), + }); + + const [file, setFiles] = useState<File | undefined>(); + + const onSubmit = () => { + if (file) { + filePicked(file, 0).then((res: unknown) => { + try { + const newArray: ruseCaseMngExcelProps[] = [] + const dataArray = Array.isArray(res) ? res : []; + dataArray.forEach((item: Record<string, unknown>) => { + const object: ruseCaseMngExcelProps = { + ruseBizNm: '', + bizBgngDate: '', + bizEndDate: '', + splyFcltKnd: '', + splyFcltNm: '', + splyFcltCpct: 0, + dmdFcltNm: '', + dmdFcltAddr: '', + ruseFcltCpct: 0, + ruseFcltPplLen: '', + ruseFcltUsg: '', + ruseFcltSplyCpct: 0, + ruseFcltPrcsMsrc: '', + grnteWqUsg: '', + grnteWqGrssClfg: 0, + grnteWqCombResdch: 0, + grnteWqTrbt: '', + grnteWqBod: '', + grnteWqSmll: '', + grnteWqCrmty: '', + grnteWqTn: '', + grnteWqTp: '', + grnteWqPh: '', + grnteWqClIonQnt: '', + grnteWqCndctStat: '', + grnteWqRmrk: '', + reqBizReuseFcltCst: 0, + reqBizPplCst: 0, + reqBizArchCst: 0, + reqBizMchnCst: 0, + reqBizMtrngCst: 0, + reqBizGisCst: 0, + reqBizCctvCst: 0, + reqBizCmcnCst: 0, + reqBizWstCst: 0, + reqBizDesignCst: 0, + reqBizSpvsCst: 0, + reqBizSbsdCst: 0, + reqBizEtc: '', + reqBizGrssCst: 0, + prcmPlanNlep: 0, + prcmPlanLcep: 0, + prcmPlanBrdnCst: 0, + prcmPlanEcqrRt: 0, + prcmPlanGrssCst: 0, + clclnNlep: 0, + clclnLcep: 0, + clclnBrdnCst: 0, + clclnEcqrRt: 0, + clclnGrssCst: 0, + }; + + Object.keys(reUseCaseNames).forEach((key: string) => { + if (item && reUseCaseNames[key] !== undefined) { + const mappedKey = reUseCaseNames[key]; + if (mappedKey && object.hasOwnProperty(mappedKey)) { + let value; + + if(value === "-"){ + value = ''; + } + if(key === "사업시작일" || key === "사업종료일"){ + if(item[key]?.toString().split(".")[1]?.length === 1){ + value = `${item[key]?.toString().split(".")[0] || ""}${item[key]?.toString().split(".")[1]?.length === 1 ? "0" : ""}${item[key]?.toString().split(".")[1] || ""}` + }else{ + value = `${item[key]?.toString().split(".")[0] || ""}${item[key]?.toString().split(".")[1] || ""}` + } + }else if(key === "재이용시설_관로(km)" || key?.toString().includes("보증수질_")){ + value = item[key]?.toString().trim() || ''; + }else{ + if(item[key]?.toString().includes("%") && Number(item[key]?.toString().replaceAll("%", ""))){ + value = item[key]?.toString().replaceAll("%", ""); + }else{ + value = item[key]; + } + } + + if(value === null || value?.toString().trim() === "-" || value?.toString().trim() === ""){ + if (typeof object[mappedKey as keyof ruseCaseMngExcelProps] === 'number') { + (object as unknown as Record<string, unknown>)[mappedKey] = ''; + } else { + (object as unknown as Record<string, unknown>)[mappedKey] = ''; + } + }else if (typeof object[mappedKey as keyof ruseCaseMngExcelProps] === 'number' && Number(value?.toString().replaceAll("%", "").replaceAll(",", ""))) { + (object as unknown as Record<string, unknown>)[mappedKey] = Number(value?.toString().replaceAll("%", "").replaceAll(",", "")); + }else{ + (object as unknown as Record<string, unknown>)[mappedKey] = value?.toString().trim() || ''; + } + } + } + }); + newArray.push(object); + }); + console.log("newArray : ", newArray); + insertRuseCaseMng(newArray); + } catch (e) { + console.log(e) + } + }); + }; + } + + return ( + <> + <div> + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)}> + <div> + <Title title={"물재이용 사례 등록"}> +
+
+

+ {"물재이용 사례 파일"} +

+ +
+ +
+ ( + + { + console.log("files", files) + if (files && files?.length === 1) setFiles(files[0]); + }} + fileTypes="" + maxFiles={1} + > + + )} + /> +
+ + +
+ + ); +} \ No newline at end of file diff --git a/apps/wr/components/ui/modal/coReg/coRegExcel/coRegExcelModal.tsx b/apps/wr/components/ui/modal/coReg/coRegExcel/coRegExcelModal.tsx index 1fa8fa8f3..1e51fb11c 100644 --- a/apps/wr/components/ui/modal/coReg/coRegExcel/coRegExcelModal.tsx +++ b/apps/wr/components/ui/modal/coReg/coRegExcel/coRegExcelModal.tsx @@ -1,107 +1,114 @@ import { useWrStore } from "@/app/store"; import { coRegExcel } from "@/app/store/matDmd/coSttn/headList"; -import type { customLdrmnt } from "@/app/store/matDmd/ldrmntCoSttn"; -import { excelToLdrmntData, names } from "@/app/store/matDmd/ldrmntCoSttn/excel"; -import { filePicked, FileUpload, FormUi, Title } from "@common/business_components/ui"; -import { Form, FormField } from "@common/business_components/ui/form"; +import { names } from "@/app/store/matDmd/ldrmntCoSttn/excel"; +import { filePicked, Title } from "@common/business_components/ui"; +import { Form } from "@common/business_components/ui/form"; +import LoadingOverlay from "@common/business_components/ui/loadingBar"; import { Button, Input } from "@common/components/ui"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { useForm } from "react-hook-form"; +import toast from "react-hot-toast"; import type { z } from "zod"; -export default function CoRegExcel() { - - +export default function CoRegExcel({ + isModal, +}: { + isModal: (open: boolean) => void; +}) { + const { ldrmnt, loadingOpen } = useWrStore((state) => state); + const { isLoading } = ldrmnt; const form = useForm>({ resolver: zodResolver(coRegExcel), }); - const { ldrmntRegData, ldrmntDelete } = useWrStore((state) => state); - const [excelData, setExcelData] = useState(); - + const { ldrmntRegData } = useWrStore((state) => state); const [file, setFiles] = useState(); - const onSubmit = (values: z.infer) => { - ldrmntDelete(); + // const handleFileChange = (e: React.ChangeEvent) => { + // const selectedFile = e.target.files?.[0]; + // if (selectedFile) { + // setFiles(selectedFile); + // } + // }; + + //TODO:Onsubmit을 써야하는지 + const onSubmit = () => { + loadingOpen(true); if (file) { - filePicked(file, 0).then((res) => { - try { - console.log("res", res) - let array: { [key: string]: string | number }[] = [] - if (Array.isArray(res)) - res.forEach((item, index) => { - let object: { [key: string]: string | number } = {} - Object.keys(names).forEach((key) => { - if (item && names[key] !== undefined) { - if (key === "공장관리번호") - item[key] = String(item[key]) - object[names[key] as string] = item[key]; - } + filePicked(file, 0) + .then(async (res) => { + try { + const array: { [key: string]: string | number }[] = []; + if (Array.isArray(res)) { + res.some((item) => { + const object: { [key: string]: string | number } = {}; + Object.keys(names).some((key) => { + if (item && names[key] !== undefined) { + // if (key === "공장관리번호") { + // item[key] = String(item[key]) + // } else { + object[names[key] as string] = item[key]; + // } + } + return false; + }); - }) - array.push(object) + array.push(object); - if (index != 0 && index % 5000 == 0) { - ldrmntRegData(array); - console.log("array", array) - array = [] - array.length = 0; - // throw new Error("error") - } - }) - ldrmntRegData(array); - console.log("array끝", array) - } catch (e) { - console.log(e) - } + return false; + }); + } + await ldrmntRegData(array); + isModal(false); + loadingOpen(false); + toast.success(`잠재수요처 등록이 완료되었습니다.`); + } catch (e) { + console.log(e); + loadingOpen(false); + alert(e); + return true; + } + }) + .catch((e) => { + console.log("file", e); + }); + } + }; - }); - }; - } + return ( + <> +
+ +
+ +
+ +
+
+

+ {"잠재수요처 파일"} +

+ +
- - return ( - <> -
- - -
- -
-
-

- {"잠재수요처 파일"} -

- - -
- -
- - ( - - { - console.log("files", files) - if (files && files?.length === 1) setFiles(files[0]); - }} - fileTypes="" - maxFiles={1} - > - - )} +
+
+ { + const selectedFile = e.target.files?.[0]; + if (selectedFile) setFiles(selectedFile); + }} + accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" />
- - -
- - ); - } +
+ + +
+ + ); +} diff --git a/apps/wr/components/ui/modal/fcltReg/bdstChgModal/bdstChgModal.tsx b/apps/wr/components/ui/modal/fcltReg/bdstChgModal/bdstChgModal.tsx index 03c257c59..09f66ed2a 100644 --- a/apps/wr/components/ui/modal/fcltReg/bdstChgModal/bdstChgModal.tsx +++ b/apps/wr/components/ui/modal/fcltReg/bdstChgModal/bdstChgModal.tsx @@ -1,8 +1,6 @@ import { useWrStore } from "@/app/store"; import { UiTable } from "@common/business_components/ui"; import ListDiv from "@common/business_components/ui/list_div"; -import { useEffect } from "react"; - import type { @@ -10,11 +8,10 @@ import type { } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; export default function BdstChgModal({ chgBuilding }: { chgBuilding:chgBuilding[] }) { -const {bdst,rain} = useWrStore((state) => { +const {bdst} = useWrStore((state) => { return state; }); const { bdstChgHeadList } = bdst; - console.log("chgBuilding", chgBuilding); return ( @@ -27,7 +24,6 @@ const { bdstChgHeadList } = bdst; onPageChange={() => {}} headName={"○건축물 변경 이력"} tableData={chgBuilding} - children={undefined} cellClick={undefined} /> diff --git a/apps/wr/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal.tsx b/apps/wr/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal.tsx index 315f9397f..d1de75ba1 100644 --- a/apps/wr/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal.tsx +++ b/apps/wr/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal.tsx @@ -1,98 +1,131 @@ "use client"; import { useWrStore } from "@/app/store"; +import type { customHtdflBdst } from "@/app/store/ruseFcltMng/fcltReg/htdflReg"; import { selectClass, type customRainBdst, } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; -import { ScrollArea, SelectBox, UiTable, type SelectDataType } from "@common/business_components"; -import { Button, Input, Label } from "@common/components"; -import { useEffect, useState } from "react"; -import { getCity, tableData } from "@/components/lib/func/commom"; import type { customRswtBdst } from "@/app/store/ruseFcltMng/fcltReg/rswtReg"; -import type { customHtdflBdst } from "@/app/store/ruseFcltMng/fcltReg/htdflReg"; +import { tableData } from "@/components/lib/func/commom"; +import HtdflRegForm from "@/components/ui/form/fcltReg/htdflReg/htdflRegForm"; import RainRegForm from "@/components/ui/form/fcltReg/rainReg/rainRegForm"; import RswtrRegForm from "@/components/ui/form/fcltReg/rswtrReg/rswtrRegForm"; -import HtdflRegForm from "@/components/ui/form/fcltReg/htdflReg/htdflRegForm"; +import { + ScrollArea, + SelectBox, + UiTable, + type SelectDataType, +} from "@common/business_components"; +import { Button, Input, Label } from "@common/components"; +import { useEffect, useState } from "react"; export type objSearchType = { - sgg: string|number; - city: string|number; - }; + sgg: string | number; + city: string | number; +}; //RNB(빗물), RST(중수도), HTB(온배수) - type MoType = 'RNB' | 'RST' | 'HTB'; // 여기에 'htdf' 추가 +type MoType = "RNB" | "RST" | "HTB"; // 여기에 'htdf' 추가 - type BdstTypeMap = { +type BdstTypeMap = { RNB: customRainBdst; RST: customRswtBdst; - HTB: customRswtBdst; // 해야해// 아직 아노딤 + HTB: customRswtBdst; // 해야해// 아직 아노딤 }; - interface BdstRegProps { +interface BdstRegProps { moType: T; + isModal: boolean; + setIsModal?: (open: boolean) => void; } -export function BdstReg({ moType }: BdstRegProps) { - const { rain, code, bdst,findBdst } = useWrStore((state) => { +export function BdstReg({ + moType, + setIsModal, + isModal, +}: BdstRegProps) { + const { code, bdst, findBdst } = useWrStore((state) => { return state; }); + + const { userSiSggCd } = code; + const [bdstTable, setbdstTable] = useState(); - const [rainBdstForm, setRainBdstForm] = useState({} as BdstTypeMap[T]); - const [searchTxt, setSearchTxt] = useState() - const [selectSgg, setSelectSgg] = useState() - const [objSearch, setObjSearch] = useState({sgg:"",city:""}); + const [rainBdstForm, setRainBdstForm] = useState( + {} as BdstTypeMap[T] + ); + const [searchTxt, setSearchTxt] = useState(); + const [objSearch, setObjSearch] = useState({ + sgg: "", + city: "", + }); + const [useCityCd, setUserCityCd] = useState([]); + const [useSggCd, setUserSggCd] = useState([]); - const getSelCity = (val: string | number) => { - const cityValue = String(val); // Ensure val is treated as a string - const data = getCity(cityValue, code.sggCd); - setSelectSgg(data); - getSgg(data[0]?.val || ""); - setObjSearch({ city: cityValue, sgg: '' }); + const searchBdst = (searchTxt: string | undefined) => { + let data = objSearch.city; + + if (objSearch.sgg !== "" && objSearch.sgg !== "0") { + data = objSearch.sgg; + } + //RNB(빗물), RST(중수도), HTB(온배수) + findBdst(String(data), searchTxt || "", moType); }; - const getSgg = (val: string | number) => { - console.log(val); - setObjSearch({...objSearch,sgg: String(val)}) - }; - - const searchBdst=(searchTxt: string | undefined)=>{ - let data =objSearch.city; - if(objSearch.city==="" ){ - - data = String(code.cityCd[0]?.val) - } - if(objSearch.sgg !=="" ){ - data = objSearch.sgg - } - - //RNB(빗물), RST(중수도), HTB(온배수) - findBdst(String(data), searchTxt || "",moType ) - - } - useEffect(() => { if (code.sggCd.length > 0) { - - setbdstTable( tableData(bdst.bdstBase, { code })); - } - }, [ bdst]); + const bdstData = bdst.bdstBase.map((bdst) => { + const archArea = + bdst?.archArea.split(".")[0] !== "-1" + ? bdst?.archArea.split(".")[0] + : "확인불가"; + return { + ...bdst, + archArea: archArea, + }; + }); + setbdstTable(tableData(bdstData, { code })); + } + }, [bdst]); + useEffect(() => { + if (userSiSggCd.length === 2 && code.cityCd.length > 0) { + const city = code.cityCd.filter((item) => item.val === userSiSggCd); + const sgg = code.sggCd.filter( + (item) => item.val.slice(0, 2) === userSiSggCd + ); + sgg.unshift({ text: "전체", val: "0" }); + setUserCityCd(city); // Fix: Ensure city is of type SelectDataType[] + setObjSearch({ ...objSearch, city: String(city[0]?.val) }); + setUserSggCd(sgg); + } else if ( + userSiSggCd.length === 5 && + code.cityCd.length > 0 && + code.sggCd.length > 0 + ) { + const city = code.cityCd.filter( + (item) => item.val === userSiSggCd.slice(0, 2) + ); + setUserCityCd(city); + const sgg = code.sggCd.filter((item) => item.val === userSiSggCd); + setUserSggCd(sgg); + setObjSearch({ city: String(city[0]?.val), sgg: String(sgg[0]?.val) }); + } + }, [userSiSggCd, code.cityCd, code.sggCd]); - const tableClick: (val: number | undefined , headId:string) => void = (val ,headId) => { - - if(val!==undefined && bdst?.bdstBase.length>0){ - const data:BdstTypeMap[T] =bdst?.bdstBase[val] - if (data) { - setRainBdstForm(data); - } - - + const tableClick: (val: number | undefined, headId: string) => void = ( + val + ) => { + if (val !== undefined && bdst?.bdstBase.length > 0) { + const data: BdstTypeMap[T] = bdst?.bdstBase[val]; + if (data) { + setRainBdstForm(data); } + } + }; - } - - useEffect(()=>{ - setbdstTable([]) // 초기화 작업 - },[]) + useEffect(() => { + setbdstTable([]); // 초기화 작업 + }, []); return ( <>
@@ -100,28 +133,52 @@ export function BdstReg({ moType }: BdstRegProps) { className="col-span-3 h-full" label={"시도"} selectClass={selectClass} - selectArray={code.cityCd || []} - onSelectValue={getSelCity} - > - {""} - + selectArray={useCityCd} + onSelectValue={(val) => { + setObjSearch({ ...objSearch, city: String(val) }); + }} + value={objSearch.city || useCityCd[0]?.val} + > { + if (val) setObjSearch({ ...objSearch, sgg: String(val) }); + }} + value={objSearch.sgg || useSggCd[0]?.val} > {" "} - - setSearchTxt(e.target.value)}/> - + + setSearchTxt(e.target.value)} + /> +
-

건축물 목록

+

+ 건축물 목록 + + (유의 사항: 신규등록하려는 건축물이 목록에 없을 경우 하단 건축물 + 정보에 추가하여 주시기 바랍니다.) + +

({ moType }: BdstRegProps) { pageSize={-10} total={-1} headName={undefined} - cellClick={(idx, headId) => (tableClick(idx, headId || ""))} + cellClick={(idx, headId) => tableClick(idx, headId || "")} onPageChange={function (page: number): void { + console.log("page ", page); throw new Error("Function not implemented."); - }}> - + }} + >
- -
- { moType === 'RNB'? - - :moType==='RST' - ? - : - } - -
-
+ {/* */} +
+ {moType === "RNB" ? ( + + ) : moType === "RST" ? ( + { + if (setIsModal && open) { + setIsModal(open); + } + }} + > + ) : ( + { + if (setIsModal && open) { + setIsModal(open); + } + }} + > + )} +
+ {/*
*/}
); diff --git a/apps/wr/components/ui/modal/fcltReg/fcOrgwtsRegModal/fcOrgwtsRegModal.tsx b/apps/wr/components/ui/modal/fcltReg/fcOrgwtsRegModal/fcOrgwtsRegModal.tsx index 92b40e146..cc38b1f30 100644 --- a/apps/wr/components/ui/modal/fcltReg/fcOrgwtsRegModal/fcOrgwtsRegModal.tsx +++ b/apps/wr/components/ui/modal/fcltReg/fcOrgwtsRegModal/fcOrgwtsRegModal.tsx @@ -1,148 +1,216 @@ "use client"; import { useWrStore } from "@/app/store"; -import type { pubHasuType, fcBizType, fcOrgwtType,} from "@/app/store/ruseFcltMng/fcltReg/pbnstReg/type"; -import type { TableUpperProps } from "@/app/store/common/type/header"; +import type { + pubHasuType, + fcOrgwtType, +} from "@/app/store/ruseFcltMng/fcltReg/pbnstReg/type"; import { tableData } from "@/components/lib/func/commom"; import RuseMngTable from "@/components/ui/table/pbnstReg/ruseMngTable/ruseMngTable"; -import { Title, UiTable } from "@common/business_components/ui"; +import { Title } from "@common/business_components/ui"; import { Label } from "@common/business_components/ui/form/lib/label"; import { Button } from "@common/business_components/ui/table/lib/button"; import { Input } from "@common/components/ui"; import { ArrowLeft, ArrowRight } from "lucide-react"; import { useEffect, useRef, useState } from "react"; +import toast from "react-hot-toast"; -export default function FcOrgwtsRegModal({ onRightSet, closeModal }: { onRightSet?: (rightData:fcOrgwtType[]) => void,closeModal?:(val:boolean) =>void }) { - const { code, pbnst, getSwtrmtFcltNm, searchFclts } = useWrStore((state) => state); +export default function FcOrgwtsRegModal({ + onRightSet, + closeModal, + initTableData, +}: { + onRightSet?: (rightData: fcOrgwtType[]) => void; + closeModal?: (val: boolean) => void; + initTableData?: any[]; +}) { + const { code, pbnst, getSwtrmtFcltNm, searchFclts } = useWrStore( + (state) => state + ); const { fcOrgwtsHeadList, pbnstHasuRes } = pbnst; const [bdstTable, setbdstTable] = useState(); const rightRef = useRef([] as pubHasuType[]); const leftRef = useRef([] as pubHasuType[]); const [rightData, setRightData] = useState(); const [inputVal, setInputVal] = useState(""); - - useEffect(()=>{ - if(Array.isArray(searchFclts)){ + const saveData = () => { + if (rightData && rightData?.length > 0) { + if (onRightSet) + onRightSet( + (rightData as pubHasuType[])?.map((item) => { + return { + ...item, + swtrmtFcltId: item.bscFctCd, // 임시값, 실제 데이터에 맞게 수정 + orgwtWtritQnt: Number(item.fctCpc), // 임시값, 실제 데이터에 맞게 수정 + tmsYn: "N", // 임시값, 실제 데이터에 맞게 수정 + dschgFmYn: "N", // 임시값, 실제 데이터에 맞게 수정 + dtlPstnExpln: item.addr, // 임시값, 실제 데이터에 맞게 수정 + choose: false, // 임시값, 실제 데이터에 맞게 수정 + } + // 필요한 경우 다른 속성들도 추가 + }) as unknown as fcOrgwtType[] + ); + + closeModal?.(false) + } + else { + toast.error("취수하수처리장 목록을 선택해주세요.") + } + } + + useEffect(() => { + if (Array.isArray(searchFclts)) { const filteredData = searchFclts.filter((item) => { - if(rightData){ - if(rightData.find(rightItem =>rightItem.bscFctCd===item.bscFctCd)) + if (rightData) { + if ( + rightData.find((rightItem) => rightItem.bscFctCd === item.bscFctCd) + ) return false; } - return true - }) - setbdstTable(Array.isArray(filteredData) ? [...filteredData] : []) + return true; + }); + setbdstTable(Array.isArray(filteredData) ? [...filteredData] : []); } - - }, [searchFclts]) + }, [searchFclts]); useEffect(() => { if (code.sggCd.length > 0 && pbnstHasuRes.length > 0) { - setbdstTable(tableData(pbnstHasuRes, { code })); - - console.log('bdstTable ', bdstTable) + const data = pbnstHasuRes.filter((item) => !initTableData?.find((initItem) => initItem.ruseUtztnFcltNm === item.bscFctNm)) + leftRef.current?.filter((item) => !initTableData?.find((initItem) => initItem.ruseUtztnFcltNm === item.bscFctNm)); + setbdstTable(tableData(data, { code })); } - }, [code, pbnstHasuRes]); + }, [code, pbnstHasuRes, initTableData]); + useEffect(() => { - console.log('rightData ', rightData) - if(onRightSet) - onRightSet( - (rightData as pubHasuType[])?.map(item => ({ - swtrmtFcltFk: item.bscFctCd, // 임시값, 실제 데이터에 맞게 수정 - orgwtWtritQnt: Number(item.fctCpc), // 임시값, 실제 데이터에 맞게 수정 - tmsYn: "N", // 임시값, 실제 데이터에 맞게 수정 - dschgFmYn: "N", // 임시값, 실제 데이터에 맞게 수정 - dtlPstnExpln: item.addr, // 임시값, 실제 데이터에 맞게 수정 - choose: false, // 임시값, 실제 데이터에 맞게 수정 - // 필요한 경우 다른 속성들도 추가 - })) as unknown as fcOrgwtType[] - ); - }, [rightData]) + if (initTableData && Array.isArray(initTableData)) { + setRightData(initTableData.map(item => ({ + ...item, + choose: item.choose, + sido: item.dtlPstnExpln?.split(" ")[0] || "", + gugun: item.dtlPstnExpln?.split(" ")[1] || "", + bscFctNm: item.ruseUtztnFcltNm, + addr: item.dtlPstnExpln, + orgwtWtritQnt: String(item.orgwtWtritQnt), + fctCpc: String(item.orgwtWtritQnt), + swtrmtFcltId: item.swtrmtFcltId, + }))); + rightRef.current = initTableData.map(item => ({ ...item })); + } + }, []); return ( - <> -
-
- - <Button className="m-2" size={"sm"} - onClick={() => {closeModal && closeModal(false) }}>저장</Button> - </div> + <div className="flex flex-col h-[80vh]"> + <div className="shrink-0 p-4 border-b z-10 bg-white"> + <div className="flex items-center"> + <Title title="취수하수처리장 추가" /> + + <div className="flex items-center gap-2 mt-8"> - <div className="flex flex-row flex-grow gap-4 mt-4"> - <div className="w-3/7"> - <div className="flex items-center justify-between"> - <Label className="w-full text-2xl text-gray-600 text-left">공공하수처리시설 목록</Label> - <div className="flex items-center"> - <Label className="w-full text-xl text-gray-600 text-right">시설명</Label> - <Input className="w-1/2 !text-xl ml-2" type="text" value={inputVal} onChange={(e) => {setInputVal(e.target.value)}} /> - <Button className="m-2" size={"sm"} onClick={() => getSwtrmtFcltNm(inputVal)}>조회</Button> - </div> - </div> - <div className="border-t border-gray-300 my-4"></div> - <RuseMngTable - tableData={bdstTable || []} - headList={fcOrgwtsHeadList} - upHeadList={[]} // TODO: Replace with actual upHeadList data if available - ref={leftRef} + + <Label className="text-xl text-gray-600">시설명</Label> + <Input + className="w-48 text-xl" + type="text" + value={inputVal} + onChange={(e) => setInputVal(e.target.value)} /> - </div> - <div className="flex flex-col items-center justify-center w-1/7 gap-2"> - <ArrowRight - type="button" - onClick={() => { - if (leftRef && typeof leftRef === "object" && "current" in leftRef) { - const checked = leftRef.current?.filter((item) => ((item as unknown as pubHasuType)?.choose)) - const unchecked = leftRef.current?.filter((item) => !(item as unknown as pubHasuType)?.choose) - if (checked && checked.length > 0) { - checked.forEach(item=>(item as unknown as pubHasuType).choose = false) - setRightData([...(checked as unknown as pubHasuType[]), - ...((rightRef.current ?? []) as unknown as pubHasuType[]) - ]) - - } - if (unchecked && unchecked.length > 0) { - setbdstTable([...(unchecked as unknown as pubHasuType[])]) - }else{ - setbdstTable([]) - } - } + <Button size="sm" onClick={() => { + + if(inputVal){ + + getSwtrmtFcltNm(inputVal) + }else{ + const filterData = pbnstHasuRes.filter((item) => ! rightData?.find(( right) => right.bscFctNm ===item.bscFctNm)) + setbdstTable(tableData(filterData, { code })); + } } - className="w-12 h-12 text-gray-500 cursor-pointer" - /> - <ArrowLeft - type="button" - onClick={() => { - if (rightRef && typeof rightRef === "object" && "current" in rightRef) { - const checked = rightRef.current?.filter((item) => ((item as unknown as pubHasuType)?.choose)) - const unchecked = rightRef.current?.filter((item) => !(item as unknown as pubHasuType)?.choose) - if (checked && checked.length > 0) { - checked.forEach(item=>(item as unknown as pubHasuType).choose = false) - setbdstTable([ - ...(checked as unknown as pubHasuType[]), - ...((leftRef.current ?? []) as unknown as pubHasuType[]) - ]) - } - if (unchecked && unchecked.length > 0) { - setRightData([...(unchecked as unknown as pubHasuType[])]) - }else{ - setRightData([]) - } - } - }} - className="w-12 h-12 text-gray-500 cursor-pointer mt-5" /> - </div> - {/* Right Table container: Takes 2/5 of the width */} - <div className="w-3/7"> - <Label className="w-full text-2xl text-gray-600 text-right">취수하수처리장 목록</Label> - <div className="border-t border-gray-300 my-4"></div> - <RuseMngTable - tableData={rightData || []} - headList={fcOrgwtsHeadList} - upHeadList={[]} // TODO: Replace with actual upHeadList data if available - ref={rightRef} - /> + + }> + 조회 + </Button> + <Button size="sm" onClick={saveData}>저장</Button> </div> </div> </div> - </> + {/* <div className="flex items-center justify-between"> + <div className="flex items-center gap-2"> + <Label className="text-xl text-gray-600">시설명</Label> + <Input + className="w-48 text-xl" + type="text" + value={inputVal} + onChange={(e) => setInputVal(e.target.value)} + /> + <Button size="sm" onClick={() => getSwtrmtFcltNm(inputVal)}>조회</Button> + </div> + </div> + </div> */} + + {/* 테이블 영역 */} + <div className="flex flex-row gap-4 flex-grow p-4 overflow-x-auto"> + {/* 왼쪽 테이블 */} + + <div className="w-5/12 min-w-[400px]"> + <Label className="text-2xl text-gray-600 mb-2 block text-right"> + 공공하수처리시설 목록 + </Label> + <RuseMngTable + tableData={bdstTable || []} + headList={fcOrgwtsHeadList} + upHeadList={[]} + ref={leftRef} + /> + </div> + + {/* 중간 이동 버튼 */} + <div className="flex flex-col justify-center items-center gap-4 w-[200px] min-w-[120px]"> + <ArrowRight + onClick={() => { + const checked = leftRef.current?.filter((item) => item.choose); + const unchecked = leftRef.current?.filter((item) => !item.choose); + if (checked?.length) { + checked.forEach((item) => (item.choose = false)); + setRightData([ + ...(checked as pubHasuType[]), + ...(rightRef.current ?? []), + ]); + } + setbdstTable(unchecked ?? []); + }} + className="w-8 h-8 text-gray-500 cursor-pointer" + /> + <ArrowLeft + onClick={() => { + const checked = rightRef.current?.filter((item) => item.choose); + const unchecked = rightRef.current?.filter( + (item) => !item.choose + ); + if (checked?.length) { + checked.forEach((item) => (item.choose = false)); + setbdstTable([ + ...(checked as pubHasuType[]), + ...(leftRef.current ?? []), + ]); + } + setRightData(unchecked ?? []); + }} + className="w-8 h-8 text-gray-500 cursor-pointer" + /> + </div> + + {/* 오른쪽 테이블 */} + <div className="w-5/12 min-w-[400px]"> + <Label className="text-2xl text-gray-600 mb-2 block text-right"> + 취수하수처리장 목록 + </Label> + <RuseMngTable + tableData={rightData || []} + headList={fcOrgwtsHeadList} + upHeadList={[]} + ref={rightRef} + /> + </div> + </div> + </div> ); } diff --git a/apps/wr/components/ui/modal/fcltReg/pbnstRegModal/pbnstRegModal.tsx b/apps/wr/components/ui/modal/fcltReg/pbnstRegModal/pbnstRegModal.tsx index b930a36bb..c0cbdda30 100644 --- a/apps/wr/components/ui/modal/fcltReg/pbnstRegModal/pbnstRegModal.tsx +++ b/apps/wr/components/ui/modal/fcltReg/pbnstRegModal/pbnstRegModal.tsx @@ -1,9 +1,8 @@ "use client"; -import { useWrStore } from "@/app/store"; import { + pbnstRegZod, type customPbnst, type PbnstRegArrrayType, - type pubHasuType, } from "@/app/store/ruseFcltMng/fcltReg/pbnstReg/type"; import { PbnstMngForm } from "@/components/ui/form/fcltReg/pbnstReg/mngReg/mngRegForm"; import { ScrollArea } from "@common/business_components"; @@ -16,59 +15,56 @@ import { } from "@common/components/ui"; import { useEffect, useState } from "react"; import { PbnstBuseForm } from "@/components/ui/form/fcltReg/pbnstReg/buseReg/bugRegForm"; -import { useFieldArray, type UseFormReturn } from "react-hook-form"; +import { type FieldErrors, type UseFormReturn } from "react-hook-form"; import { writeRgtrAndMdfr } from "@/app/store/ruseFcltMng/fcltReg/pbnstReg"; +import type { z } from "zod"; +import { confirmToast } from "@/components/lib/func/commom"; +import { useTranslation } from "react-i18next"; export function PbnstRegModal({ - data, form, + setPbnstInsert, + onInvalid, }: { - data: pubHasuType; form: UseFormReturn<{ items: PbnstRegArrrayType }>; + setPbnstInsert: (data: customPbnst) => void; + onInvalid: ( + errors: FieldErrors<{ items: z.infer<typeof pbnstRegZod>[] }> + ) => void; }) { - const { pbnst, pbnstInsert } = useWrStore((state) => { - return state; - }); - const [pbnstBdstForm, setPbnstBdstForm] = useState<customPbnst>( - {} as customPbnst - ); - const [currentTab, setCurrentTab] = useState<string>("mng"); + const { t } = useTranslation("base"); + + //TODO: 유즈 스테이트에 set만해도 되나? + const [, setCurrentTab] = useState<string>("mng"); const [indexMng, setIndexMng] = useState<number>(0); const [indexBiz, setIndexBiz] = useState<number>(0); - const onSubmit = async (valuse: { items: PbnstRegArrrayType }) => { - - const item = valuse.items[indexMng]; - const data = item ? writeRgtrAndMdfr([item]) : []; - if(data && data[0]){ - pbnstInsert(data[0]) + const onSubmit = async (valuse: { items: PbnstRegArrrayType }) => { + const item = valuse.items[indexMng]; + const data = item ? writeRgtrAndMdfr([item]) : []; + // + const handleConfirm = () => { + if (data && data[0]) { + setPbnstInsert(data[0]); } }; + confirmToast(t("isInsertMsg"), handleConfirm); + }; const handleTabChange = (value: string) => { setCurrentTab(value); - }; - - useEffect(() => { - console.log("form errors:", form.formState.errors); - }, [form.formState.errors]); - - + }; useEffect(() => { - if (data) { - - const firstForm = form.getValues("items"); - setIndexMng(firstForm.length - 1) } - }, [data, form]); - - + console.log("form errors:", form.formState.errors); + }, [form.formState.errors]); useEffect(() => { - console.log(' form.getValues() ' ,form.getValues()) - - }, [form.getValues()]); - + const firstForm = form.getValues("items"); + if (firstForm) { + setIndexMng(firstForm.length - 1); + } + }, [form]); return ( <> <h1 className="text-2xl font-medium text-label pl-8"> @@ -76,8 +72,9 @@ export function PbnstRegModal({ </h1> <div className="border-t border-gray-300 my-4"></div> - <ScrollArea className="h-[75vh]"> - <Tabs defaultValue="mng" onValueChange={handleTabChange}> + + <Tabs defaultValue="mng" onValueChange={handleTabChange}> + <div className="flex justify-between items-center"> <TabsList className=""> <TabsTrigger className="!text-xl" value="mng"> 시설정보 @@ -85,35 +82,37 @@ export function PbnstRegModal({ <TabsTrigger className="!text-xl" value="buse"> 차수별정보 </TabsTrigger> - <Button - onClick={ form.handleSubmit(onSubmit)} - size={"sm"} - className="!justify-end" - > - 저장 - </Button> + </TabsList> + <Button + onClick={form.handleSubmit(onSubmit, onInvalid)} + size={"sm"} + className="mr-5 mt-2" + > + 저장 + </Button> + </div> + <ScrollArea className="h-[75vh]"> <TabsContent className="max-h-full" value="mng"> <PbnstMngForm form={form} formType={"reg"} - firstFcltData={pbnstBdstForm} indexMng={indexMng} setIndexMng={setIndexMng} + readonly={false} /> </TabsContent> <TabsContent className="max-h-full" value="buse"> <PbnstBuseForm - form={form} + form={form} indexMng={indexMng} indexBiz={indexBiz} setIndexBiz={setIndexBiz} formType={"reg"} - firstBizData={pbnstBdstForm.fcBizs} /> </TabsContent> - </Tabs> - </ScrollArea> + </ScrollArea> + </Tabs> </> ); } diff --git a/apps/wr/components/ui/modal/ldrmntSttn/ldrmntExcelModal.tsx b/apps/wr/components/ui/modal/ldrmntSttn/ldrmntExcelModal.tsx deleted file mode 100644 index 5427c5dd2..000000000 --- a/apps/wr/components/ui/modal/ldrmntSttn/ldrmntExcelModal.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useWrStore } from "@/app/store"; -import { pbnPrfRegExcelZod } from "@/app/store/prfmncReg/pbnstPrfmncReg/headList"; -import type { FcPrfType, PrfType } from "@/app/store/prfmncReg/pbnstPrfmncReg/type"; -import { yearOptions } from "@/components/lib/func/commom"; -import { FileUpload, Form, FormField, FormUi, SelectBox, Title } from "@common/business_components/ui"; -import { Button, Input } from "@common/components/ui"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import type { z } from "zod"; - -export default function LdrmntExcelModal({ data }: { data: FcPrfType }) { - const { prfmnRegData } = useWrStore((state) => state); - const [excelData, setExcelData] = useState<PrfType[]>(); - const form = useForm<z.infer<typeof pbnPrfRegExcelZod>>({ - resolver: zodResolver(pbnPrfRegExcelZod), - defaultValues: { - ym: String(new Date().getFullYear()), - }, - }); - const onSubmit = (values: z.infer<typeof pbnPrfRegExcelZod>) => { - if (excelData) { - prfmnRegData(excelData as unknown as PrfType); - } - }; - const [files, setFiles] = useState<File[] | null>(null); - return ( - <> - <div> - <Form {...form}> - <form onSubmit={form.handleSubmit(onSubmit)}> - <div> - <Title title={"잠재수요처 등록"}> -
-
-

- {"잠재수요처 파일"} -

- - -
-
- ( - - { - if (files) setFiles(files); - }} - fileTypes="xlsx" - > - - )} - /> -
- - -
- - ) - -} \ No newline at end of file diff --git a/apps/wr/components/ui/modal/sttn/headChModal/headChModal.tsx b/apps/wr/components/ui/modal/sttn/headChModal/headChModal.tsx index 83a5a3523..0673f3ca5 100644 --- a/apps/wr/components/ui/modal/sttn/headChModal/headChModal.tsx +++ b/apps/wr/components/ui/modal/sttn/headChModal/headChModal.tsx @@ -1,39 +1,47 @@ import { useWrStore } from "@/app/store"; -import { sttnHeadListType } from "@/app/store/ruseFcltMng/fcltSttn/rainSttn/headList"; -import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@common/business_components/ui"; +import { sttnHeadListType } from "@/app/store/ruseFcltMng/fcltSttn/rainSttn/headList"; +import { Form, FormControl, FormField, FormItem, FormLabel, } from "@common/business_components/ui"; import { Button, Checkbox } from "@common/components/ui"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; -export default function HeadChModal({headList, type }:{headList : z.infer ,type:string }) { - const { rainChSttnHead , rswtChSttnHead, htdflChSttnHead, pbnstChSttnHead} = useWrStore((state) => state); - - - let items = headList.map(item => ({id:item.id, title:item.title, val:item.visible })); - - - const form = useForm>({ - resolver: zodResolver(sttnHeadListType), - defaultValues: [], - }) +export default function HeadChModal({ headList, type }: { headList: z.infer, type: string }) { + const { rainChSttnHead, rswtChSttnHead, htdflChSttnHead, pbnstChSttnHead } = useWrStore((state) => state); - useEffect(() =>{ - form.reset(headList) + const items = headList.map(item => ({ id: item.id, title: item.title, val: item.visible })); + const form = useForm>({ + resolver: zodResolver(sttnHeadListType), + defaultValues: headList, + }); - },[headList]) + useEffect(() => { + form.reset(headList); + }, [headList, form]); - const onSubmit = (valueList : z.infer ) =>{ - if(type==='RNB'){ + const rawValues = form.watch(); + const values: Array<{ visible: boolean }> = Array.isArray(rawValues) + ? rawValues + : Object.values(rawValues); + + const allChecked = values.every((v) => v.visible); + + const toggleAll = (checked: boolean) => { + const newValues = values.map((v) => ({ ...v, visible: checked })); + form.reset(newValues); + }; + + const onSubmit = (valueList: z.infer) => { + if (type === 'RNB') { rainChSttnHead(valueList) - }else if(type=== 'RST'){ + } else if (type === 'RST') { rswtChSttnHead(valueList) - }else if(type === 'HTB'){ + } else if (type === 'HTB') { htdflChSttnHead(valueList) - }else if(type === 'PBN'){ + } else if (type === 'PBN') { pbnstChSttnHead(valueList) } } @@ -42,42 +50,50 @@ export default function HeadChModal({headList, type }:{headList : z.infer
- - -
-

조회항목 설정

-
- {items.map((item,index) => ( - - { - return ( - - - - { - field.onChange(checked); - return; - } - } - /> - - - {item.title} - - - ); - }} - /> - ))} + + +
+

조회항목 설정

+
+ {/* 전체 선택/해제 체크박스 */} + + + toggleAll(checked as boolean)} + /> + + 전체 + + {items.map((item, index) => ( + { + return ( + + + { + field.onChange(checked); + return; + } + } + /> + + + {item.title} + + + ); + }} + /> + ))} diff --git a/apps/wr/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal.tsx b/apps/wr/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal.tsx index d20d10591..1a16c1fef 100644 --- a/apps/wr/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal.tsx +++ b/apps/wr/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal.tsx @@ -1,3 +1,4 @@ +import { headMakeprfmnc } from "@/app/ruseFcltMng/prfmncReg/chkgdnReg/TableHead"; import { useWrStore } from "@/app/store"; import type { TableUpperProps } from "@/app/store/common/type/header"; import { @@ -13,29 +14,30 @@ import { type customPbnst, type fcBizType, } from "@/app/store/ruseFcltMng/fcltReg/pbnstReg/type"; +import { formH1Class } from "@/components/lib/class/comClass"; +import { InputTableBody } from "@/components/lib/func/TableData"; import { PbnstMngForm } from "@/components/ui/form/fcltReg/pbnstReg/mngReg/mngRegForm"; import { ScrollArea, Title, UiTable } from "@common/business_components/ui"; +import ListDiv from "@common/business_components/ui/list_div"; import { HeadMakeColSpan } from "@common/business_components/ui/table/HeadMakeColSpan"; import { + Table, + TableBody, + TableHeader, Tabs, + TabsContent, TabsList, TabsTrigger, - Button, - TabsContent, } from "@common/components/ui"; import { zodResolver } from "@hookform/resolvers/zod"; import { useEffect, useState } from "react"; -import { set, useForm } from "react-hook-form"; +import { useForm } from "react-hook-form"; import { z } from "zod"; -export function PbnstSttnModal({ data }: { data: customPbnst }) { - const { code, pbnstSttn, prfmnc } = useWrStore((state) => state); +export function PbnstSttnModal({ data, titleVisible }: { data: customPbnst; titleVisible?: boolean }) { + const { pbnstSttn, prfmnc, pbnstPrfmncSttn } = useWrStore((state) => state); const [fcltTab, setFcltTab] = useState("fclt"); - const [ruseTab, setRuseTab] = useState("inoutTotal"); - const [ruseBizData, setRuseBizData] = useState<{ - data: any[]; - head: TableUpperProps[]; - }>(); + const [ruseTab, setRuseTab] = useState("totalPrf"); const [ruseHeadList, setRuseHeadList] = useState([]); const [ruseTableData, setRuseTableData] = useState([]); const [prfData, setPrfData] = useState<{ @@ -44,7 +46,6 @@ export function PbnstSttnModal({ data }: { data: customPbnst }) { totalPrf: any[]; }>(); const [indexMng, setIndexMng] = useState(0); - const [indexBiz, setIndexBiz] = useState(0); const form = useForm<{ items: z.infer[]; }>({ @@ -54,7 +55,6 @@ export function PbnstSttnModal({ data }: { data: customPbnst }) { }); - const { monthHeadList } = prfmnc; useEffect(() => { let tableData; if (fcltTab === "fclt") { @@ -67,10 +67,10 @@ export function PbnstSttnModal({ data }: { data: customPbnst }) { cusrAlotm: item.cusrAlotm ?? "-", etc: item.etc ?? "-", totalMoney: - Number(item.addr) + Number(item.gvsbs) + Number(item.lcep) + - Number(item.cusrAlotm), + Number(item.cusrAlotm) + + (isNaN(Number(item.etc)) ? 0 : Number(item.etc)), instlAprvSncnDt: item.instlAprvSncnDt?.toISOString ?? "-", bizCmcnDay: item.bizCmcnDay?.toISOString ?? "-", rprcsFcltCpct: item.rprcsFcltCpct ?? "-", @@ -138,7 +138,6 @@ export function PbnstSttnModal({ data }: { data: customPbnst }) { }); }); } - console.log(tableData); if (tableData && tableData.length > 0) { setRuseTableData(tableData as any[]); } @@ -167,8 +166,8 @@ export function PbnstSttnModal({ data }: { data: customPbnst }) { const getTablePrfData = (prfList: PrfType[]) => { const inUsageData = structuredClone(initValInUsage); const outUsageData = structuredClone(initValOutUsage); - const inSubtotal = structuredClone(subTotalObj); - const outSubtotal = structuredClone(subTotalObj); + const inSubtotal = { ...structuredClone(subTotalObj), 구분: "장내용수" }; + const outSubtotal = { ...structuredClone(subTotalObj), 구분: "장외용수" }; if (prfList) { prfList?.forEach((prfItem) => { @@ -179,6 +178,7 @@ export function PbnstSttnModal({ data }: { data: customPbnst }) { const usageKey = inData.usage; const value = (prfItem as any)[usageKey] ?? 0; + inData[monthKey] = value; inData.total += value; @@ -215,7 +215,6 @@ export function PbnstSttnModal({ data }: { data: customPbnst }) { (usageTotalObj as any)[key] = val; } const tableData = [...inUsageData, ...outUsageData, usageTotalObj]; - setPrfData({ inPrf: inUsageData, outPrf: outUsageData, @@ -226,10 +225,10 @@ export function PbnstSttnModal({ data }: { data: customPbnst }) { useEffect(() => { if (pbnstSttn.pbnstSttnRes && data) { const prfList = pbnstSttn.pbnstSttnRes.find( - (item) => item.pbnstRuseFcltPk === data.pbnstRuseFcltPk + (item) => item.pbnstRuseFcltId === data.pbnstRuseFcltId )?.prfList; - if (prfList) {console.log("prfList", prfList); + if (prfList) { getTablePrfData(prfList); } @@ -237,95 +236,108 @@ export function PbnstSttnModal({ data }: { data: customPbnst }) { }, [pbnstSttn.pbnstSttnRes, data]); - useEffect(()=>{ + useEffect(() => { - if(data){ - form.reset({items:[data]}) + if (data) { + form.reset({ items: [data] }) } - },[data, form]) + }, [data, form]) - - useEffect(()=>{ - console.log("prfData", prfData); - },[prfData]) return ( <>
- + {titleVisible && ( + <> + <Title title={"하수재이용 시설 상세정보"} /> + </> + )} <ScrollArea className={"h-[70vh]"}> <PbnstMngForm form={form} formType={"sttn"} indexMng={indexMng} setIndexMng={setIndexMng} + readonly={true} /> - <Tabs defaultValue="fclt" onValueChange={setFcltTab}> - <TabsList className=""> - <TabsTrigger className="!text-xl" value="fclt"> - 하수처리수 재이용시설 현황 - </TabsTrigger> - <TabsTrigger className="!text-xl" value="sply"> - 재이용 원수 취수 현황 - </TabsTrigger> - <TabsTrigger className="!text-xl" value="ppi"> - 재이용 공급관로 현황 - </TabsTrigger> - <TabsTrigger className="!text-xl" value="guarantee"> - 재이용 보증수질 현황 - </TabsTrigger> - <TabsTrigger className="!text-xl" value="dmdCo"> - 재이용 수요처 현황 - </TabsTrigger> - </TabsList> - <TabsContent className="max-h-full" value={fcltTab}> - <UiTable - headName={undefined} - tableData={ruseTableData || []} - headlist={ruseHeadList || []} - pageSize={0} - total={0} - cellClick={undefined} - onPageChange={function (page: number): void { - throw new Error("Function not implemented."); - }} - children={ - fcltTab === "fclt" ? ( - <HeadMakeColSpan upHeadList={pbnstSttn.ruseFcltNodeList} /> - ) : undefined - } - ></UiTable> - </TabsContent> - </Tabs> - - <Tabs onValueChange={setRuseTab}> - <TabsList className=""> - <TabsTrigger className="!text-xl" value="totalPrf"> - 장내외합계 - </TabsTrigger> - <TabsTrigger className="!text-xl" value="inPrf"> - 장내용수 - </TabsTrigger> - <TabsTrigger className="!text-xl" value="outPrf"> - 장외용수 - </TabsTrigger> - </TabsList> - <TabsContent className="max-h-full" value={ruseTab}> - {prfData && ( + <div className="px-6"> + <div className=""> + <h1 className={formH1Class}>하수재이용시설 현황</h1> + </div> + <Tabs defaultValue="fclt" onValueChange={setFcltTab}> + <TabsList className="mt-4"> + <TabsTrigger className="!text-xl" value="fclt"> + 하수처리수 재이용시설 현황 + </TabsTrigger> + <TabsTrigger className="!text-xl" value="sply"> + 재이용 원수 취수 현황 + </TabsTrigger> + <TabsTrigger className="!text-xl" value="ppi"> + 재이용 공급관로 현황 + </TabsTrigger> + <TabsTrigger className="!text-xl" value="guarantee"> + 재이용 보증수질 현황 + </TabsTrigger> + <TabsTrigger className="!text-xl" value="dmdCo"> + 재이용 수요처 현황 + </TabsTrigger> + </TabsList> + <TabsContent className="max-h-full" value={fcltTab}> <UiTable headName={undefined} - tableData={prfData[ruseTab as keyof typeof prfData] || []} - headlist={monthHeadList || []} + tableData={ruseTableData || []} + headlist={ruseHeadList || []} pageSize={0} total={0} cellClick={undefined} - onPageChange={function (page: number): void { + onPageChange={function (): void { throw new Error("Function not implemented."); }} - ></UiTable> - )} - </TabsContent> - </Tabs> + > + {fcltTab === "fclt" ? ( + <HeadMakeColSpan upHeadList={pbnstSttn.ruseFcltNodeList} /> + ) : undefined} + </UiTable> + </TabsContent> + </Tabs> + <div className=""> + <h1 className={formH1Class}>하수재이용실적</h1> + </div> + <Tabs defaultValue="totalPrf" onValueChange={setRuseTab}> + <TabsList className="mt-4"> + <TabsTrigger className="!text-xl" value="totalPrf"> + 장내외합계 + </TabsTrigger> + <TabsTrigger className="!text-xl" value="inPrf"> + 장내용수 + </TabsTrigger> + <TabsTrigger className="!text-xl" value="outPrf"> + 장외용수 + </TabsTrigger> + </TabsList> + <TabsContent className="max-h-full" value={ruseTab}> + {prfData && ( + <ListDiv className="col-span-8 pr-6 "> + <ScrollArea className="h-[60vh]"> + <div className="card-header"> + <h3 className="text-2xl font-medium text-label"> + {"하수처리수재이용 실적(m³) 등록결과"} + </h3> + </div> + <Table variant="secondary"> + <TableHeader className="font-bold"> + {headMakeprfmnc(pbnstPrfmncSttn.monthSttnHeadList, [])} + </TableHeader> + <TableBody key={prfmnc.pageKey + 'pageKey'}> + {InputTableBody(prfData[ruseTab as keyof typeof prfData] || [], pbnstPrfmncSttn.monthSttnHeadList, setPrfData as any)} + </TableBody> + </Table> + </ScrollArea> + </ListDiv> + )} + </TabsContent> + </Tabs> + </div> </ScrollArea> </div> </> diff --git a/apps/wr/components/ui/modal/sttn/sttnDetailModal/coDatailModal.tsx b/apps/wr/components/ui/modal/sttn/sttnDetailModal/coDatailModal.tsx index edffcc71d..cb38717a3 100644 --- a/apps/wr/components/ui/modal/sttn/sttnDetailModal/coDatailModal.tsx +++ b/apps/wr/components/ui/modal/sttn/sttnDetailModal/coDatailModal.tsx @@ -1,50 +1,65 @@ "use client"; -import { useWrStore } from "@/app/store"; +import { txtSize } from "@/app/store/common/code/basicCode"; import type { customLdrmnt } from "@/app/store/matDmd/ldrmntCoSttn"; import { ldrmntZod } from "@/app/store/matDmd/ldrmntCoSttn/headList"; import { selectClass } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; -import { handleCommaNumberChange } from "@/components/lib/func/commom"; -import { DatePicker, Form, FormField, FormUi, ScrollArea } from "@common/business_components/ui"; +import { formatCommaNumber, handleCommaNumberChange } from "@/components/lib/func/commom"; +import { Form, FormField, FormUi, ScrollArea } from "@common/business_components/ui"; import { Button, Input } from "@common/components/ui"; import { zodResolver } from "@hookform/resolvers/zod"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import type { z } from "zod"; -export default function SttnDetailModal({ +export default function CoDetailModal({ formObj, + onFormSubmit, + readonly }: { formObj: any; + onFormSubmit: (values: customLdrmnt) => void; + readonly?: boolean; }) { - const { ldrmntUpdate } = useWrStore( - (state) => state - ); const defaultValues: z.infer<typeof ldrmntZod> = ldrmntZod.parse({}); const form = useForm<z.infer<typeof ldrmntZod>>({ resolver: zodResolver(ldrmntZod), defaultValues: defaultValues, }); - const onSubmit = (values: customLdrmnt) => { - console.log("values", values) - if (values) - ldrmntUpdate(values) - }; - useEffect(() => { if (ldrmntZod) form.reset(formObj); - }, [formObj]); + + useEffect(() => { + const male = Number(form.watch("maleEmpCnt") ?? 0); + const female = Number(form.watch("fmaleEmpCnt") ?? 0); + const frgnrMale = Number(form.watch("frgnrMaleEmpCnt") ?? 0); + const frgnrFemale = Number(form.watch("frgnrFmaleEmpCnt") ?? 0); + + const total = male + female + frgnrMale + frgnrFemale; + + form.setValue("totalEmpCnt", total); + }, [ + form.watch("maleEmpCnt"), + form.watch("fmaleEmpCnt"), + form.watch("frgnrMaleEmpCnt"), + form.watch("frgnrFmaleEmpCnt"), + ]); + return ( <div> - <ScrollArea className="h-[85vh]"> + <ScrollArea className="h-[85vh] mx-8"> <Form {...form}> - <form onSubmit={form.handleSubmit(onSubmit)}> - <div className="grid grid-cols-12 flex items-center card-header"> + <form onSubmit={form.handleSubmit(onFormSubmit)}> + <div className="grid grid-cols-12 items-center card-header"> <h1 className="text-2xl font-medium text-label col-span-10"> 잠재수요처 상세정보 </h1> - <Button type="submit" className="col-start-12 col-span-1" size={"sm"}> + <Button type="submit" + className="col-start-12 col-span-1" + size={"sm"} + disabled={readonly} + > 저장 </Button> </div> @@ -58,14 +73,11 @@ export default function SttnDetailModal({ className={selectClass} placeholder="" type="hidden" - onChange={(e) => { - }} value={field.value ?? ""} /> </FormUi> )} /> - <div className="grid grid-cols-4 gap-2 mt-4"> <div className="col-span-2"> <FormField @@ -80,7 +92,9 @@ export default function SttnDetailModal({ onChange={(e) => { field.onChange(e.target.value); }} - value={field.value ?? "" ?? ""} + value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} @@ -99,14 +113,15 @@ export default function SttnDetailModal({ onChange={(e) => { field.onChange(e.target.value); }} - value={field.value ?? "" ?? ""} + value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} /> </div> </div> - <div className="grid grid-cols-4 gap-2 mt-4"> <div className="col-span-2"> <FormField @@ -121,7 +136,9 @@ export default function SttnDetailModal({ onChange={(e) => { field.onChange(e.target.value); }} - value={field.value ?? "" ?? ""} + value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} @@ -140,7 +157,9 @@ export default function SttnDetailModal({ onChange={(e) => { field.onChange(e.target.value); }} - value={field.value ?? "" ?? ""} + value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} @@ -161,7 +180,9 @@ export default function SttnDetailModal({ onChange={(e) => { field.onChange(e.target.value); }} - value={field.value ?? "" ?? ""} + value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} @@ -180,7 +201,9 @@ export default function SttnDetailModal({ onChange={(e) => { field.onChange(e.target.value); }} - value={field.value ?? "" ?? ""} + value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} @@ -199,7 +222,9 @@ export default function SttnDetailModal({ onChange={(e) => { field.onChange(e.target.value); }} - value={field.value ?? "" ?? ""} + value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} @@ -221,12 +246,13 @@ export default function SttnDetailModal({ field.onChange(e.target.value); }} value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} /> </div> - <div className="col-span-2"> <FormField control={form.control} @@ -241,6 +267,8 @@ export default function SttnDetailModal({ field.onChange(e.target.value); }} value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} @@ -262,6 +290,8 @@ export default function SttnDetailModal({ field.onChange(e.target.value); }} value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} @@ -281,6 +311,8 @@ export default function SttnDetailModal({ field.onChange(e.target.value); }} value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} @@ -300,14 +332,14 @@ export default function SttnDetailModal({ field.onChange(e.target.value); }} value={field.value ?? ""} + maxLength={13} + readOnly={readonly} /> </FormUi> )} /> </div> </div> - - <div className="grid grid-cols-5 gap-2 mt-4"> <div className="col-span-1"> <FormField @@ -318,12 +350,15 @@ export default function SttnDetailModal({ <Input className={selectClass} placeholder="" - type="number" + type="text" onChange={(e) => { // field.onChange(e.target.value); handleCommaNumberChange(e, field.onChange); }} - value={field.value ?? 0} + value={formatCommaNumber(field.value)} + min={0} + maxLength={8} + readOnly={readonly} /> </FormUi> )} @@ -338,11 +373,14 @@ export default function SttnDetailModal({ <Input className={selectClass} placeholder="" - type="number" + type="text" onChange={(e) => { - field.onChange(e.target.value); + handleCommaNumberChange(e, field.onChange); }} - value={field.value ?? 0} + value={formatCommaNumber(field.value)} + min={0} + maxLength={8} + readOnly={readonly} /> </FormUi> )} @@ -357,11 +395,14 @@ export default function SttnDetailModal({ <Input className={selectClass} placeholder="" - type="number" + type="text" onChange={(e) => { - field.onChange(e.target.value); + handleCommaNumberChange(e, field.onChange); }} - value={field.value ?? 0} + value={formatCommaNumber(field.value)} + min={0} + maxLength={8} + readOnly={readonly} /> </FormUi> )} @@ -376,11 +417,14 @@ export default function SttnDetailModal({ <Input className={selectClass} placeholder="" - type="number" + type="text" onChange={(e) => { - field.onChange(e.target.value); + handleCommaNumberChange(e, field.onChange); }} - value={field.value ?? 0} + value={formatCommaNumber(field.value)} + min={0} + maxLength={8} + readOnly={readonly} /> </FormUi> )} @@ -395,18 +439,19 @@ export default function SttnDetailModal({ <Input className={selectClass} placeholder="" - type="number" + type="text" onChange={(e) => { - field.onChange(e.target.value); + handleCommaNumberChange(e, field.onChange); }} - value={field.value ?? 0} + value={formatCommaNumber(field.value) ?? 0} + min={0} + readOnly={true} /> </FormUi> )} /> </div> </div> - <div className="grid grid-cols-12 gap-2 mt-4"> <div className="col-span-4"> <FormField @@ -422,6 +467,8 @@ export default function SttnDetailModal({ field.onChange(e.target.value); }} value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} @@ -441,6 +488,8 @@ export default function SttnDetailModal({ field.onChange(e.target.value); }} value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} @@ -460,13 +509,14 @@ export default function SttnDetailModal({ field.onChange(e.target.value); }} value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} /> </div> </div> - <div className="grid grid-cols-12 gap-2 mt-4"> <div className="col-span-4"> <FormField @@ -482,6 +532,8 @@ export default function SttnDetailModal({ field.onChange(e.target.value); }} value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} @@ -501,6 +553,8 @@ export default function SttnDetailModal({ field.onChange(e.target.value); }} value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} @@ -520,14 +574,14 @@ export default function SttnDetailModal({ field.onChange(e.target.value); }} value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} /> </div> </div> - - <div className="grid grid-cols-12 gap-2 mt-4"> <div className="col-span-4"> <FormField @@ -543,6 +597,8 @@ export default function SttnDetailModal({ field.onChange(e.target.value); }} value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} @@ -562,6 +618,8 @@ export default function SttnDetailModal({ field.onChange(e.target.value); }} value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} @@ -581,14 +639,14 @@ export default function SttnDetailModal({ field.onChange(e.target.value); }} value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} /> </div> </div> - - <div className="grid grid-cols-12 gap-2 mt-4"> <div className="col-span-4"> <FormField @@ -604,6 +662,8 @@ export default function SttnDetailModal({ field.onChange(e.target.value); }} value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} @@ -623,6 +683,8 @@ export default function SttnDetailModal({ field.onChange(e.target.value); }} value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} @@ -642,13 +704,14 @@ export default function SttnDetailModal({ field.onChange(e.target.value); }} value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} /> </div> </div> - <div className="grid grid-cols-12 gap-2 mt-4"> <div className="col-span-4"> <FormField @@ -664,6 +727,8 @@ export default function SttnDetailModal({ field.onChange(e.target.value); }} value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} @@ -683,6 +748,8 @@ export default function SttnDetailModal({ field.onChange(e.target.value); }} value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} @@ -702,6 +769,8 @@ export default function SttnDetailModal({ field.onChange(e.target.value); }} value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} @@ -723,6 +792,8 @@ export default function SttnDetailModal({ field.onChange(e.target.value); }} value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} @@ -732,7 +803,7 @@ export default function SttnDetailModal({ <FormField control={form.control} name="fctryMngNo" - render={({ field, fieldState }) => ( + render={({ field }) => ( <FormUi label="공장관리번호"> <Input className={selectClass} @@ -742,13 +813,14 @@ export default function SttnDetailModal({ field.onChange(e.target.value); }} value={field.value ?? ""} + maxLength={txtSize} + readOnly={readonly} /> </FormUi> )} /> </div> </div> - <div className="grid grid-cols-4 gap-2 mt-4"> <div className="col-span-2"> <FormField @@ -764,6 +836,8 @@ export default function SttnDetailModal({ field.onChange(e.target.value); }} value={field.value ?? ""} + maxLength={50} + readOnly={readonly} /> </FormUi> )} @@ -783,15 +857,14 @@ export default function SttnDetailModal({ field.onChange(e.target.value); }} value={field.value ?? ""} + maxLength={50} + readOnly={readonly} /> </FormUi> )} /> </div> </div> - - - </form> </Form> </ScrollArea> diff --git a/apps/wr/components/ui/modal/sttn/sttnDetailModal/sttnDetailModal.tsx b/apps/wr/components/ui/modal/sttn/sttnDetailModal/sttnDetailModal.tsx index 0f7dbf706..6610a826a 100644 --- a/apps/wr/components/ui/modal/sttn/sttnDetailModal/sttnDetailModal.tsx +++ b/apps/wr/components/ui/modal/sttn/sttnDetailModal/sttnDetailModal.tsx @@ -1,17 +1,16 @@ "use client"; -import { ScrollArea, Title, UiTable } from "@common/business_components/ui"; -import RainRegForm from "../../../form/fcltReg/rainReg/rainRegForm"; -import RswtrRegForm from "../../../form/fcltReg/rswtrReg/rswtrRegForm"; -import HtdflRegForm from "../../../form/fcltReg/htdflReg/htdflRegForm"; +import { useWrStore } from "@/app/store"; +import type { CommonTableProps } from "@/app/store/common/type/header"; +import type { customHtdflBdst } from "@/app/store/ruseFcltMng/fcltReg/htdflReg"; import type { customRainBdst } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; import type { customRswtBdst } from "@/app/store/ruseFcltMng/fcltReg/rswtReg"; -import type { customHtdflBdst } from "@/app/store/ruseFcltMng/fcltReg/htdflReg"; -import { useWrStore } from "@/app/store"; -import { useEffect, useState } from "react"; -import type { CommonTableProps } from "@/app/store/common/type/header"; -import { rainChkTableData, rswtChkTableData } from "@/components/lib/func/TableData"; +import { rainChkTableData } from "@/components/lib/func/TableData"; +import { ScrollArea, Title, UiTable } from "@common/business_components/ui"; import { HeadMakeColSpan } from "@common/business_components/ui/table/HeadMakeColSpan"; -import type { rswtChkZodType } from "@/app/store/prfmncReg/chkgdnReg/rswrtChkgdn"; +import { useEffect, useState } from "react"; +import HtdflRegForm from "../../../form/fcltReg/htdflReg/htdflRegForm"; +import RainRegForm from "../../../form/fcltReg/rainReg/rainRegForm"; +import RswtrRegForm from "../../../form/fcltReg/rswtrReg/rswtrRegForm"; export default function SttnDetailModal({ type, @@ -20,7 +19,7 @@ export default function SttnDetailModal({ type: string; formObj: any; }) { - const { code, rainSttn, rswtSttn, rainSttnChkSearch,rswtChkPolAndGuide } = useWrStore( + const { rainSttn, rswtSttn, rainSttnChkSearch } = useWrStore( (state) => state ); const [detailData, setDetailData] = useState({ @@ -30,19 +29,19 @@ export default function SttnDetailModal({ }); useEffect(() => { - if (type === "RNB" && formObj.reuseBuilding[0]) { - rainSttnChkSearch(formObj.reuseBuilding[0].bdstRuseFcltPk); + if (type === "RNB" && formObj?.reuseBuilding[0]) { + rainSttnChkSearch(formObj.reuseBuilding[0].bdstRuseFcltSn); } else if (type === "RST") { - // rswtChkPolAndGuide(formObj.reuseBuilding[0].bdstRuseFcltPk) + // rswtChkPolAndGuide(formObj.reuseBuilding[0].bdstRuseFcltSn) } }, [type, formObj]); useEffect(() => { if ( type === "RNB" && - formObj.reuseBuilding[0] && - rainSttn.rainChkRes?.bdstRuseFcltFk + formObj?.reuseBuilding[0] && + rainSttn.rainChkRes?.bdstRuseFcltSn ) { const tableTotalData = rainChkTableData([rainSttn.rainChkRes]); const data = tableTotalData.map((item) => { @@ -79,7 +78,6 @@ export default function SttnDetailModal({ useEffect(() =>{ setDetailData({data:[] , head:[], node:[]}) - },[]) return ( @@ -104,15 +102,17 @@ export default function SttnDetailModal({ } tableData={detailData.data || []} headlist={detailData.head || []} - children={ <HeadMakeColSpan upHeadList={detailData.node} />} pageSize={0} total={0} cellClick={undefined} onPageChange={function (page: number): void { + console.log('page ', page) throw new Error("Function not implemented."); }} - ></UiTable>:<></>} + > + <HeadMakeColSpan upHeadList={detailData.node} /> + </UiTable>:<></>} </ScrollArea> </div> diff --git a/apps/wr/components/ui/searchInput/searchInput.tsx b/apps/wr/components/ui/searchInput/searchInput.tsx index 796c8c298..f512516b8 100644 --- a/apps/wr/components/ui/searchInput/searchInput.tsx +++ b/apps/wr/components/ui/searchInput/searchInput.tsx @@ -1,3 +1,4 @@ +import { Input } from "@common/components/ui"; import React, { useCallback, useEffect, useRef, useState } from "react"; type SearchType = { @@ -10,6 +11,7 @@ type thisProps = { setValue: (searchNm?: string) => void; setRtnData: (rtnData: any) => void; searchList?: any[]; + readonly?:boolean }; export default function SearchInputText({ label = undefined, @@ -17,6 +19,7 @@ export default function SearchInputText({ setValue = () => {}, setRtnData = () => {}, searchList = undefined, + readonly = false, // 추가: readonly prop }: thisProps) { const [isOpen, setIsOpen] = useState(false); @@ -62,19 +65,17 @@ export default function SearchInputText({ document.removeEventListener("mousedown", handleClickOutside); }; }, []); -useEffect(() =>{ - console.log('searchList ', searchList) -},[searchList]) return ( <div ref={wrapperRef}> - <input + <Input type="text" value={textValue} onChange={handleInputChange} /* onFocus={() => setIsOpen(true)} */ className="w-full h-[3.2rem] !text-xl pl-3 text-gray-600 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder={`${label}을(를) 입력하세요`} + readOnly={readonly} // readonly 상태 적용 /> {isOpen && ( <div className=" z-1 w-full mt-2 bg-white border border-gray-300 rounded-md shadow-lg h-90 overflow-auto"> diff --git a/apps/wr/components/ui/table/chkgdTable/guideDetailTable/guideDetailTable.tsx b/apps/wr/components/ui/table/chkgdTable/guideDetailTable/guideDetailTable.tsx index 94be4ebb6..7522fdbfd 100644 --- a/apps/wr/components/ui/table/chkgdTable/guideDetailTable/guideDetailTable.tsx +++ b/apps/wr/components/ui/table/chkgdTable/guideDetailTable/guideDetailTable.tsx @@ -1,55 +1,53 @@ "use client"; import { headMakeprfmnc } from "@/app/ruseFcltMng/prfmncReg/chkgdnReg/TableHead"; -import { useWrStore } from "@/app/store"; import type { TableUpperProps } from "@/app/store/common/type/header"; import type { } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn"; +import type { chHeadType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList"; import { InputTableBody } from "@/components/lib/func/TableData"; import { - Input, Table, TableBody, - TableCell, TableHeader, - TableRow, } from "@common/components/ui"; -import { useEffect, useState, type Dispatch, type SetStateAction } from "react"; +import { useEffect, useState } from "react"; export function GuideDetailsTable({ chgRes, headList, - type, + upheadList, setChGuidData, + readonly }: { chgRes: any[]; - headList: TableUpperProps[]; + headList: TableUpperProps[]; + upheadList: chHeadType[]; type: string; setChGuidData: (chgRes: any[]) => void; + readonly?: boolean; }) { - const { code, rainChkgdn } = useWrStore((state) => state); const [inputData, setInputData] = useState<any[]>(); useEffect(() => { setInputData(chgRes); }, [chgRes]); - useEffect(() => { - setChGuidData(inputData??[] ); + setChGuidData(inputData ?? []); }, [inputData]); - + return ( <> - <div className={`card !m-[1.2rem] !p-0r `}> - <Table variant="secondary"> - <TableHeader className="font-bold">{headMakeprfmnc(headList, rainChkgdn?.rainHeadNm)}</TableHeader> - <TableBody > - {InputTableBody(inputData ?? [],headList, setInputData) } - </TableBody> - </Table> - </div> + <div className={`card !m-[1.2rem] !p-0r `}> + <Table variant="secondary"> + <TableHeader className="font-bold">{headMakeprfmnc(headList, upheadList)}</TableHeader> + <TableBody > + {InputTableBody(inputData ?? [], headList, setInputData, readonly)} + </TableBody> + </Table> + </div> </> ); } diff --git a/apps/wr/components/ui/table/chkgdTable/rainChkgd/rainChkgdTable.tsx b/apps/wr/components/ui/table/chkgdTable/rainChkgd/rainChkgdTable.tsx index d0ad437b3..f016a0cc3 100644 --- a/apps/wr/components/ui/table/chkgdTable/rainChkgd/rainChkgdTable.tsx +++ b/apps/wr/components/ui/table/chkgdTable/rainChkgd/rainChkgdTable.tsx @@ -1,15 +1,16 @@ "use client"; import { headMakeprfmnc } from "@/app/ruseFcltMng/prfmncReg/chkgdnReg/TableHead"; import { useWrStore } from "@/app/store"; -import type { rainChkZodType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn"; -import type { chHeadType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList"; import type { TableUpperProps } from "@/app/store/common/type/header"; +import { rainPageSize, type rainChkZodType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn"; +import type { chHeadType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList"; import { tableData } from "@/components/lib/func/commom"; import { chRowspanTable, - headVisibleChange, rainChkTableData } from "@/components/lib/func/TableData"; +import LoadingOverlay from "@common/business_components/ui/loadingBar"; +import UiPageNations from "@common/business_components/ui/table/uiPageNations"; import { Table, TableBody, @@ -17,21 +18,29 @@ import { } from "@common/components/ui"; import { useCallback, useEffect, useState } from "react"; export default function RainChkgdTable({ - detailUp, - handleClick=() => {}, + detailUp, + handleClick = () => { }, deleteData, rainChkData, + pageNation, + onPageChange, + pageCount, + readonly, }: { detailUp: (data: rainChkZodType[]) => void; - handleClick?: (data:any) => void; - deleteData:(selectedIds:number[])=>void + handleClick?: (data: any) => void; + deleteData: (selectedIds: number[]) => void rainChkData?: rainChkZodType[]; + pageNation: number; + onPageChange: (page: number) => void; + pageCount: number; + readonly: boolean; }) { - const { code, rainChkgdn ,rainChkSttn,getRainSttnExcel } = useWrStore((state) => state); + const { code, rainChkgdn, rainChkSttn, getRainSttnExcel } = useWrStore((state) => state); const [makeTableData, setMakeTableData] = useState<any[]>([]); const [selectedIds, setSelectedIds] = useState<number[]>([]); const [upRaintDetail, setUpRaintDetail] = useState<rainChkZodType[]>([]); - const [headList, setHeadList] = useState< {headList:TableUpperProps[],upHeadList:chHeadType[]}>({headList:[], upHeadList:[]}); + const [headList, setHeadList] = useState<{ headList: TableUpperProps[], upHeadList: chHeadType[] }>({ headList: [], upHeadList: [] }); const toggleSelection = (pk: number, checked: boolean) => { @@ -46,30 +55,31 @@ export default function RainChkgdTable({ ) => { setMakeTableData((prev) => - prev.map((row, idx) => { - return idx === rowIndex ? { ...row, [field]: value } : row + prev.map((row, idx) => { + return idx === rowIndex ? { ...row, [field]: value } : row } ) ); }; - const handleCellClick= useCallback((index: number, headId: string) => { + const handleCellClick = useCallback((index: number, headId: string) => { if ( headId === "totalSplyUswtrUtztnQnt" || headId === "totalRuseFcltUtztnQnt" ) { - + handleClick(makeTableData[index]) } - },[makeTableData, handleClick]); + }, [makeTableData, handleClick]); useEffect(() => { setSelectedIds([])// 셀렉트 초기화를 위한 작업 - if(rainChkData){ - const rainChg = rainChkData.map((item: any) => { + if (rainChkData && rainChkData.length > 0) { + + const rainChg = rainChkData?.map((item: any) => { const bud = tableData([item.building], { code }); return { ...item, @@ -77,16 +87,19 @@ export default function RainChkgdTable({ rowspan: item.rainyDetails?.length, }; }); + setMakeTableData(rainChkTableData(rainChg)); + } else { + setMakeTableData([]); } }, [rainChkData, code]); useEffect(() => { //데이터를 평탄화 한것중에 체크박스 선택한 테이블만 선택 - + //현황 페이지에서 필요한 엑셀 데이터를 바뀔때마다 넣기 위해서 getRainSttnExcel(makeTableData); - const selectedData = makeTableData.filter((item) =>selectedIds.includes(item.chkgdnPk)); + const selectedData = makeTableData.filter((item) => selectedIds.includes(item.chkgdnSn)); if (selectedData.length > 0) { const updateTableData = rainChkgdn.rainChk @@ -97,11 +110,10 @@ export default function RainChkgdTable({ // 변경 데이터는 평탄화한 데이터 즉 루트에 변경되어 있음 이용용도까지 잇음 그걸로 찾기 const findRain = selectedData.find( (sd) => - rd.chkgdnFk === sd.chkgdnPk && rd.utztnUsgCd === sd.utztnUsgCd + rd.chkgdnSn === sd.chkgdnSn && rd.utztnUsgCd === sd.utztnUsgCd ); if (findRain != undefined) { - console.log(findRain) return { ...rd, ph: Number(findRain.ph), @@ -127,63 +139,71 @@ export default function RainChkgdTable({ } }) .filter(Boolean); - + setUpRaintDetail(updateTableData as rainChkZodType[]); } - }, [makeTableData,selectedIds]); + }, [makeTableData, selectedIds]); useEffect(() => { detailUp(upRaintDetail); }, [upRaintDetail]); - useEffect(() =>{ - if(selectedIds.length >0){ + useEffect(() => { + if (selectedIds.length > 0) { // 삭제를 위한 데이터 삽입 - deleteData(selectedIds ) + deleteData(selectedIds) } - },[selectedIds]) + }, [selectedIds]) ///현황일 경우 input 비활성화 한다. - useEffect(() => { - if(rainChkSttn.isSttn){ - + useEffect(() => { + if (rainChkSttn.isSttn) { + setHeadList({ headList: rainChkSttn.rainChkgdnHeadList, upHeadList: rainChkSttn.rainHeadNm }) - }else{ + } else { setHeadList({ headList: rainChkgdn.rainChkgdnHeadList, - upHeadList: rainChkgdn.rainHeadNm + upHeadList: rainChkgdn.rainMainHeadNm }) } - - }, [rainChkSttn.isSttn, rainChkgdn.rainChkgdnHeadList , rainChkSttn.rainChkgdnHeadList]); - + }, [rainChkSttn.isSttn, rainChkgdn.rainChkgdnHeadList, rainChkSttn.rainChkgdnHeadList]); return ( - <Table variant="secondary"> - <TableHeader className="font-bold"> - {headMakeprfmnc( - headList?.headList, - headList?.upHeadList - )} - </TableHeader> + <> + <LoadingOverlay isLoading={rainChkgdn.isLoading}></LoadingOverlay> + <Table variant="secondary"> + <TableHeader className="font-bold"> + {headMakeprfmnc( + headList?.headList, + headList?.upHeadList + )} + </TableHeader> - <TableBody> - {chRowspanTable( - makeTableData, - headList.headList, - handleChange, - toggleSelection, - selectedIds, - handleCellClick - )} - </TableBody> - </Table> + <TableBody> + {chRowspanTable( + makeTableData, + headList.headList, + handleChange, + toggleSelection, + selectedIds, + handleCellClick, + readonly + )} + </TableBody> + </Table> + <UiPageNations + total={pageCount} + cur={pageNation} + pageSize={rainPageSize} + onPageChange={onPageChange} + /> + </> ); } diff --git a/apps/wr/components/ui/table/chkgdTable/rswtChkgd/rswtChkgdTable.tsx b/apps/wr/components/ui/table/chkgdTable/rswtChkgd/rswtChkgdTable.tsx index da6cacc9a..2bb3a645f 100644 --- a/apps/wr/components/ui/table/chkgdTable/rswtChkgd/rswtChkgdTable.tsx +++ b/apps/wr/components/ui/table/chkgdTable/rswtChkgd/rswtChkgdTable.tsx @@ -1,35 +1,46 @@ "use client"; import { headMakeprfmnc } from "@/app/ruseFcltMng/prfmncReg/chkgdnReg/TableHead"; import { useWrStore } from "@/app/store"; -import type { chHeadType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList"; -import type { - rswtChkZodType, - rswtrChkDetailsZodType, -} from "@/app/store/prfmncReg/chkgdnReg/rswrtChkgdn"; import type { TableUpperProps } from "@/app/store/common/type/header"; +import type { chHeadType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList"; +import { + rswtPageSize, + type rswtChkZodType, + type rswtrChkDetailsZodType, +} from "@/app/store/prfmncReg/chkgdnReg/rswrtChkgdn"; import { tableData } from "@/components/lib/func/commom"; import { chRowspanTable, rswtChkTableData, } from "@/components/lib/func/TableData"; +import LoadingOverlay from "@common/business_components/ui/loadingBar"; +import UiPageNations from "@common/business_components/ui/table/uiPageNations"; import { Table, TableBody, TableHeader } from "@common/components/ui"; import { useCallback, useEffect, useState } from "react"; export default function RswtChkgdTable({ detaiRswtlUpdate, - handleRswtClick = () => {}, + handleRswtClick = () => { }, deleteRswtData, rswtChkData, + pageNation, + onPageChange, + pageCount, + readonly = false, }: { detaiRswtlUpdate: (data: rswtrChkDetailsZodType[]) => void; handleRswtClick?: (data: any) => void; deleteRswtData: (selectedIds: number[]) => void; rswtChkData: rswtChkZodType[]; + pageNation: number; + onPageChange: (page: number) => void; + pageCount: number; + readonly:boolean }) { - const { code, rswtChkgdn,rswtChkSttn , rswtChkgdnInit, rswtChkPolAndGuide,getRswtSttnExcel } = useWrStore( + const { code, rswtChkgdn, rswtChkSttn, rswtChkPolAndGuide, getRswtSttnExcel } = useWrStore( (state) => state ); - const [headList, setHeadList] = useState< {headList:TableUpperProps[],upHeadList:chHeadType[]}>({headList:[], upHeadList:[]}); + const [headList, setHeadList] = useState<{ headList: TableUpperProps[], upHeadList: chHeadType[] }>({ headList: [], upHeadList: [] }); const [makeTableData, setMakeTableData] = useState<any[]>([]); const [selectedIds, setSelectedIds] = useState<number[]>([]); @@ -56,7 +67,7 @@ export default function RswtChkgdTable({ const handleCellClick = useCallback( (index: number, headId: string) => { if (headId === "splyUswtrUtztnQnt" || headId === "ruseFcltUtztnQnt") { - rswtChkPolAndGuide(makeTableData[index].chkgdnPk); + rswtChkPolAndGuide(makeTableData[index].chkgdnSn); } }, [makeTableData, handleRswtClick] @@ -67,19 +78,18 @@ export default function RswtChkgdTable({ }, [rswtChkgdn.rswtChkPolGuide]); useEffect(() => { - console.log("makeTableData ", makeTableData) getRswtSttnExcel(makeTableData) const selectedData = makeTableData .map((item) => { return { - chkgdnFk: item.chkgdnFk, + chkgdnSn: item.chkgdnSn, utztnUsgCd: item.utztnUsgCd, vltnDt: item.vltnDt, vltnMttr: item.vltnMttr, levyAmt: Number(item.levyAmt), }; }) - .filter((item) => selectedIds.includes(item.chkgdnFk)); + .filter((item) => selectedIds.includes(item.chkgdnSn)); if (selectedData) setUpRswttDetail(selectedData); }, [makeTableData, selectedIds]); @@ -111,25 +121,24 @@ export default function RswtChkgdTable({ }, [selectedIds]); ///현황일 경우 input 비활성화 한다. - useEffect(() => { - if(rswtChkSttn.isSttn){ + useEffect(() => { + if (rswtChkSttn.isSttn) { setHeadList({ headList: rswtChkSttn.rswtChkSttnHeadList, upHeadList: rswtChkSttn.rswtSttnHeadNm }) - }else{ + } else { setHeadList({ headList: rswtChkgdn.rswtChkHeadList, upHeadList: rswtChkgdn.rswtHeadNm }) } - - }, [rswtChkSttn.isSttn, rswtChkgdn.rswtChkHeadList , rswtChkSttn.rswtChkSttnHeadList]); - + }, [rswtChkSttn.isSttn, rswtChkgdn.rswtChkHeadList, rswtChkSttn.rswtChkSttnHeadList]); return ( <> + <LoadingOverlay isLoading={rswtChkgdn.isLoading}></LoadingOverlay> <Table variant="secondary"> <TableHeader className="font-bold"> {headMakeprfmnc(headList?.headList, headList?.upHeadList)} @@ -142,10 +151,17 @@ export default function RswtChkgdTable({ handleChange, toggleSelection, selectedIds, - handleCellClick + handleCellClick, + readonly )} </TableBody> </Table> + <UiPageNations + total={pageCount} + cur={pageNation} + pageSize={rswtPageSize} + onPageChange={onPageChange} + /> </> ); } diff --git a/apps/wr/components/ui/table/pbnstReg/ruseMngTable/ruseMngTable.tsx b/apps/wr/components/ui/table/pbnstReg/ruseMngTable/ruseMngTable.tsx index e4ffc1f8d..07e7c1e8d 100644 --- a/apps/wr/components/ui/table/pbnstReg/ruseMngTable/ruseMngTable.tsx +++ b/apps/wr/components/ui/table/pbnstReg/ruseMngTable/ruseMngTable.tsx @@ -35,11 +35,14 @@ const RuseMngTable = React.forwardRef<any, RuseMngTableProps>( { tableData, headList, upHeadList, code, onTableSet, indexMng, indexBiz }, ref ) => { + + const [tableState, setTableState] = useState([...tableData]); useEffect(() => { if (ref && typeof ref === "object" && "current" in ref) { ref.current = tableState; } + if (onTableSet) onTableSet(tableState); }, [tableState, onTableSet]); @@ -152,4 +155,6 @@ const RuseMngTable = React.forwardRef<any, RuseMngTableProps>( ); } ); +RuseMngTable.displayName = "RuseMngTable"; + export default RuseMngTable; diff --git a/apps/wr/components/ui/table/pbnstReg/ruseMngTableForm/ruseMngTableForm.tsx b/apps/wr/components/ui/table/pbnstReg/ruseMngTableForm/ruseMngTableForm.tsx index acb923687..b13160e8b 100644 --- a/apps/wr/components/ui/table/pbnstReg/ruseMngTableForm/ruseMngTableForm.tsx +++ b/apps/wr/components/ui/table/pbnstReg/ruseMngTableForm/ruseMngTableForm.tsx @@ -1,8 +1,11 @@ import { headMakeprfmnc } from "@/app/ruseFcltMng/prfmncReg/chkgdnReg/TableHead"; import type { TableUpperProps } from "@/app/store/common/type/header"; import type { chHeadType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList"; -import type { customPbnst as OriginalCustomPbnst } from "@/app/store/ruseFcltMng/fcltReg/pbnstReg/type"; import { selectClass } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; +import { + formatCommaNumber, + handleCommaNumberChange, +} from "@/components/lib/func/commom"; import { DatePicker, @@ -28,19 +31,32 @@ type RuseMngTableProps = { headList: TableUpperProps[]; upHeadList: chHeadType[]; code?: { name: string; val: SelectDataType[] }[]; - onTableSet?: (item: any) => void; indexMng?: number; indexBiz?: number; form?: any; - formNm?:string + formNm?: string; + readonly?: boolean; }; const RuseMngTableForm = React.forwardRef<any, RuseMngTableProps>( - ({ tableData, headList, upHeadList, code, onTableSet, indexMng, form,formNm,indexBiz }, ref) => { - + ( + { + tableData, + headList, + upHeadList, + code, + indexMng, + form, + formNm, + indexBiz, + readonly= false + }, + ref + ) => { return ( <> - <Table key={`table-${indexMng}-${indexBiz}`} variant="secondary"> + <Table key={`${formNm}-table-${indexMng}-${indexBiz}`} variant="secondary" + ref={ref}> <TableHeader className="font-bold"> {upHeadList ? ( headMakeprfmnc(headList, upHeadList) @@ -60,39 +76,45 @@ const RuseMngTableForm = React.forwardRef<any, RuseMngTableProps>( <TableBody> {tableData.map((item, i) => ( - <TableRow key={i}> + <TableRow key={`${item.id }-${i}` } > {headList.map((head, j) => ( <TableCell key={`${head.id}-${j}`} className="text-center whitespace-nowrap" - onClick={() => {}} + onClick={() => { }} > <FormField control={form.control} name={ typeof indexMng === "number" && - indexMng >= 0 && - typeof indexBiz === "number" && - indexBiz >= 0? - `items.${indexMng}.fcBizs.${indexBiz}.${formNm}.${i}.${head.id}`: - typeof indexMng === "number" && indexMng >= 0? - `items.${indexMng}.${formNm}.${i}.${head.id}`: - `items.${indexMng}.${formNm}.${i}.${head.id}`} + indexMng >= 0 && + typeof indexBiz === "number" && + indexBiz >= 0 + ? `items.${indexMng}.fcBizs.${indexBiz}.${formNm}.${i}.${head.id}` + : typeof indexMng === "number" && indexMng >= 0 + ? `items.${indexMng}.${formNm}.${i}.${head.id}` + : `items.${indexMng}.${formNm}.${i}.${head.id}` + } render={({ field }) => ( <FormUi label=""> {head.edit === true ? ( <Input className={`${head.type === "number" ? "text-right" : "text-center"} !text-xl`} - type={head.type} + type="text" onChange={(txt) => { - field.onChange(txt.target.value); + if (head.type === "number") { + handleCommaNumberChange(txt, field.onChange); + } else { + field.onChange(txt.target.value); + } }} value={ head.type === "number" - ? (Number(field.value) ?? 0) + ? formatCommaNumber(field.value) : field.value || "" } - readOnly ={head.readonly ? true : false} + readOnly={readonly|| head.readonly ? true : false} + maxLength={head.maxLength?head.maxLength:15} /> ) : head.type === "select" ? ( <SelectBox @@ -102,47 +124,56 @@ const RuseMngTableForm = React.forwardRef<any, RuseMngTableProps>( (item) => item.name === head.selectData )?.val ?? [] } - value={ - field.value || "" - } + value={field.value || ""} className={""} selectClass={selectClass} onSelectValue={(val) => { - - if(head.readonly === true){ - return - }else{ - + if (head.readonly === true) { + return; + } else { field.onChange(val); } }} - readOnly ={head.readonly ? true : false} - // Disabled prop removed as SelectBox does not support it + readOnly={ readonly||head.readonly ? true : false} + // Disabled prop removed as SelectBox does not support it ></SelectBox> ) : head.type === "date" ? ( <DatePicker mode="single" field={{ - value:typeof field.value === "string" ? new Date(field.value) : field.value, + value: + typeof field.value === "string" + ? new Date(field.value) + : field.value, onChange: (date: Date | undefined) => { - console.log("date", date); - field.onChange(date); // readonly 상태가 아닐 때만 값 변경 + field.onChange(date); // readonly 상태가 아닐 때만 값 변경 }, }} selectClass={"w-full"} - readOnly ={head.readonly ? true : false} + readOnly={readonly||head.readonly ? true : false} /> ) : head.type === "checkbox" ? ( <Checkbox className="" - checked={ field.value || false} + checked={field.value || false} onCheckedChange={(checked) => { - field.onChange(checked); + if(readonly || head.readonly) { + return; + }else{ + field.onChange(checked); + } }} ></Checkbox> ) : ( - `${item[head.id] || ""}` + ( + + <span className="text-black">{ + head.type === "number" + ? formatCommaNumber(field.value) + : field.value || "" + }</span> + ) )} </FormUi> )} @@ -157,4 +188,7 @@ const RuseMngTableForm = React.forwardRef<any, RuseMngTableProps>( ); } ); + +RuseMngTableForm.displayName = "RuseMngTableForm"; + export default RuseMngTableForm; diff --git a/apps/wr/eslint.config.mjs b/apps/wr/eslint.config.mjs index c85fb67c4..73d6b7492 100644 --- a/apps/wr/eslint.config.mjs +++ b/apps/wr/eslint.config.mjs @@ -1,6 +1,6 @@ +import { FlatCompat } from "@eslint/eslintrc"; import { dirname } from "path"; import { fileURLToPath } from "url"; -import { FlatCompat } from "@eslint/eslintrc"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -11,6 +11,10 @@ const compat = new FlatCompat({ const eslintConfig = [ ...compat.extends("next/core-web-vitals", "next/typescript"), + {rules:{ + // other rules.. + "@typescript-eslint/no-explicit-any": "off", + }}, ]; export default eslintConfig; diff --git a/apps/wr/middleware.ts b/apps/wr/middleware.ts index 330252074..61f41d634 100644 --- a/apps/wr/middleware.ts +++ b/apps/wr/middleware.ts @@ -1,38 +1,67 @@ import { NextRequest, NextResponse } from "next/server"; import { CS_URL } from "./components/lib/config"; -//import { CLAIM_NAME, AUTH_USER_ID } from "./src/shared/config"; +import { CLAIM_NAME, AUTH_USER_ID } from "./src/shared/config"; -const CLAIM_NAME = "Authorization"; -const AUTH_USER_ID = "token-id"; +// const CLAIM_NAME = "Authorization"; +// const AUTH_USER_ID = "token-id"; const serverApiUrl = CS_URL; +const GEOSERVER_URL = process.env.NEXT_PUBLIC_GEOSERVER_URL || ""; +const LX_GEOSERVER_URL = process.env.NEXT_PUBLIC_LX_GEOSERVER_URL || ""; +const SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ""; + export function middleware(req: NextRequest) { const pathname = req.nextUrl.pathname; console.log(`[Middleware] Request URL: ${req.url}`); const searchParams = req.nextUrl.searchParams; + // if (pathname.startsWith("/server")) { + // const queryParams = searchParams.toString(); + // const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${ + // queryParams ? `?${queryParams}` : "" + // }`; + // console.log(`[Middleware] Rewriting from '/server' path to: ${newUrl}`); + // return NextResponse.rewrite(new URL(newUrl)); + // } + if (pathname.startsWith("/server")) { - const queryParams = searchParams.toString(); + const decodedParams = Array.from(searchParams.entries()) + .map(([key, value]) => `${key}=${decodeURIComponent(value)}`) + .join("&"); + const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${ - queryParams ? `?${queryParams}` : "" + decodedParams ? `?${decodedParams}` : "" }`; - console.log(`[Middleware] Rewriting from '/server' path to: ${newUrl}`); return NextResponse.rewrite(new URL(newUrl)); } + if (pathname === "/") { + console.log(" maim 화면이동 "); + return NextResponse.redirect(new URL("/map/mainMap", req.url)); + } + + if (pathname.startsWith("/cms-service")) { + const requestHeaders = new Headers(req.headers); + const Authorization = req.cookies.get(CLAIM_NAME)?.value; + const newUrl = `${serverApiUrl}${pathname}`; + + console.log("Authorization cms-service ", Authorization); + if (Authorization) requestHeaders.set(CLAIM_NAME, Authorization); + //if (userId) requestHeaders.set(AUTH_USER_ID, userId); + + console.log( + `[Middleware] Rewriting from '/cms-service' path to: ${newUrl}` + ); + return NextResponse.rewrite(new URL(newUrl), { + headers: requestHeaders, + }); + } + if (pathname.startsWith("/attach-service")) { const requestHeaders = new Headers(req.headers); - const token = - "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhMGVjN2EwNy03NTJhLTRmN2YtODlhNi04NTRlNjNkMmUzYTAiLCJhdXRob3JpdGllcyI6IlJPTEVfQURNSU4sUk9MRV9BTk9OWU1PVVMiLCJtbmdyWW4iOiJZIiwiZXhwIjoxNzQ4ODQ4NDkxfQ.ZiYpKxAMyyoPuUquxsWxfQbMBzYN4tsa7itZ49_OEsdGvbUUmKzrIV6Or7VVbEjtAqhh7W97nqB5EP6GRzFC9Q"; - const userId = "a0ec7a07-752a-4f7f-89a6-854e63d2e3a0"; - //const token = req.cookies.get(CLAIM_NAME)?.value; - //const userId = req.cookies.get(AUTH_USER_ID)?.value; + const token = req.cookies.get(CLAIM_NAME)?.value; + const userId = req.cookies.get(AUTH_USER_ID)?.value; const newUrl = `${serverApiUrl}${pathname}`; - console.log("serverApiUrl ", serverApiUrl); - console.log("pathname ", pathname); - //wr:dev: pathname /attach-service/api/v1/tus/upload/5e60b70a-956e-40c3-be5a-cdd3100bf8ad - - //http://dev.onasys.co.kr:8000/attach-service/api/v1/tus/upload/5e60b70a-956e-40c3-be5a-cdd3100bf8ad if (token) requestHeaders.set(CLAIM_NAME, token); if (userId) requestHeaders.set(AUTH_USER_ID, userId); @@ -44,9 +73,79 @@ export function middleware(req: NextRequest) { }); } + if (pathname.startsWith("/user-service")) { + const requestHeaders = new Headers(req.headers); + const newUrl = `${serverApiUrl}${pathname}`; + + console.log( + `[Middleware] Rewriting from '/user-service' path to: ${newUrl}` + ); + return NextResponse.rewrite(new URL(newUrl), { + headers: requestHeaders, + }); + } + + if (pathname.startsWith("/geoserver")) { + let queryParams = searchParams.toString(); + const typename = searchParams.get("TYPENAME"); + console.log("typename ", typename); + + let newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; + + if ( + typename == "hasudo:tb_il_swage_prcs_fclt" || //하수처리시설 + typename == "hasudo:tb_ms_swage_prcs_zone" || //하수처리구역 + typename == "hasudo:tb_ms_sbmrg_dngr_map" || //침수위험지도 + typename == "hasudo:tb_ms_flood_dngr_map" //도시침수지도 + ) { + newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; + } else if ( + typename == "hasudo:tb_ms_swage_ppl" || //관로 + typename == "hasudo:tb_ms_swage_mhl" || //맨홀 + typename == "hasudo:tb_ms_swage_pmstn" || //펌프장 + typename == "hasudo:tb_ms_stofcbr" //우수토실 + ) { + newUrl = `${LX_GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; + } else if ( + typename == "hasudo:om_dt_untrt_swg_rain" || //중점우수토실 + typename == "hasudo:om_wqlt_station" || //관측소 + typename == "hasudo:tb_wr_bdst_ruse_fclt" || //재이용시설 + typename == "hasudo:tb_wr_rnwtr_utztn_fclt" || //빗물이용시설 + typename == "hasudo:tb_wr_rswtr_fclt" || //중수도시설 + typename == "hasudo:tb_wr_htdfl_fclt" || //온배수시설 + typename == "hasudo:tb_wr_ruse_fclt_dmd_co" || //수요처 + typename == "hasudo:tb_wr_tmpr_dmd_co" //잠재수요처 + ) { + const url = req.nextUrl.clone(); + url.searchParams.set("TYPENAME", typename.replace("hasudo:", "")); + queryParams = url.searchParams.toString(); + newUrl = `${SEWERAGE_API_URL}/v1/map/geometry${queryParams ? `?${queryParams}` : ""}`; + } + + console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`); + return NextResponse.rewrite(new URL(newUrl)); + } + + if (pathname.startsWith("/sewerage-service")) { + const queryParams = searchParams.toString(); + const newUrl = `${SEWERAGE_API_URL}${pathname.replace("/sewerage-service/api", "")}${ + queryParams ? `?${queryParams}` : "" + }`; + console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`); + return NextResponse.rewrite(new URL(newUrl)); + } + return NextResponse.next(); } export const config = { - matcher: ["/server/:path*", "/attach-service/:path*"], + matcher: [ + "/cms-service/:path*", + "/server/:path*", + "/user-service/:path*", + "/attach-service/:path*", + "/geoserver/:path*", + "/sewerage-service/:path*", + "/" + ], }; diff --git a/apps/wr/next.config.ts b/apps/wr/next.config.ts index d157358c9..35ea029a8 100644 --- a/apps/wr/next.config.ts +++ b/apps/wr/next.config.ts @@ -4,6 +4,7 @@ const subDomain = process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13001"; const nextConfig: NextConfig = { + output: "standalone", async rewrites() { return { beforeFiles: [ diff --git a/apps/wr/package.json b/apps/wr/package.json index 44ba5a06b..a29dcd66b 100644 --- a/apps/wr/package.json +++ b/apps/wr/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "next dev -p 3001", "build": "next build", - "start": "next start", + "start": "next start -p 3001", "lint": "next lint" }, "dependencies": { @@ -16,11 +16,14 @@ "@tanstack/react-table": "^8.21.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "i18next": "^25.3.2", + "js-cookie": "^3.0.5", "lucide-react": "^0.474.0", "moment": "^2.30.1", "next": "15.1.7", "react-hook-form": "^7.54.2", "react-hot-toast": "^2.5.2", + "react-i18next": "^15.6.0", "tailwind-merge": "^3.0.1", "tailwindcss-animate": "^1.0.7", "tus-js-client": "^4.3.1", diff --git a/apps/wr/public/images/map/layericon/BML_CBAS_PS.png b/apps/wr/public/images/map/layericon/BML_CBAS_PS.png new file mode 100644 index 000000000..5ff4add3e Binary files /dev/null and b/apps/wr/public/images/map/layericon/BML_CBAS_PS.png differ diff --git a/apps/wr/public/images/map/layericon/BML_ELEV_PS.png b/apps/wr/public/images/map/layericon/BML_ELEV_PS.png new file mode 100644 index 000000000..83970a331 Binary files /dev/null and b/apps/wr/public/images/map/layericon/BML_ELEV_PS.png differ diff --git a/apps/wr/public/images/map/layericon/BML_ETCP_PS.png b/apps/wr/public/images/map/layericon/BML_ETCP_PS.png new file mode 100644 index 000000000..df9e4957b Binary files /dev/null and b/apps/wr/public/images/map/layericon/BML_ETCP_PS.png differ diff --git a/apps/wr/public/images/map/layericon/BML_STMS_PS.png b/apps/wr/public/images/map/layericon/BML_STMS_PS.png new file mode 100644 index 000000000..423f2e8fc Binary files /dev/null and b/apps/wr/public/images/map/layericon/BML_STMS_PS.png differ diff --git a/apps/wr/public/images/map/layericon/BML_SYMB_PS.png b/apps/wr/public/images/map/layericon/BML_SYMB_PS.png new file mode 100644 index 000000000..497589bea Binary files /dev/null and b/apps/wr/public/images/map/layericon/BML_SYMB_PS.png differ diff --git a/apps/wr/public/images/map/layericon/Layer_Select_BTN.png b/apps/wr/public/images/map/layericon/Layer_Select_BTN.png new file mode 100644 index 000000000..1e810feaf Binary files /dev/null and b/apps/wr/public/images/map/layericon/Layer_Select_BTN.png differ diff --git a/apps/wr/public/images/map/layericon/RDL_BYST_PS.png b/apps/wr/public/images/map/layericon/RDL_BYST_PS.png new file mode 100644 index 000000000..1e78cba68 Binary files /dev/null and b/apps/wr/public/images/map/layericon/RDL_BYST_PS.png differ diff --git a/apps/wr/public/images/map/layericon/RDL_CROS_PS.png b/apps/wr/public/images/map/layericon/RDL_CROS_PS.png new file mode 100644 index 000000000..57b5f2ca5 Binary files /dev/null and b/apps/wr/public/images/map/layericon/RDL_CROS_PS.png differ diff --git a/apps/wr/public/images/map/layericon/RDL_ETCT_PS.png b/apps/wr/public/images/map/layericon/RDL_ETCT_PS.png new file mode 100644 index 000000000..29c0b77b4 Binary files /dev/null and b/apps/wr/public/images/map/layericon/RDL_ETCT_PS.png differ diff --git a/apps/wr/public/images/map/layericon/RDL_OCUP_PS.png b/apps/wr/public/images/map/layericon/RDL_OCUP_PS.png new file mode 100644 index 000000000..8a1ac0074 Binary files /dev/null and b/apps/wr/public/images/map/layericon/RDL_OCUP_PS.png differ diff --git a/apps/wr/public/images/map/layericon/RDL_RDSN_PS.png b/apps/wr/public/images/map/layericon/RDL_RDSN_PS.png new file mode 100644 index 000000000..5629de9fb Binary files /dev/null and b/apps/wr/public/images/map/layericon/RDL_RDSN_PS.png differ diff --git a/apps/wr/public/images/map/layericon/RDL_SCLT_PS.png b/apps/wr/public/images/map/layericon/RDL_SCLT_PS.png new file mode 100644 index 000000000..7281dc61d Binary files /dev/null and b/apps/wr/public/images/map/layericon/RDL_SCLT_PS.png differ diff --git a/apps/wr/public/images/map/layericon/RDL_SEPC_PS.png b/apps/wr/public/images/map/layericon/RDL_SEPC_PS.png new file mode 100644 index 000000000..087b4e941 Binary files /dev/null and b/apps/wr/public/images/map/layericon/RDL_SEPC_PS.png differ diff --git a/apps/wr/public/images/map/layericon/RDL_STAT_PS.png b/apps/wr/public/images/map/layericon/RDL_STAT_PS.png new file mode 100644 index 000000000..13d8ea689 Binary files /dev/null and b/apps/wr/public/images/map/layericon/RDL_STAT_PS.png differ diff --git a/apps/wr/public/images/map/layericon/RDL_STLT_PS.png b/apps/wr/public/images/map/layericon/RDL_STLT_PS.png new file mode 100644 index 000000000..489524b5c Binary files /dev/null and b/apps/wr/public/images/map/layericon/RDL_STLT_PS.png differ diff --git a/apps/wr/public/images/map/layericon/RDL_TFSN_PS.png b/apps/wr/public/images/map/layericon/RDL_TFSN_PS.png new file mode 100644 index 000000000..6dfff6696 Binary files /dev/null and b/apps/wr/public/images/map/layericon/RDL_TFSN_PS.png differ diff --git a/apps/wr/public/images/map/layericon/RDL_TRCT_PS.png b/apps/wr/public/images/map/layericon/RDL_TRCT_PS.png new file mode 100644 index 000000000..ed1a72d8a Binary files /dev/null and b/apps/wr/public/images/map/layericon/RDL_TRCT_PS.png differ diff --git a/apps/wr/public/images/map/layericon/RDL_TREE_PS.png b/apps/wr/public/images/map/layericon/RDL_TREE_PS.png new file mode 100644 index 000000000..0ee84dedc Binary files /dev/null and b/apps/wr/public/images/map/layericon/RDL_TREE_PS.png differ diff --git a/apps/wr/public/images/map/layericon/RDL_TRSN_PS.png b/apps/wr/public/images/map/layericon/RDL_TRSN_PS.png new file mode 100644 index 000000000..15675cb31 Binary files /dev/null and b/apps/wr/public/images/map/layericon/RDL_TRSN_PS.png differ diff --git a/apps/wr/public/images/map/layericon/RDL_TSCT_PS.png b/apps/wr/public/images/map/layericon/RDL_TSCT_PS.png new file mode 100644 index 000000000..ffc225159 Binary files /dev/null and b/apps/wr/public/images/map/layericon/RDL_TSCT_PS.png differ diff --git a/apps/wr/public/images/map/layericon/SWL_CLAY_PS.png b/apps/wr/public/images/map/layericon/SWL_CLAY_PS.png new file mode 100644 index 000000000..e603ffa9c Binary files /dev/null and b/apps/wr/public/images/map/layericon/SWL_CLAY_PS.png differ diff --git a/apps/wr/public/images/map/layericon/SWL_CLAY_PS_RED.png b/apps/wr/public/images/map/layericon/SWL_CLAY_PS_RED.png new file mode 100644 index 000000000..0e5450688 Binary files /dev/null and b/apps/wr/public/images/map/layericon/SWL_CLAY_PS_RED.png differ diff --git a/apps/wr/public/images/map/layericon/SWL_DEPT_PS.png b/apps/wr/public/images/map/layericon/SWL_DEPT_PS.png new file mode 100644 index 000000000..fede6c304 Binary files /dev/null and b/apps/wr/public/images/map/layericon/SWL_DEPT_PS.png differ diff --git a/apps/wr/public/images/map/layericon/SWL_MANH_PS.png b/apps/wr/public/images/map/layericon/SWL_MANH_PS.png new file mode 100644 index 000000000..75217f494 Binary files /dev/null and b/apps/wr/public/images/map/layericon/SWL_MANH_PS.png differ diff --git a/apps/wr/public/images/map/layericon/SWL_MANH_PS_BLACK.png b/apps/wr/public/images/map/layericon/SWL_MANH_PS_BLACK.png new file mode 100644 index 000000000..5d2e8a70a Binary files /dev/null and b/apps/wr/public/images/map/layericon/SWL_MANH_PS_BLACK.png differ diff --git a/apps/wr/public/images/map/layericon/SWL_MANH_PS_BLUE.png b/apps/wr/public/images/map/layericon/SWL_MANH_PS_BLUE.png new file mode 100644 index 000000000..e521cb84a Binary files /dev/null and b/apps/wr/public/images/map/layericon/SWL_MANH_PS_BLUE.png differ diff --git a/apps/wr/public/images/map/layericon/SWL_MANH_PS_GRAY.png b/apps/wr/public/images/map/layericon/SWL_MANH_PS_GRAY.png new file mode 100644 index 000000000..e19f3d8b3 Binary files /dev/null and b/apps/wr/public/images/map/layericon/SWL_MANH_PS_GRAY.png differ diff --git a/apps/wr/public/images/map/layericon/SWL_MANH_PS_GREEN.png b/apps/wr/public/images/map/layericon/SWL_MANH_PS_GREEN.png new file mode 100644 index 000000000..4503c13c6 Binary files /dev/null and b/apps/wr/public/images/map/layericon/SWL_MANH_PS_GREEN.png differ diff --git a/apps/wr/public/images/map/layericon/SWL_MANH_PS_RED.png b/apps/wr/public/images/map/layericon/SWL_MANH_PS_RED.png new file mode 100644 index 000000000..b8fa45f9c Binary files /dev/null and b/apps/wr/public/images/map/layericon/SWL_MANH_PS_RED.png differ diff --git a/apps/wr/public/images/map/layericon/SWL_MANH_PS_YELLOW.png b/apps/wr/public/images/map/layericon/SWL_MANH_PS_YELLOW.png new file mode 100644 index 000000000..b7de1fbfd Binary files /dev/null and b/apps/wr/public/images/map/layericon/SWL_MANH_PS_YELLOW.png differ diff --git a/apps/wr/public/images/map/layericon/SWL_SPEW_PS.png b/apps/wr/public/images/map/layericon/SWL_SPEW_PS.png new file mode 100644 index 000000000..d260ea3a6 Binary files /dev/null and b/apps/wr/public/images/map/layericon/SWL_SPEW_PS.png differ diff --git a/apps/wr/public/images/map/layericon/SWL_SPOT_PS.png b/apps/wr/public/images/map/layericon/SWL_SPOT_PS.png new file mode 100644 index 000000000..84c7992e4 Binary files /dev/null and b/apps/wr/public/images/map/layericon/SWL_SPOT_PS.png differ diff --git a/apps/wr/public/images/map/layericon/SWL_SPOT_PS_BLACK.png b/apps/wr/public/images/map/layericon/SWL_SPOT_PS_BLACK.png new file mode 100644 index 000000000..fe67f8f98 Binary files /dev/null and b/apps/wr/public/images/map/layericon/SWL_SPOT_PS_BLACK.png differ diff --git a/apps/wr/public/images/map/layericon/SWL_SPOT_PS_BLUE.png b/apps/wr/public/images/map/layericon/SWL_SPOT_PS_BLUE.png new file mode 100644 index 000000000..691f2f45e Binary files /dev/null and b/apps/wr/public/images/map/layericon/SWL_SPOT_PS_BLUE.png differ diff --git a/apps/wr/public/images/map/layericon/SWL_SPOT_PS_GREEN.png b/apps/wr/public/images/map/layericon/SWL_SPOT_PS_GREEN.png new file mode 100644 index 000000000..d09c4bd97 Binary files /dev/null and b/apps/wr/public/images/map/layericon/SWL_SPOT_PS_GREEN.png differ diff --git a/apps/wr/public/images/map/layericon/SWL_SPOT_PS_GREY.png b/apps/wr/public/images/map/layericon/SWL_SPOT_PS_GREY.png new file mode 100644 index 000000000..32d0bf519 Binary files /dev/null and b/apps/wr/public/images/map/layericon/SWL_SPOT_PS_GREY.png differ diff --git a/apps/wr/public/images/map/layericon/SWL_SPOT_PS_RED.png b/apps/wr/public/images/map/layericon/SWL_SPOT_PS_RED.png new file mode 100644 index 000000000..a2da36f9b Binary files /dev/null and b/apps/wr/public/images/map/layericon/SWL_SPOT_PS_RED.png differ diff --git a/apps/wr/public/images/map/layericon/SWL_VENT_PS.png b/apps/wr/public/images/map/layericon/SWL_VENT_PS.png new file mode 100644 index 000000000..834eeec21 Binary files /dev/null and b/apps/wr/public/images/map/layericon/SWL_VENT_PS.png differ diff --git a/apps/wr/public/images/map/layericon/TL_SPBD_ENTRC.png b/apps/wr/public/images/map/layericon/TL_SPBD_ENTRC.png new file mode 100644 index 000000000..8e7de8593 Binary files /dev/null and b/apps/wr/public/images/map/layericon/TL_SPBD_ENTRC.png differ diff --git a/apps/wr/public/images/map/layericon/TL_SPGF_LOC.png b/apps/wr/public/images/map/layericon/TL_SPGF_LOC.png new file mode 100644 index 000000000..74b0adac3 Binary files /dev/null and b/apps/wr/public/images/map/layericon/TL_SPGF_LOC.png differ diff --git a/apps/wr/public/images/map/layericon/TL_SPOT_NOIN.png b/apps/wr/public/images/map/layericon/TL_SPOT_NOIN.png new file mode 100644 index 000000000..4800beaeb Binary files /dev/null and b/apps/wr/public/images/map/layericon/TL_SPOT_NOIN.png differ diff --git a/apps/wr/public/images/map/layericon/TL_SPRD_BANO.png b/apps/wr/public/images/map/layericon/TL_SPRD_BANO.png new file mode 100644 index 000000000..8d08dca29 Binary files /dev/null and b/apps/wr/public/images/map/layericon/TL_SPRD_BANO.png differ diff --git a/apps/wr/public/images/map/layericon/TL_SPRD_CRSRD.png b/apps/wr/public/images/map/layericon/TL_SPRD_CRSRD.png new file mode 100644 index 000000000..57b5f2ca5 Binary files /dev/null and b/apps/wr/public/images/map/layericon/TL_SPRD_CRSRD.png differ diff --git a/apps/wr/public/images/map/layericon/TL_SPRD_NONM.png b/apps/wr/public/images/map/layericon/TL_SPRD_NONM.png new file mode 100644 index 000000000..e68c05ab6 Binary files /dev/null and b/apps/wr/public/images/map/layericon/TL_SPRD_NONM.png differ diff --git a/apps/wr/public/images/map/layericon/TL_SPRD_RENM.png b/apps/wr/public/images/map/layericon/TL_SPRD_RENM.png new file mode 100644 index 000000000..4800beaeb Binary files /dev/null and b/apps/wr/public/images/map/layericon/TL_SPRD_RENM.png differ diff --git a/apps/wr/public/images/map/layericon/TL_SPRD_RWNM.png b/apps/wr/public/images/map/layericon/TL_SPRD_RWNM.png new file mode 100644 index 000000000..e68c05ab6 Binary files /dev/null and b/apps/wr/public/images/map/layericon/TL_SPRD_RWNM.png differ diff --git a/apps/wr/public/images/map/layericon/UFL_BMAN_PS.png b/apps/wr/public/images/map/layericon/UFL_BMAN_PS.png new file mode 100644 index 000000000..f2f64a641 Binary files /dev/null and b/apps/wr/public/images/map/layericon/UFL_BMAN_PS.png differ diff --git a/apps/wr/public/images/map/layericon/UFL_BOPC_PS.png b/apps/wr/public/images/map/layericon/UFL_BOPC_PS.png new file mode 100644 index 000000000..0f8e99f45 Binary files /dev/null and b/apps/wr/public/images/map/layericon/UFL_BOPC_PS.png differ diff --git a/apps/wr/public/images/map/layericon/UFL_BPIP_PS.png b/apps/wr/public/images/map/layericon/UFL_BPIP_PS.png new file mode 100644 index 000000000..dcc3622a7 Binary files /dev/null and b/apps/wr/public/images/map/layericon/UFL_BPIP_PS.png differ diff --git a/apps/wr/public/images/map/layericon/UFL_BPOL_PS.png b/apps/wr/public/images/map/layericon/UFL_BPOL_PS.png new file mode 100644 index 000000000..4f1102989 Binary files /dev/null and b/apps/wr/public/images/map/layericon/UFL_BPOL_PS.png differ diff --git a/apps/wr/public/images/map/layericon/UFL_BTRS_PS.png b/apps/wr/public/images/map/layericon/UFL_BTRS_PS.png new file mode 100644 index 000000000..557c17089 Binary files /dev/null and b/apps/wr/public/images/map/layericon/UFL_BTRS_PS.png differ diff --git a/apps/wr/public/images/map/layericon/UFL_GMNT_PS.png b/apps/wr/public/images/map/layericon/UFL_GMNT_PS.png new file mode 100644 index 000000000..6081d8b00 Binary files /dev/null and b/apps/wr/public/images/map/layericon/UFL_GMNT_PS.png differ diff --git a/apps/wr/public/images/map/layericon/UFL_GPIP_PS.png b/apps/wr/public/images/map/layericon/UFL_GPIP_PS.png new file mode 100644 index 000000000..5d7e86fd2 Binary files /dev/null and b/apps/wr/public/images/map/layericon/UFL_GPIP_PS.png differ diff --git a/apps/wr/public/images/map/layericon/UFL_GPRS_PS.png b/apps/wr/public/images/map/layericon/UFL_GPRS_PS.png new file mode 100644 index 000000000..79c120f20 Binary files /dev/null and b/apps/wr/public/images/map/layericon/UFL_GPRS_PS.png differ diff --git a/apps/wr/public/images/map/layericon/UFL_GVAL_PS.png b/apps/wr/public/images/map/layericon/UFL_GVAL_PS.png new file mode 100644 index 000000000..e8f7cde2e Binary files /dev/null and b/apps/wr/public/images/map/layericon/UFL_GVAL_PS.png differ diff --git a/apps/wr/public/images/map/layericon/UFL_KMAN_PS.png b/apps/wr/public/images/map/layericon/UFL_KMAN_PS.png new file mode 100644 index 000000000..764ce8ba5 Binary files /dev/null and b/apps/wr/public/images/map/layericon/UFL_KMAN_PS.png differ diff --git a/apps/wr/public/images/map/layericon/UFL_KPOL_PS.png b/apps/wr/public/images/map/layericon/UFL_KPOL_PS.png new file mode 100644 index 000000000..d9fa712cf Binary files /dev/null and b/apps/wr/public/images/map/layericon/UFL_KPOL_PS.png differ diff --git a/apps/wr/public/images/map/layericon/UFL_VALV_PS.png b/apps/wr/public/images/map/layericon/UFL_VALV_PS.png new file mode 100644 index 000000000..1b5d70178 Binary files /dev/null and b/apps/wr/public/images/map/layericon/UFL_VALV_PS.png differ diff --git a/apps/wr/public/images/map/layericon/WTL_FIRE_PS.png b/apps/wr/public/images/map/layericon/WTL_FIRE_PS.png new file mode 100644 index 000000000..4137a3f74 Binary files /dev/null and b/apps/wr/public/images/map/layericon/WTL_FIRE_PS.png differ diff --git a/apps/wr/public/images/map/layericon/WTL_FLOW_PS.png b/apps/wr/public/images/map/layericon/WTL_FLOW_PS.png new file mode 100644 index 000000000..285f7edfa Binary files /dev/null and b/apps/wr/public/images/map/layericon/WTL_FLOW_PS.png differ diff --git a/apps/wr/public/images/map/layericon/WTL_MANH_PS.png b/apps/wr/public/images/map/layericon/WTL_MANH_PS.png new file mode 100644 index 000000000..5f2a57cfe Binary files /dev/null and b/apps/wr/public/images/map/layericon/WTL_MANH_PS.png differ diff --git a/apps/wr/public/images/map/layericon/WTL_META_PS.png b/apps/wr/public/images/map/layericon/WTL_META_PS.png new file mode 100644 index 000000000..22c7af31b Binary files /dev/null and b/apps/wr/public/images/map/layericon/WTL_META_PS.png differ diff --git a/apps/wr/public/images/map/layericon/WTL_PIPE_PS.png b/apps/wr/public/images/map/layericon/WTL_PIPE_PS.png new file mode 100644 index 000000000..c9c6863f6 Binary files /dev/null and b/apps/wr/public/images/map/layericon/WTL_PIPE_PS.png differ diff --git a/apps/wr/public/images/map/layericon/WTL_RSRV_PS.png b/apps/wr/public/images/map/layericon/WTL_RSRV_PS.png new file mode 100644 index 000000000..d7b307a61 Binary files /dev/null and b/apps/wr/public/images/map/layericon/WTL_RSRV_PS.png differ diff --git a/apps/wr/public/images/map/layericon/WTL_RSRV_PS_GRAY.png b/apps/wr/public/images/map/layericon/WTL_RSRV_PS_GRAY.png new file mode 100644 index 000000000..33a667a1e Binary files /dev/null and b/apps/wr/public/images/map/layericon/WTL_RSRV_PS_GRAY.png differ diff --git a/apps/wr/public/images/map/layericon/WTL_VALV_PS.png b/apps/wr/public/images/map/layericon/WTL_VALV_PS.png new file mode 100644 index 000000000..ca2b4788b Binary files /dev/null and b/apps/wr/public/images/map/layericon/WTL_VALV_PS.png differ diff --git a/apps/wr/public/images/map/layericon/default.png b/apps/wr/public/images/map/layericon/default.png new file mode 100644 index 000000000..92bf1f587 Binary files /dev/null and b/apps/wr/public/images/map/layericon/default.png differ diff --git a/apps/wr/public/images/map/layericon/reset.png b/apps/wr/public/images/map/layericon/reset.png new file mode 100644 index 000000000..45f889206 Binary files /dev/null and b/apps/wr/public/images/map/layericon/reset.png differ diff --git a/apps/wr/public/locales/ko/message.json b/apps/wr/public/locales/ko/message.json new file mode 100644 index 000000000..d3c55de5e --- /dev/null +++ b/apps/wr/public/locales/ko/message.json @@ -0,0 +1,30 @@ +{ + "deleteMsg" : "{{name}}가 삭제되었습니다.", + "insertMsg": "{{name}}가 저장되었습니다.", + "updateMsg": "{{name}}가 수정되었습니다.", + "errorMsg": "{{name}} 중 오류가 발생했습니다.", + "fileErrMsg": "빈 파일 또는 파일 오류입니다.", + "selectErrMsg": "{{name}}을(를) 선택해주세요.", + "noChangedErrMsg": "변경사항이 없습니다.", + "dateErrMsg": "시작년도를 종료년도 이후로 설정할 수 없습니다.", + "fileDateErrMsg": "파일업로드 이전에 등록년도를 작성해주세요.", + "isDeleteMsg": "선택한 정보를 삭제하시겠습니까?", + "isUpdateMsg": "수정한 정보를 저장하시겠습니까?", + "isInsertMsg": "등록하시겠습니까?", + "isUpdatetMsg": "수정하시겠습니까?", + "matchFinishMsg" : "{{name}}이(가) 완료되었습니다.", + "excelDownErrMsg": "다운로드할 데이터가 없습니다.", + "isMatchFinishMsg":" 매칭완료를 진행하시겠습니까?", + "isNotMatchFinishMsg":"검토완료중 매칭완료 여부를 체크한 항목이 없습니다.", + "canNotChangeMatStateMsg":"매칭완료된 항목은 상태를 변경할 수 없습니다.", + + + + "alreadyInsertedErrMsg": "사업자번호 {{num}}(은)는 이미 등록되었습니다. 다시 등록해주세요.", + "saveRowErrMsg": "수정을 원하는 행을 선택하고 저장버튼을 눌러주세요.", + "costCalcErrMsg":"공사비 계산 진행 후 저장을 눌러주세요", + "maxYearErrMsg": "최대 {{year}}년까지 선택가능합니다.", + "checkQtyMsg": "요구수량(m³/일)을 확인해주세요 계산할 수 없습니다." + +} + diff --git a/apps/wr/public/sample_coSttn.xlsx b/apps/wr/public/sample_coSttn.xlsx index 836ee002e..222813c49 100644 Binary files a/apps/wr/public/sample_coSttn.xlsx and b/apps/wr/public/sample_coSttn.xlsx differ diff --git a/apps/wr/public/sample_htdflSttn.xlsx b/apps/wr/public/sample_htdflSttn.xlsx index fd1dc5a89..463c1ca87 100644 Binary files a/apps/wr/public/sample_htdflSttn.xlsx and b/apps/wr/public/sample_htdflSttn.xlsx differ diff --git a/apps/wr/public/sample_pbnstPrfmncReg.xlsx b/apps/wr/public/sample_pbnstPrfmncReg.xlsx index 67f0d559e..1f2ff81aa 100644 Binary files a/apps/wr/public/sample_pbnstPrfmncReg.xlsx and b/apps/wr/public/sample_pbnstPrfmncReg.xlsx differ diff --git a/apps/wr/public/sample_rainSttn.xlsx b/apps/wr/public/sample_rainSttn.xlsx index f00a520cf..f3e7a5ba8 100644 Binary files a/apps/wr/public/sample_rainSttn.xlsx and b/apps/wr/public/sample_rainSttn.xlsx differ diff --git a/apps/wr/public/sample_rainy.xlsx b/apps/wr/public/sample_rainy.xlsx index 73a52d89c..ec7577c51 100644 Binary files a/apps/wr/public/sample_rainy.xlsx and b/apps/wr/public/sample_rainy.xlsx differ diff --git a/apps/wr/public/sample_rswtStats.xlsx b/apps/wr/public/sample_rswtStats.xlsx index 702c4cec0..bee059e26 100644 Binary files a/apps/wr/public/sample_rswtStats.xlsx and b/apps/wr/public/sample_rswtStats.xlsx differ diff --git a/apps/wr/public/sample_rswtSttn.xlsx b/apps/wr/public/sample_rswtSttn.xlsx index f64b37ea3..cec85971d 100644 Binary files a/apps/wr/public/sample_rswtSttn.xlsx and b/apps/wr/public/sample_rswtSttn.xlsx differ diff --git a/apps/wr/public/sample_ruseCaseMng.xlsx b/apps/wr/public/sample_ruseCaseMng.xlsx new file mode 100644 index 000000000..96027d557 Binary files /dev/null and b/apps/wr/public/sample_ruseCaseMng.xlsx differ diff --git a/apps/wr/src/entities/auth/api/login-service.ts b/apps/wr/src/entities/auth/api/login-service.ts new file mode 100644 index 000000000..1808f7de8 --- /dev/null +++ b/apps/wr/src/entities/auth/api/login-service.ts @@ -0,0 +1,75 @@ + +import type { LoginService, ILogin } from "../model/login-interface"; +import { tokenManager } from "../lib/token-manager"; +import {PROXY_HOST, ACCESS_TOKEN, AUTH_USER_ID,SITE_ID, CLAIM_NAME, ENDPOINT, JWT_EXPIRATION_TIME } from "../../../shared/config"; + +export class LoginApiService implements LoginService { + private static instance: LoginApiService; + + private constructor() {} + + public static getInstance(): LoginApiService { + if (!LoginApiService.instance) { + LoginApiService.instance = new LoginApiService(); + } + return LoginApiService.instance; + } + + public async login(payload: ILogin): Promise<void> { + try { + console.log(' ENDPOINT.USER_SERVICE.LOGIN ', ENDPOINT.USER_SERVICE.LOGIN) + const response = await fetch( PROXY_HOST+ ENDPOINT.USER_SERVICE.LOGIN, { + method: "POST", + body: JSON.stringify({ + userId: payload.userId, + password: payload.password, + }), + }); + // const data = response.headers.get(ACCESS_TOKEN); + const accessToken = response.headers.get(ACCESS_TOKEN); + const tokenId = response.headers.get(AUTH_USER_ID); + + if (!accessToken || !tokenId) { + throw new Error("Missing required authentication tokens"); + } + tokenManager.setToken(CLAIM_NAME, accessToken) + tokenManager.setToken(AUTH_USER_ID, tokenId) + tokenManager.setToken("site-id-key", SITE_ID) + tokenManager.setupRefreshTimer( + () => this.silentRefresh(), + JWT_EXPIRATION_TIME, + ); + } catch (error) { + + throw new Error(`Login failed: ${error}`); + } + } + + + public async silentRefresh(): Promise<void> { + console.log(' silentRefresh ') + + try { + const response = await fetch(PROXY_HOST+ + `${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`, + { method: "PUT" }, + ); + + const data = await response.json(); + const accessToken = data[ACCESS_TOKEN]; + const tokenId = data[AUTH_USER_ID]; + + if (!accessToken || !tokenId) { + throw new Error("Missing required authentication tokens"); + } + + tokenManager.setToken(CLAIM_NAME, accessToken); + tokenManager.setToken(AUTH_USER_ID, tokenId); + } catch (error) { + tokenManager.clearAuthToken(); + throw new Error(`Silent refresh failed: ${error}`); + } + } +} + +export const LoginApi = LoginApiService.getInstance(); diff --git a/apps/wr/src/entities/auth/index.ts b/apps/wr/src/entities/auth/index.ts new file mode 100644 index 000000000..9dc9dd368 --- /dev/null +++ b/apps/wr/src/entities/auth/index.ts @@ -0,0 +1,2 @@ +export { LoginApi } from "./api/login-service"; +//export { AuthWrapper } from "./lib/auth-wrapper"; diff --git a/apps/wr/src/entities/auth/lib/token-manager.ts b/apps/wr/src/entities/auth/lib/token-manager.ts new file mode 100644 index 000000000..40d32f33e --- /dev/null +++ b/apps/wr/src/entities/auth/lib/token-manager.ts @@ -0,0 +1,59 @@ +import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config"; +import { getCookie, removeCookie, setCookie } from "@/src/shared/lib/cookie"; + +export class TokenManager { + private static instance: TokenManager; + private refreshTimer: NodeJS.Timeout | null = null; + + private constructor() {} + + public static getInstance(): TokenManager { + if (!TokenManager.instance) { + TokenManager.instance = new TokenManager(); + } + return TokenManager.instance; + } + + public getToken(key: string): string | undefined { + return getCookie(key); + } + + public setToken(key: string, value: string): void { + setCookie(key, value, { + secure: window.location.protocol === "https:", + sameSite: "lax", + }); + } + + public removeToken(key: string): void { + removeCookie(key); + } + + public setupRefreshTimer( + callback: () => Promise<void>, + expirationTime: number, + ): void { + if (this.refreshTimer) { + clearTimeout(this.refreshTimer); + } + + this.refreshTimer = setTimeout(() => { + callback().catch((error) => { + console.error("Silent refresh failed:", error); + this.clearAuthToken(); + }); + }, expirationTime - 60000); + } + + public clearAuthToken(): void { + if (this.refreshTimer) { + clearTimeout(this.refreshTimer); + this.refreshTimer = null; + } + + this.removeToken(CLAIM_NAME); + this.removeToken(AUTH_USER_ID); + } +} + +export const tokenManager = TokenManager.getInstance(); diff --git a/apps/wr/src/entities/auth/model/login-interface.ts b/apps/wr/src/entities/auth/model/login-interface.ts new file mode 100644 index 000000000..5aaf53044 --- /dev/null +++ b/apps/wr/src/entities/auth/model/login-interface.ts @@ -0,0 +1,9 @@ +export interface ILogin { + userId: string; + password: string; +} + +export interface LoginService { + login(payload: ILogin): Promise<void>; + silentRefresh(): Promise<void>; +} diff --git a/apps/wr/src/shared/config/api.ts b/apps/wr/src/shared/config/api.ts new file mode 100644 index 000000000..b0d1a5be2 --- /dev/null +++ b/apps/wr/src/shared/config/api.ts @@ -0,0 +1,56 @@ +import { PROXY_HOST } from "./env"; + +export const BASE_URL = `${PROXY_HOST}`; + +export const ENDPOINT = { + ATTACH_SERVICE: { + TUS_UPLOAD: "/attach-service/api/v1/tus/upload", + EDITOR_UPLOAD: "/attach-service/api/v1/upload/editor", + IMAGES: "/attach-service/api/v1/images", + MOVIE_THUMBNAIL: "/attach-service/api/v1/movie/thumbnail", + ATTACHMENTS: "/attach-service/api/v1/atfl/attachments", + DOWNLOAD: "/attach-service/api/v1/atflcd/download", + STREAM_VIDEO: "/attach-service/api/v1/stream/video", + }, + USER_SERVICE: { + LOGIN: "/user-service/login", + USERS: "/user-service/api/v1/users", + SILENT_REFRESH: "/user-service/api/v1/users/token/refresh", + AUTHORIZATIONS: "/user-service/api/v1/authorizations", + ROLE_AUTHORIZATIONS: "/user-service/api/v1/role-authorizations", + ROLES: "/user-service/api/v1/roles", + }, + CMS_SERVICE: { + SITES: "/cms-service/api/v1/sites", + IPS: "/cms-service/api/v1/site-ips", + DOMAINS: "/cms-service/api/v1/site-dmns", + POSTINGS: "/cms-service/api/v1/site-pstgs", + CODE: "/cms-service/api/v1/code/cache", + MENUS: "/cms-service/api/v1/menus", + MENU_ROLES: "/cms-service/api/v1/menu-roles", + MENU_AUTHRTS: "/cms-service/api/v1/menu-authrts", + CONTENTS: "/cms-service/api/v1/contents", + CONTENTS_DETAILS: "/cms-service/api/v1/contents-details", + ORGANIZATIONS: "/cms-service/api/v1/ognzs", + USER_ORGANIZATION: "/cms-service/api/v1/user-ognz", + IMAGES: "/cms-service/api/v1/images", + BANNERS: "/cms-service/api/v1/bnrs", + TEMPLATE: "/cms-service/api/v1/tmplt", + TERMS: "/cms-service/api/v1/trms", + LWPO_CODE: "/cms-service/api/v1/lwpo-cds", + UP_CODE: "/cms-service/api/v1/up-codes", + POPUPS: "/cms-service/api/v1/popups", + SCHEDULES: "/cms-service/api/v1/schdls", + SURVEY: "/cms-service/api/v1/srvy", + SURVEY_QUESTION: "/cms-service/api/v1/srvy-qitem", + SURVEY_ARTICLE: "/cms-service/api/v1/srvy-artcl", + }, + BOARD_SERVICE: { + BBS: "/board-service/api/v1/bbs", + BBS_TEMPLATES: "/board-service/api/v1/bbsTmplt", + POSTS: "/board-service/api/v1/posts", + POSTS_COMMENTS: "/board-service/api/v1/posts-cmnts", + }, +}; + +export const JWT_EXPIRATION_TIME = 1800000; diff --git a/apps/wr/src/shared/config/env.ts b/apps/wr/src/shared/config/env.ts new file mode 100644 index 000000000..acd0e25e2 --- /dev/null +++ b/apps/wr/src/shared/config/env.ts @@ -0,0 +1,15 @@ +export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "WR"; +export const PORT = process.env.NEXT_PUBLIC_PORT || "3001"; +export const PROXY_HOST = process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`; +export const SERVER_API_WR_URL = process.env.NEXT_PUBLIC_WR_SERVER_API_URL ||'http://con.mind-one.co.kr:11080/api/v1'; +export const SERVER_API_FO_URL = process.env.NEXT_PUBLIC_FO_SERVER_API_URL||' http://con.mind-one.co.kr:11082/api/v1'; +export const SERVER_API_CS_URL = process.env.NEXT_PUBLIC_CS_SERVER_API_URL||'http://dev.onasys.co.kr:8000'; +export const SERVER_API_CS_TUS_URL = process.env.NEXT_PUBLIC_CS_TUS_UPLOAD ||'/attach-service/api/v1/tus/upload' +export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; +export const ACCESS_TOKEN = process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token"; +export const REFRESH_TOKEN =process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token"; +export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; +export const SERVER_SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL; + + + diff --git a/apps/wr/src/shared/config/index.ts b/apps/wr/src/shared/config/index.ts new file mode 100644 index 000000000..cd83447d5 --- /dev/null +++ b/apps/wr/src/shared/config/index.ts @@ -0,0 +1,2 @@ +export * from "./api"; +export * from "./env"; diff --git a/apps/wr/src/shared/lib/cookie.ts b/apps/wr/src/shared/lib/cookie.ts new file mode 100644 index 000000000..4bc0fe142 --- /dev/null +++ b/apps/wr/src/shared/lib/cookie.ts @@ -0,0 +1,31 @@ + +import Cookies from "js-cookie"; + + +export function getCookie(name: string): string | undefined { + if (typeof window === "undefined") { + return undefined; + } + return Cookies.get(name); +} + +export function setCookie( + name: string, + value: string, + options?: Cookies.CookieAttributes, +): void { + if (typeof window === "undefined") { + return; + } + Cookies.set(name, value, options); +} + +export function removeCookie( + name: string, + options?: Cookies.CookieAttributes, +): void { + if (typeof window === "undefined") { + return; + } + Cookies.remove(name, options); +} diff --git a/apps/wr/tsconfig.json b/apps/wr/tsconfig.json index d897e147f..493584782 100644 --- a/apps/wr/tsconfig.json +++ b/apps/wr/tsconfig.json @@ -2,6 +2,7 @@ "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../tsconfig.base.json", "compilerOptions": { + "noImplicitAny": false, "baseUrl": ".", "target": "ES2017", "lib": ["dom", "dom.iterable", "esnext"], diff --git a/package.json b/package.json index 038d81914..89154a13d 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,14 @@ "packages/*" ], "scripts": { - "build": "turbo run build --filter=!wr", + "build": "turbo run build", "build:common": "turbo run build --filter=@common/*", "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", + "dev:fo": "turbo run dev --filter=fo", + "dev:dm": "turbo run dev --filter=dm", "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 e7aae9fc5..c56278dd3 100644 --- a/packages/common/business_components/package.json +++ b/packages/common/business_components/package.json @@ -35,17 +35,24 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^3.6.0", + "libphonenumber-js": "^1.12.9", "lucide-react": "^0.474.0", + "ol": "^7.3.0", + "ol-ext": "^4.0.11", + "proj4": "^2.9.0", "path": "^0.12.7", "react-day-picker": "^8.10.1", "react-dropzone": "^14.3.8", "react-hook-form": "^7.54.2", + "react-dnd": "^16.0.1", "recharts": "^2.15.1", "sonner": "^2.0.1", "tailwind-merge": "^3.0.1", + "tailwindcss-animate": "^1.0.7", "xlsx": "^0.18.5", "xlsx-populate": "^1.21.0", - "zod": "^3.24.2" + "zod": "^3.24.2", + "@minoru/react-dnd-treeview": "3.5.1-alpha.1" }, "devDependencies": { "@eslint/js": "^9.17.0", diff --git a/packages/common/business_components/src/ui/chart/BarChart.tsx b/packages/common/business_components/src/ui/chart/BarChart.tsx index 0802a72e4..79aa61c98 100644 --- a/packages/common/business_components/src/ui/chart/BarChart.tsx +++ b/packages/common/business_components/src/ui/chart/BarChart.tsx @@ -6,6 +6,7 @@ import { type ChartConfig } from "./lib/chart"; import type { CSSProperties } from "react"; +import { formatCommaNumber } from "./lib/function"; const customStyles = { legendContainer: { @@ -74,17 +75,25 @@ const CustomLegend = (props: any) => { ); }; -export default function Chart({chartConfig=undefined, chartData=[], chartTitle='차트', chartClassName='w-full h-full', graphSize=40, yFieldText='', showLegend=false} : BarChartProps) { - const colors = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEEAD", "#D4A5A5"]; - const getMaxValue = (data: any[], keyPrefix: string) => { - return Math.max(...data.map(item => - Object.keys(item) - .filter(key => key.includes(keyPrefix)) - .map(key => item[key]) - ).flat()); - }; +// chartData의 모든 값 중 최대값 구하기 +const getGlobalMax = (data: Record<string, number>[]) => { + if (!data || data.length === 0) return 0; + return Math.max( + ...data.flatMap(item => + Object.entries(item) + .filter(([key]) => key !== 'label') + .map(([, value]) => value as number) + ) + ); +}; - const barMaxValue = getMaxValue(chartData, 'barVal'); +export default function Chart({chartConfig=undefined, chartData=[], chartTitle='차트', chartClassName='w-full h-full', graphSize=40, yFieldText='', showLegend=false} : BarChartProps) { + const colors = ["#FF6B6B", "#4ECDC4", "#44ad57", "#FED16A", "#FFEEAD", "#D4A5A5"]; + const globalMax = getGlobalMax(chartData); + const tickCount = 5; + // interval을 globalMax / tickCount로 계산 + const interval = globalMax / tickCount; + const ticks = Array.from({ length: tickCount + 1 }, (_, i) => Math.round(interval * i)); return ( <> {chartTitle && chartTitle !== "" && chartTitle !== "undefined" ? ( @@ -93,8 +102,8 @@ export default function Chart({chartConfig=undefined, chartData=[], chartTitle=' </div>): null} <div className="flex justify-center"> <ChartContainer config={chartConfig ?? {}} className={chartClassName + " !w-[90%]"}> - <BarChart accessibilityLayer data={chartData}> - <CartesianGrid vertical={false} /> + <BarChart accessibilityLayer data={chartData} className="!h-[88%]"> + <CartesianGrid vertical={false} className="!h-[85%]" /> <XAxis dataKey="label" tickLine={true} @@ -106,12 +115,13 @@ export default function Chart({chartConfig=undefined, chartData=[], chartTitle=' <YAxis tickLine={true} tickMargin={10} - tickCount={6} axisLine={true} fontSize={12} fontWeight={500} - domain={[0, Math.ceil(barMaxValue * 1)]} - label={yFieldText ? { value: yFieldText, className:"text-[13px] font-bold", position: 'insideLeft', angle: -90, offset: 20 } : undefined} + domain={[0, globalMax]} + ticks={ticks} + tickFormatter={(value) => formatCommaNumber(value)} + label={yFieldText ? { value: yFieldText, className:"text-[15px] font-bold", position: 'insideLeft', angle: -90, offset: -30 } : undefined} /> <ChartTooltip cursor={false} content={<CustomTooltip active={undefined} payload={undefined} label={undefined} />}/> {chartData && chartData.length > 0 && ( diff --git a/packages/common/business_components/src/ui/chart/DoubleBar.tsx b/packages/common/business_components/src/ui/chart/DoubleBar.tsx index 9627485aa..be930f3af 100644 --- a/packages/common/business_components/src/ui/chart/DoubleBar.tsx +++ b/packages/common/business_components/src/ui/chart/DoubleBar.tsx @@ -83,7 +83,6 @@ export default function DoubleBarChart({chartConfig=undefined, chartData=[], cha .map(key => item[key]) ).flat()); }; - console.log(chartData); const barMaxValue = getMaxValue(chartData, 'barVal'); return ( <> diff --git a/packages/common/business_components/src/ui/chart/DoughnutChart.tsx b/packages/common/business_components/src/ui/chart/DoughnutChart.tsx index 39dee3d9e..ec8fc2fd9 100644 --- a/packages/common/business_components/src/ui/chart/DoughnutChart.tsx +++ b/packages/common/business_components/src/ui/chart/DoughnutChart.tsx @@ -6,22 +6,13 @@ import { Card, CardContent } from "./lib/card"; -import { ChartContainer, ChartTooltip } from "./lib/chart"; +import { ChartContainer, ChartTooltip, type ChartConfig } from "./lib/chart"; type ChartDataItem = { label: string; val: number; }; -type ChartConfigItem = { - label: string; - fill: string; -}; - -type ChartConfig = { - [key: string]: ChartConfigItem; -}; - type TooltipPayloadItem = { name: string; value: number; @@ -47,23 +38,52 @@ type LegendProps = { payload?: LegendEntry[]; }; -/** 차트 기본 스타일 */ +/** 차트 기본 스타일 (라인차트와 동일하게) */ const customStyles = { legendContainer: { display: "flex", + flexWrap: "wrap", // 여러 줄로 자동 줄바꿈 justifyContent: "center", - gap: "20px", - fontSize: "9px", - fontWeight: "bold", - marginTop: "10px", + alignItems: "center", + gap: "12px", // 아이템 간 간격 + fontSize: "13px", + fontWeight: "normal", + marginTop: "16px", + marginBottom: "8px", + minHeight: "60px", // 범례 영역 높이 통일 } as CSSProperties, }; +const renderCustomizedLabel = ({ + cx, + cy, + midAngle, + outerRadius, + payload, +}: any) => { + const RADIAN = Math.PI / 180; + const radius = outerRadius + 25; + const x = cx + radius * Math.cos(-midAngle * RADIAN); + const y = cy + radius * Math.sin(-midAngle * RADIAN); + + return ( + <text + x={x} + y={y} + fill="#333" + fontSize={11} + textAnchor={x > cx ? "start" : "end"} + dominantBaseline="central" + > + {`${payload.label}: ${payload.val.toLocaleString()}`} + </text> + ); +}; // 커스텀 툴팁 -const CustomTooltip = ({ active, payload, label }: { - active?: boolean; - payload?: TooltipPayloadItem[]; - label?: string +const CustomTooltip = ({ active, payload, label }: { + active?: boolean; + payload?: TooltipPayloadItem[]; + label?: string }) => { if (active && payload && payload.length) { return ( @@ -81,40 +101,68 @@ const CustomTooltip = ({ active, payload, label }: { return null; }; -// 커스텀 범례 const CustomLegend = (props: LegendProps) => { const { payload } = props; if (!payload) return null; - return ( <div style={customStyles.legendContainer}> {payload.map((entry: LegendEntry, index: number) => ( - <div key={`legend-${index}`} className="flex items-center gap-2"> - <div className="w-[14px] h-[14px] ml-10" style={{ backgroundColor: entry.fill }}></div> - <span className="text-[12px]">{entry.value}</span> + <div + key={`legend-${index}`} + className="flex items-center" + style={{ + minWidth: "90px", + margin: "2px 0", + }} + > + <div + style={{ + width: "14px", + height: "14px", + backgroundColor: entry.fill, + borderRadius: "3px", + marginRight: "8px", + minWidth: "14px", + minHeight: "14px", + }} + ></div> + <span style={{ fontSize: "13px", whiteSpace: "nowrap" }}> + {entry.payload?.label || entry.value} + </span> </div> ))} </div> ); }; -export default function DoughnutChart(props: { - chartConfig: any; - chartData: ChartDataItem[] | undefined; +export default function DoughnutChart(props: { + chartConfig: ChartConfig; + chartData: ChartDataItem[] | undefined; chartTitle: string }) { + const legendData = props.chartData?.map(item => ({ + value: item.label, + fill: props.chartConfig?.[item.label]?.color || 'blue', + payload: { + label: item.label, + val: item.val, + fill: props.chartConfig?.[item.label]?.color || 'blue' + } + })) ?? []; + return ( <Card className="flex flex-col"> <CardContent className="flex-1 pb-0"> {props.chartTitle && props.chartTitle !== "" && props.chartTitle !== "undefined" ? ( - <div style={{ textAlign: "left", fontSize: "13px", fontWeight: "bold", marginTop:"13px", marginBottom: "10px" }}> + <div style={{ textAlign: "left", fontSize: "13px", fontWeight: "bold", marginTop: "13px", marginBottom: "10px" }}> {props.chartTitle} </div> ) : null} <div className="h-[calc(100%-42px)] justify-center"> <ChartContainer config={props.chartConfig} - className="mx-auto aspect-square max-h-[300px]" + // className="mx-auto aspect-square max-h-[300px]" + className="min-h-[300px] w-full" > <PieChart> <ChartTooltip @@ -124,15 +172,19 @@ export default function DoughnutChart(props: { <Pie data={props.chartData?.map(item => ({ ...item, - fill: props.chartConfig[item.label]?.fill || 'blue' + fill: props.chartConfig[item.label]?.color || 'blue' }))} dataKey="val" nameKey="label" innerRadius={60} - /> - <CustomLegend /> + label={(props) => { + return renderCustomizedLabel(props); + }} + > + </Pie> </PieChart> </ChartContainer> + <CustomLegend payload={legendData} /> </div> </CardContent> </Card> diff --git a/packages/common/business_components/src/ui/chart/LineBarChart.tsx b/packages/common/business_components/src/ui/chart/LineBarChart.tsx index 8c7202104..d81d6e2c8 100644 --- a/packages/common/business_components/src/ui/chart/LineBarChart.tsx +++ b/packages/common/business_components/src/ui/chart/LineBarChart.tsx @@ -94,6 +94,7 @@ export default function LineBarchart({ // 데이터의 최대값 계산 const getMaxValue = (data: any[], keyPrefix: string) => { + if (!data || data.length === 0) return 0; return Math.max(...data.map(item => Object.keys(item) .filter(key => key.includes(keyPrefix)) @@ -128,7 +129,7 @@ export default function LineBarchart({ tickCount={6} orientation="left" domain={[0, Math.ceil(lineMaxValue * 1)]} - label={{ value: yFieldText, angle: -90, position: 'insideLeft' }} + label={{ value: yFieldText, angle: -90, position: 'insideLeft', offset: -8 }} />} {/* 오른쪽 Y축: barVal */} {yFieldText2 && <YAxis diff --git a/packages/common/business_components/src/ui/chart/LineBarChart2.tsx b/packages/common/business_components/src/ui/chart/LineBarChart2.tsx new file mode 100644 index 000000000..ff8e6b5cb --- /dev/null +++ b/packages/common/business_components/src/ui/chart/LineBarChart2.tsx @@ -0,0 +1,186 @@ +"use client" +import type { CSSProperties } from "react"; +import { Bar, CartesianGrid, ComposedChart, Legend, Line, Tooltip, XAxis, YAxis } from "recharts"; +import { + ChartContainer, + type ChartConfig +} from "./lib/chart"; +import type { ContentType } from "recharts/types/component/DefaultLegendContent"; + +// 툴크 커스텀 css +/** 차트 기본 스타일 */ +const customStyles = { + legendContainer: { + display: "flex", + justifyContent: "center", + gap: "20px", + fontSize: "9px", + fontWeight : "bold", + marginTop: "10px", + } as CSSProperties, +}; + +type LineBarChartProps = { + chartConfig: ChartConfig | undefined; + chartData: any[] | undefined; + chartTitle: string; + chartClassName?: string; + graphSize?: number; + yFieldText?: string | undefined; + yFieldText2?: string | undefined; + showLegend?: boolean; +} + +type LegendEntry = { + value: string; + color: string; + type?: string; + payload?: unknown; +}; + +type LegendProps = { + payload?: LegendEntry[]; +}; + +// 커스텀 툴팁 +const CustomTooltip = ({ active, payload, label }: { active?: boolean; payload?: Array<{ name: string; value: number; color: string }>; label?: string }) => { + if (active && payload && payload.length) { + return ( + <div className="w-[180px] rounded bg-white p-2.5 shadow-[0px_2px_10px_rgba(0,0,0,0.2)]"> + <p className="text-[12px] font-bold m-3">{label}</p> + {payload.map((item, index) => ( + <div key={index} style={{ marginTop: "10px", display: "flex", alignItems: "center", gap: "8px" }}> + {/* 색상 박스 추가 (범례 색상과 동일) */} + <div style={{ width: "10px", height: "10px", backgroundColor: item.color, borderRadius: "50%" }}></div> + <span className="text-[11px] font-bold">{` ${item.name} : ${item.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}`}</span> + </div> + ))} + </div> + ); + } + return null; +}; + +// 커스텀 범례 (글씨 크기 키우기 + 스타일 추가) +const CustomLegend = (props: LegendProps) => { + const { payload } = props; + if (!payload) return null; + + return ( + <div style={customStyles.legendContainer}> + {payload.map((entry: LegendEntry, index: number) => ( + <div key={`legend-${index}`} className="flex items-center gap-2"> + <div className="w-[14px] h-[14px] ml-10" style={{ backgroundColor: entry.color }}></div> + <span className="text-[12px]">{entry.value}</span> + </div> + ))} + </div> + ); +}; + +export default function LineBarchart2({ + chartConfig, + chartData, + chartTitle, + chartClassName = 'w-full h-full', + graphSize = 40, + yFieldText, + yFieldText2, + showLegend = true +}: LineBarChartProps) { + if (!chartData || chartData.length === 0) { + return <div>데이터가 없습니다.</div>; + } + + // 데이터의 최대값 계산 + const getMaxValue = (data: any[], keyPrefix: string) => { + if (!data || data.length === 0) return 0; + return Math.max(...data.map(item => + Object.keys(item) + .filter(key => key.includes(keyPrefix)) + .map(key => item[key]) + ).flat()); + }; + + const lineMaxValue = getMaxValue(chartData, 'lineVal'); + const barMaxValue = getMaxValue(chartData, 'barVal'); + return ( + <div className={chartClassName}> + {chartTitle && chartTitle !== "" && chartTitle !== "undefined" ? ( + <div style={{ textAlign: "left", fontSize: "13px", fontWeight: "bold", marginTop:"13px", marginBottom: "10px" }}> + {chartTitle} + </div> + ) : null} + <div className="h-[calc(100%-42px)] flex justify-center"> + <ChartContainer config={chartConfig ?? {}} className="h-full w-full"> + <ComposedChart + data={chartData} + height={graphSize * 7.5} + width={graphSize * 12.5} + margin={{ top: 5, right: 30, left: 20, bottom: 5 }} + > + <CartesianGrid strokeDasharray="3 3" /> + <XAxis dataKey="label" fontSize="8px" fontWeight="bold"/> + {/* 왼쪽 Y축: lineVal */} + {yFieldText && <YAxis + yAxisId="left" + className="text-[12px] font-bold" + tickCount={6} + orientation="left" + domain={[0, Math.ceil(lineMaxValue * 1)*2]} + label={{ value: yFieldText, angle: -90, position: 'insideLeft', offset: -8 }} + />} + {/* 오른쪽 Y축: barVal */} + {yFieldText2 && <YAxis + yAxisId="right" + className="text-[12px] font-bold" + tickCount={6} + orientation="right" + domain={[0, Math.ceil(barMaxValue * 1)*2]} + label={{ value: yFieldText2, angle: -90, position: 'insideRight' }} + reversed={true} + />} + <Tooltip content={<CustomTooltip active={undefined} payload={undefined} label={undefined} />} /> + {showLegend && <Legend content={CustomLegend as ContentType}/>} + {/* 바 차트 (오른쪽 Y축 사용) */} + {Object.keys(chartData[0]).map((key, index) => { + const config = chartConfig?.[key]; + if(key.includes("barVal")) { + return ( + <Bar + key={`bar-${key}-${index}`} + yAxisId="right" + dataKey={key} + fill={config?.color || "black"} + name={String(config?.label || "Y축")} + maxBarSize={80} + /> + ); + } + return null; + })} + {/* 라인 차트 (왼쪽 Y축 사용) */} + {Object.keys(chartData[0]).map((key, index) => { + const config = chartConfig?.[key]; + if(key.includes("lineVal")) { + return ( + <Line + key={`line-${key}-${index}`} + yAxisId="left" + type="linear" + dataKey={key} + stroke={config?.color || "black"} + strokeWidth={2} + dot={{r: 0}} + name={String(config?.label || "X축")} + /> + ); + } + return null; + })} + </ComposedChart> + </ChartContainer> + </div> + </div> + ); +} \ No newline at end of file diff --git a/packages/common/business_components/src/ui/chart/LineChart.tsx b/packages/common/business_components/src/ui/chart/LineChart.tsx index 4014702b6..23570912d 100644 --- a/packages/common/business_components/src/ui/chart/LineChart.tsx +++ b/packages/common/business_components/src/ui/chart/LineChart.tsx @@ -1,11 +1,11 @@ "use client" import type { CSSProperties } from "react"; import { CartesianGrid, Legend, Line, LineChart, Tooltip, XAxis, YAxis } from "recharts"; +import type { ContentType } from "recharts/types/component/DefaultLegendContent"; import { ChartContainer, type ChartConfig } from "./lib/chart"; -import type { ContentType } from "recharts/types/component/DefaultLegendContent"; type LineChartProps = { chartConfig: ChartConfig | undefined; @@ -18,6 +18,7 @@ type LineChartProps = { } /** 차트 기본 스타일 */ +/* const customStyles = { legendContainer: { display: "flex", @@ -28,6 +29,23 @@ const customStyles = { marginTop: "10px", } as CSSProperties, }; +*/ + +/** 차트 기본 스타일 */ +const customStyles = { + legendContainer: { + display: "flex", + flexWrap: "wrap", + justifyContent: "center", + alignItems: "center", + gap: "12px", + fontSize: "13px", + fontWeight: "normal", + marginTop: "16px", + marginBottom: "8px", + minHeight: "32px", + } as CSSProperties, +}; type LegendEntry = { value: string; @@ -60,7 +78,7 @@ const CustomTooltip = ({ active, payload, label }: { active?: boolean; payload?: }; // 커스텀 범례 (글씨 크기 키우기 + 스타일 추가) -const CustomLegend = (props: LegendProps) => { +/*const CustomLegend = (props: LegendProps) => { const { payload } = props; if (!payload) return null; @@ -74,6 +92,40 @@ const CustomLegend = (props: LegendProps) => { ))} </div> ); +};*/ + +// 커스텀 범례 (도넛차트와 동일하게) +const CustomLegend = (props: LegendProps) => { + const { payload } = props; + if (!payload) return null; + + return ( + <div style={customStyles.legendContainer}> + {payload.map((entry: LegendEntry, index: number) => ( + <div + key={`legend-${index}`} + className="flex items-center" + style={{ + minWidth: "90px", + margin: "2px 0", + }} + > + <div + style={{ + width: "14px", + height: "14px", + backgroundColor: entry.color, + borderRadius: "3px", + marginRight: "8px", + minWidth: "14px", + minHeight: "14px", + }} + ></div> + <span style={{ fontSize: "13px", whiteSpace: "nowrap" }}>{entry.value}</span> + </div> + ))} + </div> + ); }; type DataType = {label: string; val: string}; @@ -141,7 +193,7 @@ export default function Linechart({ > <CartesianGrid vertical={false} /> <XAxis dataKey="label" fontSize="8px" fontWeight="bold" /> - <YAxis yAxisId="left" className="text-[12px] font-bold" domain={[0, Math.ceil(lineMaxValue * 1.1)]} tickCount={6} orientation="left" label={{ value: yFieldText, className:"text-[12px] font-bold", angle: -90, position: 'insideLeft' }} /> + <YAxis yAxisId="left" className="text-[12px] font-bold" tickFormatter={(value) => value.toLocaleString()} tickCount={6} orientation="left" label={{ value: yFieldText, className:"text-[12px] font-bold", angle: -90, position: 'insideLeft' }} /> <Tooltip content={<CustomTooltip />} /> {showLegend && <Legend content={CustomLegend as ContentType}/>} {Object.keys(chartData).map((key) => { diff --git a/packages/common/business_components/src/ui/chart/index.tsx b/packages/common/business_components/src/ui/chart/index.tsx index 898a65a79..ee308e378 100644 --- a/packages/common/business_components/src/ui/chart/index.tsx +++ b/packages/common/business_components/src/ui/chart/index.tsx @@ -1,9 +1,9 @@ - import React from 'react'; -import BarChart from './BarChart'; -import DoughnutChart from './DoughnutChart'; -import LineChart from './LineChart'; -import LineBarchart from './LineBarChart'; + import BarChart from './BarChart'; import DoubleBarChart from './DoubleBar'; +import DoughnutChart from './DoughnutChart'; +import LineBarchart from './LineBarChart'; +import LineBarchart2 from './LineBarChart2'; +import LineChart from './LineChart'; /**차트 FORM */ const CHART_FORM = { @@ -15,6 +15,8 @@ const CHART_FORM = { DN :'daughnut', /**LINE,BAR차트 */ LINEBAR : 'linebar', + /**LINE,BAR차트 */ + LINEBAR2 : 'linebar2', /**더블바 차트 형태*/ DOUBLEBAR : 'doublebar' }; @@ -36,7 +38,8 @@ const CHART_FORM = { // 각 차트에서 사용하는 ChartConfig 타입 정의 type ChartConfigItem = { label: string; - fill: string; + fill?: string; + color?: string; }; type ChartConfig = { @@ -72,6 +75,10 @@ function MainChart(props: MainChartProps) { return ( <LineBarchart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} yFieldText2={props.yFieldText2} showLegend={props.showLegend}/> ); + } else if (props.chartForm == CHART_FORM.LINEBAR2){ + return ( + <LineBarchart2 chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} yFieldText2={props.yFieldText2} showLegend={props.showLegend}/> + ); } else if (props.chartForm == CHART_FORM.DOUBLEBAR){ return ( <DoubleBarChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} showLegend={props.showLegend}/> @@ -80,5 +87,6 @@ function MainChart(props: MainChartProps) { } export { MainChart }; +export type { ChartConfig }; // eslint-disable-next-line react-refresh/only-export-components export { CHART_FORM }; diff --git a/packages/common/business_components/src/ui/chart/lib/function.tsx b/packages/common/business_components/src/ui/chart/lib/function.tsx new file mode 100644 index 000000000..dc1921f0a --- /dev/null +++ b/packages/common/business_components/src/ui/chart/lib/function.tsx @@ -0,0 +1,12 @@ +// 숫자입력시 , 삽입 +export const formatCommaNumber = ( + value: number | string | undefined | null +): string => { + if (value === undefined || value === null || value === "") return ""; + const num = Number(value); + // 숫자로 변환할 수 없는 값이 들어올 경우 빈 문자열을 반환하여 예상치 못한 "NaN" 출력을 방지합니다. + if (isNaN(num)) { + return ""; + } + return num.toLocaleString(); +}; \ No newline at end of file diff --git a/packages/common/business_components/src/ui/date_month_picker/index.tsx b/packages/common/business_components/src/ui/date_month_picker/index.tsx new file mode 100644 index 000000000..a8da0ff0e --- /dev/null +++ b/packages/common/business_components/src/ui/date_month_picker/index.tsx @@ -0,0 +1,88 @@ +'use client'; + +import { cn } from "../../lib/utils"; +import { + Popover, + PopoverTrigger, + PopoverContent, + Button, +} from "@common/components/ui"; +import { ko } from "date-fns/locale"; +import { format } from "date-fns"; +import { useState } from "react"; +import { MonthPicker } from "./mothPicker"; +import { CalendarIcon } from "lucide-react"; + +type MonthPickerProps = { + field: { value: string; onChange: (date: string) => void }; // yyyyMM 문자열 + selectClass?: string; + readOnly?: boolean; + startYear?: number; // 시작 연도 props 추가 + yearReadOnly?: boolean; +}; + +// yyyyMM → Date 객체 +const parseYYYYMM = (value: string) => { + const year = Number(value.substring(0, 4)); + const month = Number(value.substring(4, 6)) - 1; + return new Date(year, month); +}; + +// Date 객체 → yyyyMM 문자열 +const formatDateToYYYYMM = (date: Date) => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + return `${year}${month}`; +}; + +export const DateMonthPicker = ({ + field, + selectClass, + readOnly = false, + startYear, + yearReadOnly = false, +}: MonthPickerProps) => { + const [open, setOpen] = useState(false); + + const parsedDate = field?.value ? parseYYYYMM(field.value) : new Date(); + + const handleSelect = ( + date: Date | undefined, + shouldClosePopover?: boolean + ) => { + if (!readOnly && date) { + field.onChange(formatDateToYYYYMM(date)); + if (shouldClosePopover) setOpen(false); + } + }; + + return ( + <Popover open={open && !readOnly} onOpenChange={readOnly ? undefined : setOpen}> + <PopoverTrigger asChild> + <Button + variant="outline" + color="white" + disabled={readOnly} + className={cn( + selectClass ?? "w-[240px] justify-between pl-3 text-left font-normal", + !field?.value && "text-muted-foreground", + readOnly && "opacity-60 cursor-not-allowed" + )} + > + {parsedDate + ?format(parsedDate, "yyyy년 M월", { locale: ko }) + : "월을 선택하세요"} + <CalendarIcon className="ml-2 h-4 w-4" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-auto p-0" align="start"> + <MonthPicker + currentMonth={parsedDate} + onMonthChange={handleSelect} + startYear={startYear} + yearReadOnly={yearReadOnly} + /> + </PopoverContent> + </Popover> + ); +}; diff --git a/packages/common/business_components/src/ui/date_month_picker/mothPicker.tsx b/packages/common/business_components/src/ui/date_month_picker/mothPicker.tsx new file mode 100644 index 000000000..78c9cb90b --- /dev/null +++ b/packages/common/business_components/src/ui/date_month_picker/mothPicker.tsx @@ -0,0 +1,133 @@ +import { cn } from "../../lib/utils"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components/ui"; +import { + eachMonthOfInterval, + endOfYear, + format, + isEqual, + isFuture, + parse, + startOfMonth, + startOfToday, +} from "date-fns"; +import { ko } from "date-fns/locale"; + +function getStartOfCurrentMonth() { + return startOfMonth(startOfToday()); +} + +export type MonthPickerProps = { + currentMonth: Date; + onMonthChange: ( + newMonth: Date | undefined, + shouldClosePopover?: boolean + ) => void; + startYear?: number; + yearReadOnly?: boolean; +}; + +export function MonthPicker({ currentMonth, onMonthChange, startYear, yearReadOnly }: MonthPickerProps) { + + const today = startOfToday(); +const currentYear = today.getFullYear(); + const yearFromProp = format(currentMonth, "yyyy"); +const selectedYear = parseInt(yearFromProp); + + // 현재 월의 시작 날짜 + const fullYearStart = parse(yearFromProp, "yyyy", new Date()); + + const yearEnd = + selectedYear === currentYear ? today : endOfYear(fullYearStart); + + + + + // startYear가 없으면 기존과 동일하게 (현재년도 - 20년부터 현재년도까지) + // startYear가 있으면 해당 년도부터 현재년도까지, 내림차순으로 정렬 + const yearRange = startYear + ? Array.from({ length: currentYear - startYear + 1 }, (_, i) => currentYear - i) + : Array.from({ length: 21 }, (_, i) => currentYear - 20 + i); + + const months = eachMonthOfInterval({ start: fullYearStart, end: yearEnd }); + + const getMonthButtonClass = (month: Date) => + cn( + "inline-flex h-9 w-16 items-center justify-center rounded-md p-0 text-sm font-normal ring-offset-white transition-colors hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 aria-selected:opacity-100 dark:ring-offset-slate-950 dark:hover:bg-slate-800 dark:hover:text-slate-50 dark:focus-visible:ring-slate-800", + isEqual(month, currentMonth) && + "bg-slate-900 text-slate-50 hover:bg-slate-900 hover:text-slate-50 focus:bg-slate-900 focus:text-slate-50 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50 dark:hover:text-slate-900 dark:focus:bg-slate-50 dark:focus:text-slate-900", + // !isEqual(month, currentMonth) && + // isEqual(month, getStartOfCurrentMonth()) && + // "bg-slate-100 text-slate-900 dark:bg-slate-800 dark:text-slate-50" + ); + + return ( + <div className="p-3"> + <div className="flex flex-col space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0"> + <div className="space-y-4"> + {/* 년도 셀렉트 */} + <div className="flex justify-center pt-1"> + <Select + value={yearFromProp} + disabled={yearReadOnly} + onValueChange={(selectedYearStr) => { + const selectedYearNum = parseInt(selectedYearStr); + const newDate = new Date( + selectedYearNum, + currentMonth.getMonth(), + 1 + ); + onMonthChange(newDate, false); + }} + > + <SelectTrigger className="w-[140px] h-9 text-base"> + <SelectValue placeholder="년도" /> + </SelectTrigger> + <SelectContent> + {yearRange.map((year) => ( + <SelectItem key={year} value={String(year)}> + {year}년 + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + {/* 월 그리드 */} + <div + className="grid w-full grid-cols-3 gap-2" + role="grid" + aria-labelledby="month-picker" + > + {months.map((month) => ( + <div + key={month.toString()} + className="relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-slate-100 first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md dark:[&:has([aria-selected])]:bg-slate-800" + role="presentation" + > + <button + name="month" + className={getMonthButtonClass(month)} + disabled={isFuture(month)} + role="gridcell" + tabIndex={-1} + type="button" + onClick={() => onMonthChange(month, true)} + > + <time dateTime={format(month, "yyyy-MM-dd")}> + {format(month, 'MMM', { locale: ko })} + </time> + </button> + </div> + ))} + </div> + </div> + </div> + </div> + ); +} diff --git a/packages/common/business_components/src/ui/date_picker/index.tsx b/packages/common/business_components/src/ui/date_picker/index.tsx index 9df32b3b5..34760c245 100644 --- a/packages/common/business_components/src/ui/date_picker/index.tsx +++ b/packages/common/business_components/src/ui/date_picker/index.tsx @@ -6,62 +6,116 @@ import { format } from "date-fns"; import { CalendarIcon } from "lucide-react"; import type { DayPickerSingleProps } from "react-day-picker"; -import { Button, Calendar, Popover, PopoverContent, PopoverTrigger } from "@common/components/ui"; +import { + Button, + Calendar, + Popover, + PopoverContent, + PopoverTrigger, +} from "@common/components/ui"; import { cn } from "../../lib/utils"; import { FormControl } from "../form"; import { ko } from "date-fns/locale"; import { useState } from "react"; -type DatePickerProps = DayPickerSingleProps & { field: { value: Date; onChange: (date: Date | undefined) => void }, selectClass: string | undefined, readOnly?: boolean }; +type DatePickerProps = DayPickerSingleProps & { + field: { value: Date; onChange: (date: Date | undefined) => void }; + selectClass: string | undefined; + readOnly?: boolean; +}; +export function DatePicker({ + field, + selectClass, + readOnly = false, + ...props +}: DatePickerProps) { + const [open, setOpen] = useState(false); -export function DatePicker({ field, selectClass, readOnly = false, ...props }: DatePickerProps) { - const [open, setOpen] = useState(false); + // const handleSelect = (date: Date | undefined) => { + // console.log("날짜", date) + // if (!readOnly) { + // // readOnly가 아닐 때만 변경 허용 + // field.onChange(date); // React Hook Form 업데이트 + // setOpen(false); + // } + // }; - const handleSelect = (date: Date | undefined) => { - if (!readOnly) { // readOnly가 아닐 때만 변경 허용 - field.onChange(date); // React Hook Form 업데이트 - setOpen(false); - } - }; + //저장시 9시간 전으로 돌아가는 문제 + const handleSelect = (date: Date | undefined) => { + if (!readOnly) { + if (date) { + const utcMidnight = new Date( + //utc 삽입 + Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) + ); + field.onChange(utcMidnight); + } else { + field.onChange(undefined); + } + setOpen(false); + } + }; - return ( - <Popover open={open && !readOnly} onOpenChange={readOnly ? undefined : setOpen}> - <PopoverTrigger asChild> - <FormControl> - <Button - color="white" - variant={"outline"} - disabled={readOnly} // readOnly일 때 버튼 비활성화 - className={!selectClass ? cn( - "w-[240px] pl-3 text-left font-normal", - !field?.value && "text-muted-foreground", - readOnly && "opacity-60 cursor-not-allowed" // readOnly 스타일 추가 - ) : cn(selectClass, readOnly && "opacity-60 cursor-not-allowed")} - > - {field?.value ? ( - format( - field.value instanceof Date && !isNaN(field.value.getTime()) - ? field.value - : new Date() + return ( + <Popover + open={open && !readOnly} + onOpenChange={readOnly ? undefined : setOpen} + > + <PopoverTrigger asChild> + <FormControl> + <Button + color="white" + variant={"outline"} + className={ + !selectClass + ? cn( + "w-[240px] pl-3 text-left font-normal", + !field?.value && "text-muted-foreground", - , "PPP", { locale: ko }) - ) : ( - <span>Pick a date</span> - )} - <CalendarIcon className="h-4" /> - </Button> - </FormControl> - </PopoverTrigger> - <PopoverContent className="w-auto p-0" align="start"> - <Calendar - mode="single" - selected={field?.value} - onSelect={handleSelect} - initialFocus - locale={ko} - /> - </PopoverContent> - </Popover> - ); + //감리 지적사항으로 삭제 예정 리드온리는 리드온리 로만 보일수 있도록 수정 + // disabled={readOnly} // readOnly일 때 버튼 비활성화 + //readOnly && "opacity-60 cursor-not-allowed" 감리 지적사항으로 + ) + : cn( + selectClass, + // , readOnly && "opacity-60 cursor-not-allowed" + ) + } + > + {field?.value ? ( + format( + field.value instanceof Date && !isNaN(field.value.getTime()) + ? field.value + : new Date(), + + "PPP", + { locale: ko }, + ) + ) : ( + <span>날짜를 선택해주세요 </span> + )} + <CalendarIcon className="h-4" /> + </Button> + </FormControl> + </PopoverTrigger> + <PopoverContent className="w-auto p-0" align="start"> + <Calendar + className="w-full" + + //-- 데이트 피커가 드롭박스 형태로 년월을 선택할수 있도록 하는 Props 로 활성화 되면 드롭박스로 선택 + + // captionLayout="dropdown-buttons" + // fromYear={2000} + // toYear={new Date().getFullYear()} + showOutsideDays={false} // 금월 까지만 표출되도록 + mode="single" + selected={field?.value} + onSelect={handleSelect} + disabled={{ after: new Date() }} // 금일까지만 표시됨 (금일 이후는 안보임) 20250721 캘린더가 변경됨!! + locale={ko} + /> + </PopoverContent> + </Popover> + ); } diff --git a/packages/common/business_components/src/ui/date_picker_range/index.tsx b/packages/common/business_components/src/ui/date_picker_range/index.tsx new file mode 100644 index 000000000..7cf19d819 --- /dev/null +++ b/packages/common/business_components/src/ui/date_picker_range/index.tsx @@ -0,0 +1,49 @@ +import "../date_picker/date-picker.css"; + +import { format } from "date-fns"; +import { CalendarIcon } from "lucide-react"; +import type { DayPickerSingleProps } from "react-day-picker"; + +import { Button, Calendar, Popover, PopoverContent, PopoverTrigger } from "@common/components/ui"; +import { cn } from "../../lib/utils"; + +type DatePickerRangeProps = { + placeholder?: string; + disabled?: boolean | ((date: Date) => boolean); + onSelect: (date: Date | undefined) => void; +} & DayPickerSingleProps; + +export function DatePickerRange({ + className, + placeholder = "날짜 선택", + disabled, + mode, + selected, + onSelect, + defaultMonth, +}: DatePickerRangeProps) { + return ( + <Popover> + <PopoverTrigger asChild> + <Button + variant="outline" + className={cn("date-picker-trigger", className)} + disabled={typeof disabled === "boolean" ? disabled : false} + > + <CalendarIcon className="!size-[1.6rem]" /> + {selected ? ( + format(selected, "yyyy-MM-dd") + ) : ( + <span>{placeholder}</span> + )} + </Button> + </PopoverTrigger> + <PopoverContent className="w-auto p-0"> + <Calendar initialFocus classNames={{ cell: "rounded-md" }} mode={mode} onSelect={onSelect} + disabled={typeof disabled === "function" ? disabled : undefined} + defaultMonth={defaultMonth} + /> + </PopoverContent> + </Popover> + ); +} diff --git a/packages/common/business_components/src/ui/dialog_ui/index.tsx b/packages/common/business_components/src/ui/dialog_ui/index.tsx index b9e341279..df7fffd3a 100644 --- a/packages/common/business_components/src/ui/dialog_ui/index.tsx +++ b/packages/common/business_components/src/ui/dialog_ui/index.tsx @@ -1,6 +1,7 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from "@common/components"; +import { cn } from "../../lib/utils"; -type ModalType = { +type ModalType = { bodyClass: string; body: React.ReactNode; open?: boolean; @@ -9,38 +10,45 @@ type ModalType = { noFooterYn?: 'N' | 'Y'; headerTitle?: string; footerContent?: React.ReactNode; + useDirectRender?: boolean; } -export function UiModal({ - bodyClass, - body, - open, +export function UiModal({ + bodyClass, + body, + open, onOpenChange, noHeaderYn = 'N', noFooterYn = 'N', headerTitle, - footerContent + footerContent, + useDirectRender = false }: ModalType) { - + return ( <> - <Dialog open={open} onOpenChange={onOpenChange}> - <DialogContent className={bodyClass}> - {noHeaderYn === 'N' && ( - <DialogHeader> - <DialogDescription> - {headerTitle} - </DialogDescription> - </DialogHeader> - )} - {(open && body)} - {noFooterYn === 'N' && ( - <DialogFooter className="sm:justify-start"> - {footerContent} - </DialogFooter> - )} - </DialogContent> - </Dialog> + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className={bodyClass}> + {noHeaderYn === 'N' && ( + <DialogHeader> + <DialogDescription> + {headerTitle} + </DialogDescription> + </DialogHeader> + )} + {(open && body) ? + useDirectRender + ? body + : <div>{body}</div> + : <></> + } + {noFooterYn === 'N' && ( + <DialogFooter className="sm:justify-start"> + {footerContent} + </DialogFooter> + )} + </DialogContent> + </Dialog> </> ); } diff --git a/packages/common/business_components/src/ui/index.ts b/packages/common/business_components/src/ui/index.ts index 19d966a53..eab3a7eab 100644 --- a/packages/common/business_components/src/ui/index.ts +++ b/packages/common/business_components/src/ui/index.ts @@ -20,4 +20,6 @@ export * from "./table"; export * from "./table_div"; export * from "./title"; export * from "./sdmInsertForm"; -export * from "./popover_ui" \ No newline at end of file +export * from "./popover_ui"; +export * from "./date_month_picker"; +export * from "./date_picker_range" \ No newline at end of file diff --git a/packages/common/business_components/src/ui/loadingBar/index.tsx b/packages/common/business_components/src/ui/loadingBar/index.tsx new file mode 100644 index 000000000..7c6df0114 --- /dev/null +++ b/packages/common/business_components/src/ui/loadingBar/index.tsx @@ -0,0 +1,26 @@ +import { LoadingWithAnimation } from "@common/components/ui/loading"; +import React from "react"; + +interface LoadingOverlayProps { + isLoading: boolean; + customZindex?: string; +} + +const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ + isLoading, + customZindex, +}) => { + if (!isLoading) return null; + + return ( + <div + className={`fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 ${customZindex || "z-50"}`} + > + <div className="relative h-16 w-16"> + <LoadingWithAnimation color="primary" /> + </div> + </div> + ); +}; + +export default LoadingOverlay; diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index f45fe4e12..3ea1a59de 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -7,90 +7,228 @@ import * as olProj from 'ol/proj'; import { register } from 'ol/proj/proj4'; import proj4 from 'proj4'; import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; -import GisLonLat from './GisLonLat'; import GisMapToggle from './GisMapToggle'; import GisSelect from './GisSelect'; import GisToolBar from './GisToolBar'; import GisTree from './GisTree'; import './lib/css/gis.common.css'; import GisApp from './lib/gis.core'; +// import FacilityInfoPopup from './FacilityInfoPopup'; export const GisContext = createContext({ gisCore: null, gisMap: null, treeData: null, checkedLayerIds: null }); export const useGisContext = () => useContext(GisContext); +export const searchUpperPpls = async (fcltMngNo) => { + const BASE_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ''; -function Gis({ isOpen , gisMaps, isSearchModalOpen, treeData = [], isSearch = false, isMiniMap = false, setIsOpen, setFeatureData, featureData, searchView : SearchView, detailView: DetailView, onFeatureSelect, setIsSearchModalOpen }) { + const res = await fetch(`${BASE_URL}/v1/fcltmhl/upperPpls/${fcltMngNo}`, { + method: "GET", + }); + + let data = null; + if (res.ok) { + try { + // 응답이 비어있는지 확인 + const contentLength = res.headers.get('content-length'); + if (contentLength === '0') alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); + + // 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인 + const text = await res.text(); + if (!text || text.trim() === '') alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); + + // JSON 파싱 시도 + data = JSON.parse(text); + } catch (error) { + console.error('JSON parsing error:', error); + alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); + } + } + + const select = GisApp.Module.select; + + const gisMap = GisApp.Module.core?.map; + + let foundFeature = null; + + data.forEach(item => { + const pplMngNo = item.ppl_mng_no.trim(); + + gisMap.getLayers().getArray().filter(e => e.get("lyrTyNm") == 'tb_ms_swage_ppl').forEach(layer => { + const source = layer.getSource?.(); + if (source?.getFeatures) { + const features = source.getFeatures(); + for (const f of features) { + if (f.values_.fclt_mng_no.trim() === pplMngNo) { + foundFeature = f; + break; + } + } + } + }); + + if (foundFeature) select.selectMultiFeature(foundFeature, true); + }); + + if (!foundFeature) alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); + + let geometry = foundFeature?.getGeometry(); + if (geometry) { + let extent = geometry.getExtent(); + + gisMap.getView().fit(extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 14, + constrainResolution: true, + nearest: true, + }); + } +}; + + +function Gis({ + isOpen, //우측 상세 레이어 팝업 + gisMaps, + isSearchModalOpen, + treeData = [], + isSearch = false, + isMiniMap = false, + setIsOpen, + setFeatureData, + featureData, + imgVisble = false, + searchView: SearchView, + detailView: DetailView, + onFeatureSelect, + setIsSearchModalOpen, + onCheckedLayersChange, + setData = () => {}, +}) { const mapRef = useRef(); const [mapObject, setMapObject] = useState({}); const [tabVisible, setTabVisible] = useState(true); + const [layerNm, setLayerNm] = useState('') // 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); const checkedLayerIds = useRef([]); + const layerNmRef = useRef('') + + // 팝업 데이터를 저장할 상태와 오버레이 참조를 저장할 ref 추가 + // const [facilityInfoPopupData, setFacilityInfoPopupData] = useState([]); + // const facilityOverlaysRef = useRef([]); + // 체크된 레이어 상태 관리 + const [checkedLayers, setCheckedLayers] = useState([]); + const [checkedLayersData, setCheckedLayersData] = useState([]); + let gisMap = null; const closePopup = () => { setIsOpen(false); // 팝업 닫기 setFeatureData(null) }; - // SearchModal, GisContent 등에서 호출 + // Feature Click Event const handleFeatureSelect = useCallback((data) => { - gisMap = mapInstanceRef.current; + // gisMap = mapInstanceRef.current; + // gisMap = mapInstanceRef.current; - if (!gisMap) { - console.warn('지도가 초기화되지 않았습니다.'); - return; - } + // if (!gisMap) { + // console.warn('지도가 초기화되지 않았습니다.'); + // return; + // } + // if (!gisMap) { + // console.warn('지도가 초기화되지 않았습니다.'); + // return; + // } - // 항상 같은 select 객체 사용 - const select = GisApp.Module.select; - if (!select) return; + // // 항상 같은 select 객체 사용 + // const select = GisApp.Module.select; + // if (!select) return; + // // 항상 같은 select 객체 사용 + // const select = GisApp.Module.select; + // if (!select) return; - // 1. 기존 선택 초기화 - select.resetSelectedFeature(); + // // 1. 기존 선택 초기화 + // select.resetSelectedFeature(); - // 2. data가 OpenLayers Feature 인스턴스인지 확인 - let foundFeature = null; - if (data && typeof data.getGeometry === 'function' && typeof data.setStyle === 'function') { - // 이미 Feature 인스턴스라면 그대로 사용 - foundFeature = data; - } else { - // 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용 - const featureId = data.fid || data.ftrIdn || data.id; - gisMap.getLayers().getArray().forEach(layer => { - const source = layer.getSource?.(); - if (source?.getFeatureById) { - const feature = source.getFeatureById(String(featureId).trim()); - if (feature) foundFeature = feature; - } - }); - } + // // 2. data가 OpenLayers Feature 인스턴스인지 확인 + // let foundFeature = null; + // if (data && typeof data.getGeometry === 'function' && typeof data.setStyle === 'function') { + // // 이미 Feature 인스턴스라면 그대로 사용 + // foundFeature = data; + // } else { + // // 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용 + // let featureId = data.fid; + // const layerName = layerNmRef.current + // if (layerName === '하수재이용시설') { + // featureId = data.searchPk; + // } else if (layerName === '빗물이용시설' || layerName == '중수도시설') { + // featureId = data.bdstInfoSn; + // } else if (layerName === '하수처리시설') { + // featureId = data.bscFctCd; + // } else if (layerName === '온배수시설') { + // featureId = data.bdstRuseFcltSn; + // } else if(layerName == '수요처'){ + // featureId = data.dmdInfoId.replaceAll('-','') + // } + // else { + // console.warn('해당 레이어에 대한 featureId 매핑 없음', layerNm); + // featureId = data.fid || data.id || data.ftrIdn; + // } + // // gisMap.getLayers().getArray().forEach(layer => { + // // const source = layer.getSource?.(); + // // if (source?.getFeatureById) { + // // const feature = source.getFeatureById(String(featureId).trim()); + // // if (feature) foundFeature = feature; + // // } + // // }); + // // const [layerType, id] = String(featureId).split('.'); + // gisMap.getLayers().getArray().forEach(layer => { + // const source = layer.getSource?.(); + // if (source?.getFeatures) { + // const allFeatures = source.getFeatures(); + // for (const f of allFeatures) { + // const id = String(f.getId()); + // // console.log('id', id, featureId) + // if (id && id.split('.')[1] === String(featureId)) { + // foundFeature = f; + // break; + // } + // } + // } + // }); + // } - // 3. 피처 선택 - if (foundFeature) { - let geometry = foundFeature.getGeometry(); - if (geometry) { - let extent = geometry.getExtent(); + // // 3. 피처 선택 + // if (foundFeature) { + // let geometry = foundFeature.getGeometry(); + // if (geometry) { + // let extent = geometry.getExtent(); - gisMap.getView().fit(extent, { - duration: 300, - padding: [50, 50, 50, 50], - maxZoom: 16, - constrainResolution: true, - nearest: true, - }); - } - select.selectFeature(foundFeature, true); - } else { - console.warn('OpenLayers Feature를 찾을 수 없습니다:', data); - } + // gisMap.getView().fit(extent, { + // duration: 300, + // padding: [50, 50, 50, 50], + // maxZoom: 16, + // constrainResolution: true, + // nearest: true, + // }); + // } + // select.selectFeature(foundFeature, true); + // } else { + // console.warn('OpenLayers Feature를 찾을 수 없습니다:', data); + // } - setFeatureData(data); - setIsOpen(true); - setIsSearchModalOpen(false); + // setFeatureData(data); + // setIsOpen(true); + // setIsSearchModalOpen(false); + + const layerName = layerNmRef.current + data.layerNm = layerName; + onFeatureSelect?.(data); // 선택된 피쳐 데이터 콜백 호출 }, [mapInstanceRef]); + // 지도 생성 및 초기화 useEffect(() => { if (!mapRef.current) return; if (mapInstanceRef.current) return; // 이미 생성된 맵이 있다면 중단 @@ -104,7 +242,7 @@ function Gis({ isOpen , gisMaps, isSearchModalOpen, treeData = [], isSearch = fa ]); register(proj4); - //맵 생성 + // OpenLayers 지도 객체 생성 const map = new OlMap({ layers: [gisCore.baseLayers['base'], gisCore.baseLayers['satellite']], renderer: 'webgl', @@ -121,7 +259,7 @@ function Gis({ isOpen , gisMaps, isSearchModalOpen, treeData = [], isSearch = fa projection: GisApp.TO_PROJ, zoom: gisCore.defaults.zoom, center: olProj.transform( - [126.6757, 37.5585], + [127.4875, 37.4916], GisApp.FROM_PROJ, GisApp.TO_PROJ ), @@ -132,8 +270,8 @@ function Gis({ isOpen , gisMaps, isSearchModalOpen, treeData = [], isSearch = fa gisMaps.current = map - //미니맵 - if(isMiniMap){ + // 미니맵 추가 + if (isMiniMap) { const overviewMap = new OverviewMap({ layers: [GisApp.Module.core.baseMiniMapLayers['base']], view: new View({ @@ -147,6 +285,7 @@ function Gis({ isOpen , gisMaps, isSearchModalOpen, treeData = [], isSearch = fa map.addControl(overviewMap); } + // 스케일 컨트롤 및 베이스 레이어 설정 gisCore.__proto__.map = map; gisCore.scaleControl(); gisCore.changeBaseLayer('base'); @@ -154,17 +293,21 @@ function Gis({ isOpen , gisMaps, isSearchModalOpen, treeData = [], isSearch = fa mapInstanceRef.current = map; // 생성 완료 플래그 설정 }, []); + // treeData 변경 시 레이어 코드 생성 useEffect(() => { if (!treeData || treeData.length === 0) return; + // 기존 오버레이 및 레이어 모두 삭제 gisCore.deleteAllOverlay(); gisCore.deleteAllLayer(); + // 트리 데이터를 레이어 코드 객체로 변환 const layerCode = gisCore.convertLayerInfosToLayerCode(treeData, 'lyrSn'); - GisApp.layerCode = layerCode; + GisApp.layerCode = layerCode; // 전역 레이어 코드 설정 //setTreeData(data); - const layerItems = getLayerItems(layerCode); + // 편집 가능한 레이어 목록 생성 + //const layerItems = getLayerItems(layerCode); //setEditLayerItems(prev => [...layerItems]) }, [treeData]); @@ -193,31 +336,171 @@ function Gis({ isOpen , gisMaps, isSearchModalOpen, treeData = [], isSearch = fa } } + // // 데이터 로드 및 팝업 생성 로직 + // useEffect(() => { + // if (!mapInstanceRef.current) return; // 맵이 생성되지 않았으면 실행 안 함 + + // // 1. 팝업 데이터 가져오기 (feature.js의 getData와 유사) + // const fetchPopupData = async () => { + // // 실제 API 호출 로직으로 대체해야 합니다. + // // 예시: const response = await yourApiService.getFcltyWtCnt(); + // // setFacilityInfoPopupData(response.data); + + // // 임시 목업 데이터 + // const mockData = [ + // { ftrIdn: 1, ftrCde: 'SA206', bplcNm: '처리장A', fcltyUseYn: 'Y', x: 127.005, y: 37.282, aCnt: 10, cCnt: 5, eCnt: 3, mCnt: 8, iCnt: 2 }, + // { ftrIdn: 2, ftrCde: 'SA112', bplcNm: '처리장B', fcltyUseYn: 'N', x: 127.010, y: 37.285, aCnt: 8, cCnt: 4, eCnt: 2, mCnt: 6, iCnt: 1 } + // ]; + // setFacilityInfoPopupData(mockData); + // }; + + // fetchPopupData(); + + // }, [mapInstanceRef.current]); // 맵 인스턴스가 생성된 후 한 번만 실행 + + // // 데이터가 변경되면 오버레이를 생성/업데이트하는 로직 + // useEffect(() => { + // const map = mapInstanceRef.current; + // if (!map || facilityInfoPopupData.length === 0) return; + + // // 기존 오버레이 정리 + // facilityOverlaysRef.current.forEach(overlay => map.removeOverlay(overlay)); + // facilityOverlaysRef.current = []; + + // facilityInfoPopupData.forEach(data => { + // const popupId = `${data.ftrIdn}_${data.ftrCde}`; + + // // 팝업을 담을 div 엘리먼트 생성 + // const popupElement = document.createElement('div'); + // popupElement.id = popupId; + + // // OpenLayers Overlay 생성 + // const position = olProj.transform([Number(data.x), Number(data.y)], GisApp.FROM_PROJ, GisApp.TO_PROJ); + // const overlay = new Overlay({ + // element: popupElement, + // position: position, + // offset: [-75, -210], // 팝업 너비의 절반, 높이만큼 오프셋 조정 + // positioning: 'bottom-center', + // className: 'facility-popup-overlay' // 필요시 공통 클래스 추가 + // }); + + // map.addOverlay(overlay); + // facilityOverlaysRef.current.push(overlay); + + // // React 컴포넌트를 popupElement에 렌더링 + // const root = createRoot(popupElement); + // root.render(<FacilityInfoPopup data={data} />); + // }); + + // // 초기 가시성 업데이트 + // updatePopupVisibility(); + + // }, [facilityInfoPopupData]); // 팝업 데이터가 변경될 때마다 실행 + + // // 레이어 가시성에 따라 팝업 가시성을 제어하는 함수 + // const updatePopupVisibility = useCallback(() => { + // const map = mapInstanceRef.current; + // if (!map) return; + + // const visibleLayers = new Set(); + // map.getLayers().forEach(layer => { + // if (layer.getVisible()) { + // // feature.js의 로직처럼 typeName이나 ftrCde를 사용해 필터링 + // const typeName = layer.get('typeName'); + // if (typeName === 'VW_WTL_FCLTY_AS') { // 예시 레이어 이름 + // visibleLayers.add(layer.get('ftrCde')); + // } + // } + // }); + + // facilityOverlaysRef.current.forEach(overlay => { + // const element = overlay.getElement(); + // const ftrCde = element.querySelector('table')?.classList[0]; // FacilityPopup의 클래스에서 ftrCde 가져오기 + + // if (visibleLayers.has(ftrCde)) { + // element.style.display = ''; + // } else { + // element.style.display = 'none'; + // } + // }); + // }, []); + + // GisTree에서 체크 상태가 변경될 때 이 함수를 호출하도록 연결해야 합니다. + // 예시: GisTree 컴포넌트에 onCheck 콜백을 추가하고, 그 콜백에서 이 함수를 호출 + // useEffect(() => { + // // 레이어 가시성이 변경될 때마다 팝업 가시성 업데이트 + // const map = mapInstanceRef.current; + // if (!map) return; + + // const handleLayerVisibilityChange = () => { + // updatePopupVisibility(); + // }; + + // map.getLayers().on('change:length', handleLayerVisibilityChange); + // map.getLayers().forEach(layer => { + // layer.on('change:visible', handleLayerVisibilityChange); + // }); + + // return () => { + // // 클린업 + // map.getLayers().un('change:length', handleLayerVisibilityChange); + // map.getLayers().forEach(layer => { + // layer.un('change:visible', handleLayerVisibilityChange); + // }); + // }; + // }, [updatePopupVisibility]); + + // 체크된 레이어 정보를 처리하는 함수 + const handleCheckedLayersChange = useCallback((checkedIds, checkedNodesData) => { + setTimeout(() => { + setCheckedLayers(checkedIds); + setCheckedLayersData(checkedNodesData); + onCheckedLayersChange?.(checkedIds, checkedNodesData); + }, 0); + }, [onCheckedLayersChange]); + return ( <> <GisContext.Provider value={{ gisCore, gisMap: mapInstanceRef.current, treeData, checkedLayerIds }}> <div className="h-[calc(100vh-10rem)] flex relative"> <div ref={mapRef} className="gis-map flex-1 relative" style={{ height: '100%', padding: 0 }} /> - <GisTree visible={tabVisible} setVisible={setTabVisible} /> - + <GisTree + visible={tabVisible} + setVisible={setTabVisible} + onCheckedChange={handleCheckedLayersChange} + onLayerIconClick={(layerNm) => { + layerNmRef.current = layerNm + setLayerNm(layerNm); setIsOpen(true); const dummyFeature = { + getProperties: () => ({}), // 최소 구조 + }; + onFeatureSelect(dummyFeature); + }} + imageVisible={imgVisble} + /> + {/* <GisContent onFeatureSelect={handleFeatureSelect} isSearch={isSearch} isSearchModalOpen={isSearchModalOpen} setIsSearchModalOpen={setIsSearchModalOpen} /> */} - - {/* isSearchModalOpen이 true면 visible=false로 팝업 숨김 */} - {isOpen && featureData && !isSearchModalOpen && ( + + {/* DetailView가 존재할 때만 렌더링 isSearchModalOpen이 true면 visible=false로 팝업 숨김 */} + {isOpen && !isSearchModalOpen && ( <DetailView - featureData={featureData} + featureData={featureData || { getProperties: () => ({}) }} + seletectedLayerNm={layerNm} onClose={closePopup} // 닫기 콜백 전달 + onFeatureSelect={handleFeatureSelect} /> )} - <SearchView - open={isSearchModalOpen} - onOpenChange={setIsSearchModalOpen} - onFeatureSelect={onFeatureSelect} - /> - <GisLonLat/> - <GisToolBar/> - <GisMapToggle/> - <GisSelect onFeatureSelect={onFeatureSelect} /> + {/* SearchView가 존재할 때만 렌더링 */} + {SearchView && ( + <SearchView + open={isSearchModalOpen} + onOpenChange={setIsSearchModalOpen} + onFeatureSelect={handleFeatureSelect} + /> + )} + {/* <GisLonLat /> */} + <GisToolBar /> + <GisMapToggle /> + <GisSelect onFeatureSelect={handleFeatureSelect} /> </div> {isSearch && ( diff --git a/packages/common/business_components/src/ui/map/GisContent.js b/packages/common/business_components/src/ui/map/GisContent.js index a8595efb0..d2594ba95 100644 --- a/packages/common/business_components/src/ui/map/GisContent.js +++ b/packages/common/business_components/src/ui/map/GisContent.js @@ -1,12 +1,8 @@ -import React, { useCallback, 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 GisMapToggle from './GisMapToggle'; import { Search } from 'lucide-react'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useGisContext } from './Gis'; +import './lib/css/gis.edit.css'; +import GisApp from './lib/gis.core'; export default function GisContent({ edit = false, onFeatureSelect, isSearch, setIsSearchModalOpen }) { const { gisMap, gisCore } = useGisContext(); diff --git a/packages/common/business_components/src/ui/map/GisMapToggle.js b/packages/common/business_components/src/ui/map/GisMapToggle.js index b157dc141..c890f75f8 100644 --- a/packages/common/business_components/src/ui/map/GisMapToggle.js +++ b/packages/common/business_components/src/ui/map/GisMapToggle.js @@ -1,7 +1,5 @@ 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); diff --git a/packages/common/business_components/src/ui/map/GisSelect.js b/packages/common/business_components/src/ui/map/GisSelect.js index a9116e626..e9deb72f7 100644 --- a/packages/common/business_components/src/ui/map/GisSelect.js +++ b/packages/common/business_components/src/ui/map/GisSelect.js @@ -1,7 +1,7 @@ -import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; +import React, { useContext, useEffect } from 'react'; +import { GisContext } from './Gis'; import GisApp from './lib/gis.core'; import GisOlSelect from './lib/gis.select'; -import { GisContext } from './Gis'; export default React.memo(function GisSelect({ onFeatureSelect }) { diff --git a/packages/common/business_components/src/ui/map/GisTree.js b/packages/common/business_components/src/ui/map/GisTree.js index 6a2e8face..6fe396112 100644 --- a/packages/common/business_components/src/ui/map/GisTree.js +++ b/packages/common/business_components/src/ui/map/GisTree.js @@ -1,15 +1,16 @@ -import GisApp from './lib/gis.core'; -import React, { useEffect, useState } from 'react'; -import { useGisContext } from './Gis'; -import { DndProvider } from "react-dnd"; import { - Tree, MultiBackend, - getBackendOptions + Tree, + getBackendOptions, + DndProvider } from "@minoru/react-dnd-treeview"; import { ChevronLeft, ChevronRight } from 'lucide-react'; +import React, { useEffect, useState } from 'react'; -export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes = () => { }, visible, setVisible }) { +import { useGisContext } from './Gis'; +import GisApp from './lib/gis.core'; + +export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes = () => { }, visible, setVisible, onCheckedChange, onLayerIconClick,imageVisible=false }) { const { gisCore, treeData } = useGisContext(); const [checked, setChecked] = useState([]); const [expanded, setExpanded] = useState([]); @@ -24,19 +25,31 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes } }, [treeData]); + // 트리 노드 체크 시 레이어 표시/숨김 useEffect(() => { if (nodes.length > 0) { + // 기본으로 체크된 노드들 찾기 const defaultChecked = getDefaultCheckedNodes(nodes); setChecked(defaultChecked); + console.log('defaultChecked : ', defaultChecked); + + // 기본으로 확장된 노드들 설정 const defaultExpanded = Array.from(new Set([ ...defaultChecked, ...defaultChecked.flatMap((id) => getAncestors(id, nodes)) ])); setExpanded(defaultExpanded); - gisCore.setLayerOnMap(defaultChecked, true); - } - }, [nodes]); + // 기본 체크된 레이어들을 지도에 표시 + gisCore.setLayerOnMap(defaultChecked, true); + + // 초기 체크된 레이어 정보를 상위로 전달 + const defaultCheckedNodesData = nodes.filter(node => defaultChecked.includes(node.id)); + onCheckedChange?.(defaultChecked, defaultCheckedNodesData); + } + }, [nodes]); // onCheckedChange를 의존성에 추가 + + // 초기 표시 여부가 'Y'인 레이어들 찾기 const getDefaultCheckedNodes = (nodes, values = []) => { for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; @@ -94,6 +107,11 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes const unchecked = allIds.filter((nid) => !newChecked.includes(nid)); gisCore.setLayerOnMap(newChecked, true); gisCore.setLayerOnMap(unchecked, false); + + // 체크된 노드 정보와 함께 노드 데이터를 상위로 전달 + const checkedNodesData = nodes.filter(node => newChecked.includes(node.id)); + onCheckedChange?.(newChecked, checkedNodesData); + return newChecked; }); }; @@ -170,7 +188,10 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes image.src = canvas.toDataURL('image/png'); return image; }; - + const handleLayerImgClick = (nodeNm, nodeId, layerNm) => { + if(!checked.includes(nodeId)) alert('시설을 선택해주세요') + else onLayerIconClick(layerNm) + } if (!treeData || treeData.length === 0) return null; return ( @@ -203,6 +224,7 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes <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> + {imageVisible ? depth > 0 && <span><img className="cursor-pointer w-5 h-5" src='/images/map/layericon/Layer_Select_BTN.png' onClick={()=>{handleLayerImgClick(node.text, node.id, node.data.lyrTyNm)}} alt={node.text}></img></span> : null} </div> )} sort={(a, b) => { 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 index 0a8a045e7..f9b67a07b 100644 --- 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 @@ -179,7 +179,7 @@ padding: 2px; position: absolute; bottom: 10px; - right: 200px; + right: 10px; } /* 로드뷰 관련 */ @@ -978,7 +978,7 @@ table.board-table tbody th td { position: absolute; z-index: 1; right: 0px; - bottom: 0px; + bottom: 30px; } .ol-overviewmap .ol-overviewmap-map { border: 1px solid #7b98bc; 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 index db58c898f..5fcc396f3 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.core.js +++ b/packages/common/business_components/src/ui/map/lib/gis.core.js @@ -31,8 +31,6 @@ const 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 = '/geoserver/hasudo/ows'; GisApp.SN_GEOSERVER_URL = '/sewerage-service/api/v1/map/geometry'; GisApp.GEOSERVER_WMS_URL = '/geoserver/hasudo/wms'; @@ -44,10 +42,15 @@ GisApp.GOOGLE_SATELLITE_URL = `https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z GisApp.GOOGLE_HYBRID_URL = `https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}&key=${GisApp.GOOGLE_API_KEY}`; GisApp.GOOGLE_3D_URL = `https://tile.googleapis.com/v1/3dtiles/root.json?key=${GisApp.GOOGLE_API_KEY}`; -// TODO: NGII 정보 +// 인터넷망: NGII 정보 GisApp.NGII_API_KEY = '04trYP9_xwLAfALjwZ-B8g'; // NGII에서 발급받은 API 키 GisApp.NGII_WMTS_URL = `https://map.ngii.go.kr/openapi/Gettile.do?apikey=${GisApp.NGII_API_KEY}`; +//행정망: NGII 정보 +//GisApp.NGII_API_KEY = 'BDE2BEF39DAFEC293AB6730150F222AC'; // NGII에서 발급받은 API 키 +//GisApp.NGII_WMTS_URL = `http://10.98.25.94:18080/openapi/Gettile.do?apikey=${GisApp.NGII_API_KEY}`; + + GisApp.EMAP_URL = '/emap/'; GisApp.EMAP_SATELLITE_URL = '/emap/satellite/'; GisApp.VWORLD_URL = 'https://xdworld.vworld.kr/2d/Base/service/{z}/{x}/{y}.png'; @@ -56,7 +59,6 @@ GisApp.OPENSTREET_URL = '//tile.openstreetmap.org/{z}/{x}/{y}.png'; GisApp.BASE_MAP_TYPES = []; GisApp.FROM_PROJ = 'EPSG:4326'; -// GisApp.TO_PROJ = 'EPSG:3857'; GisApp.TO_PROJ = 'EPSG:5179'; //ngii 좌표계 GisApp.TO_DATA_PROJ = 'EPSG:4326'; GisApp.CONTEXT_LAYER_PATH = '/images/map/layericon/'; @@ -94,7 +96,7 @@ GisApp.core.prototype = { target: '', centerX: 37.2806094, centerY: 127.0032502, - zoom: 12, + zoom: 15, minZoom: 6, maxZoom: 20, }, @@ -564,15 +566,17 @@ GisApp.core.prototype = { */ 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]; + if(!layerCode.lyrTyNm) continue; const queryFlter = layerCode?.flterSql || '' + ' ' + (addQurryFlter || ''); layerCode['flterSql'] = queryFlter; - //layer = this.addWmsLayer(layerCode); + //layer = this.addWmsLayer(layerCode); layer = this.addWfsLayer(layerCode); } } 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 index d9657c313..b997a7a0a 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.layer.js +++ b/packages/common/business_components/src/ui/map/lib/gis.layer.js @@ -71,17 +71,12 @@ GisOlLayer.prototype = { /** * [사용]WFS 벡터 소스 반환 */ - getVectorSourceGeoserver: function (options) { + getVectorSourceBbox: function (options) { let url = GisApp.GEOSERVER_URL; let toProj = GisApp.TO_PROJ; let lyrTyNm = options.lyrTyNm; let typename = this.storageNm + lyrTyNm; - if(options.lyrTyNm == "om_dt_untrt_swg_rain"){ //중점우수토실실 - url = GisApp.SN_GEOSERVER_URL; - typename = lyrTyNm; - } - const params = {}; params['TYPENAME'] = typename; params['SERVICE'] = 'WFS'; @@ -89,16 +84,23 @@ GisOlLayer.prototype = { 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) { + + const [minX, minY, maxX, maxY] = extent; + const bboxFilter = `BBOX(geom, ${minX}, ${minY}, ${maxX}, ${maxY}, '${toProj}')`; + const filter = options?.flterSql && options.flterSql && options.flterSql.trim() !== '' + ? `(${options.flterSql}) AND ${bboxFilter}` + : bboxFilter; + + params['CQL_FILTER'] = filter; + + const strParams = new URLSearchParams(params).toString(); return url + '?' + strParams; }, - strategy: all, + strategy: bbox, }); return source; }, @@ -108,8 +110,11 @@ GisOlLayer.prototype = { getVectorTileSource: function (options) { let url = GisApp.GEOSERVER_URL; let toProj = GisApp.TO_PROJ; + let lyrTyNm = options.lyrTyNm; + let typename = this.storageNm + lyrTyNm; + let params = {}; - params['TYPENAME'] = this.storageNm + options.typeName; + params['TYPENAME'] = typename; params['SERVICE'] = 'WFS'; params['VERSION'] = '1.1.0'; params['REQUEST'] = 'GetFeature'; @@ -117,19 +122,14 @@ GisOlLayer.prototype = { params['OUTPUTFORMAT'] = 'application/json'; let strParams = new URLSearchParams(params).toString(); - let source = new VectorTile({ + let source = new VectorSource({ 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; + url: function (extent) { + return url + '?' + strParams; }, - tileGrid: createXYZ, + strategy: all, }); + return source; return source; }, @@ -140,8 +140,22 @@ GisOlLayer.prototype = { option = option || {}; //레이어 생성 let returnLayer = this.makeLayer(option); + //레이어 소스 생성 - let source = this.getVectorSourceGeoserver(option); + let source; + + let lyrTyNm = option.lyrTyNm; + if(lyrTyNm == 'tb_ms_flood_dngr_map' + || lyrTyNm == 'tb_ms_sbmrg_dngr_map' + || lyrTyNm == 'tb_ms_swage_mhl' + || lyrTyNm == 'tb_ms_swage_ppl' + || lyrTyNm == 'tb_ms_swage_pmstn' + ) { + source = this.getVectorSourceBbox(option); + }else { + source = this.getVectorTileSource(option); + } + returnLayer.setSource(source); //스타일 추가 returnLayer.setStyle(this.gisStyle.getLayerStyleFunction(returnLayer, option)); @@ -204,7 +218,7 @@ GisOlLayer.prototype = { return null; } let vectorSource = new VectorSource({ - strategy: all, + strategy: bbox, }); if (feature) { if (Object.prototype.toString.call(feature).slice(8, -1).indexOf('Object') > -1) { 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 index 21ab39609..d0209a6ed 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.select.js +++ b/packages/common/business_components/src/ui/map/lib/gis.select.js @@ -429,6 +429,19 @@ GisOlSelect.prototype = { auto: auto, }); }, + /** + * 피처 다중 셀렉트 + */ + selectMultiFeature: function (feature, auto) { + let me = this; + 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 index 713347f54..8cff08dca 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.style.js +++ b/packages/common/business_components/src/ui/map/lib/gis.style.js @@ -87,7 +87,7 @@ GisOlStyle.prototype = { this.setOverlay(layer, feature, overlayHtml); } - let baseResolution = 20; + let baseResolution = 2; let minScale = 0.4; let maxScale = 1.5; let scale = baseResolution / resolution > maxScale ? maxScale : baseResolution / resolution; diff --git a/packages/common/business_components/src/ui/sdmInsertForm/form.tsx b/packages/common/business_components/src/ui/sdmInsertForm/form.tsx index 6ccaac946..09dd10525 100644 --- a/packages/common/business_components/src/ui/sdmInsertForm/form.tsx +++ b/packages/common/business_components/src/ui/sdmInsertForm/form.tsx @@ -1,17 +1,16 @@ -import { FormField } from "../form"; -import { SelectBox } from "../select"; -import { Input } from "@common/components"; -import { FormUi } from "../form_ui"; -import { useCallback, useEffect, useState } from "react"; +import { Input } from "@common/components"; +import { AsYouType } from "libphonenumber-js"; +import React, { useCallback, useEffect, useState } from 'react'; import type { ControllerRenderProps, UseFormReturn } from "react-hook-form"; +import { FormField } from "../form"; +import { FormUi } from "../form_ui"; +import { SelectBox } from "../select"; import { SdmSearchForm } from "./sdmNameBox"; -import { set } from "date-fns"; // 수요처 정보 export type DemandCompType = { index : number | ''; - dmdInfoPk : string | ''; - dmdInfoFk : string | ''; + dmdInfoId : string | ''; bzmnNo : string | ''; dmdCoNm : string | ''; daddr : string | ''; @@ -49,12 +48,13 @@ interface FormSchema { ctpvCd: string; sggCd: string; dmdCoNm: string; - dmdInfoPk: string; + dmdInfoId: string; daddr: string; telno: string; bznmTotNo?: string; - rgtr?: string; + rgtr?: string | ''; dmdAplyPswd?: string; + dmdAplyPswdConfirm?: string; } interface SdmRegFormProps { @@ -79,6 +79,34 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun const [updateCoNm, setUpdateNm] = useState<string >('') + + const formatPhoneNumber = (value:string) => { + const input = value; + const formatter = new AsYouType("KR"); + const formatted = formatter.input(input); + + return formatted + }; + + // 사업자등록번호 포맷팅 함수 (3-2-5 형식) + const formatBusinessNumber = (value: string) => { + // 숫자만 추출 + const digitsOnly = value.replace(/\D/g, ""); + + // 10자리까지만 처리 + const limitedDigits = digitsOnly.slice(0, 10); + + // 3-2-5 형식으로 포맷팅 + if (limitedDigits.length <= 3) { + return limitedDigits; + } else if (limitedDigits.length <= 5) { + return `${limitedDigits.slice(0, 3)}-${limitedDigits.slice(3)}`; + } else { + return `${limitedDigits.slice(0, 3)}-${limitedDigits.slice(3, 5)}-${limitedDigits.slice(5)}`; + } + }; + + const initializeSigunData = useCallback((sidoCd: string) => { if (sidoCd === '0') { setSigunValue('0'); @@ -98,19 +126,19 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun type: string, field: ControllerRenderProps<FormSchema, keyof FormSchema> | undefined ) => { - if(!updateList?.dmdInfoPk) + if(!updateList?.dmdInfoId) switch (type) { case "ctpvCd": initializeSigunData(val); setSidoValue(val); field?.onChange(val); - form.setValue('dmdInfoPk', ''); + form.setValue('dmdInfoId', ''); form.setValue('bznmTotNo', ''); break; case "sggCd": setSigunValue(val); field?.onChange(val); - form.setValue('dmdInfoPk', ''); + form.setValue('dmdInfoId', ''); form.setValue('bznmTotNo', ''); break; } @@ -129,11 +157,11 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun const rtnCompData = item; form.setValue('dmdCoNm', rtnCompData?.dmdCoNm || ''); - form.setValue('dmdInfoPk', rtnCompData?.dmdInfoPk.substring(0,10) || ''); - form.setValue('bznmTotNo', rtnCompData?.dmdInfoPk.substring(10,14)==='0000' ? '' : rtnCompData?.dmdInfoPk.substring(10,14)); + form.setValue('dmdInfoId', rtnCompData?.dmdInfoId.substring(0,10) || ''); + form.setValue('bznmTotNo', rtnCompData?.dmdInfoId.substring(10,14)==='0000' ? '' : rtnCompData?.dmdInfoId.substring(10,14)); // 수요처 불러온 후 사업자번호를 바꿀경우를 대비함 - setOrgBznmNo(rtnCompData?.dmdInfoPk); + setOrgBznmNo(rtnCompData?.dmdInfoId); const sidoCd = rtnCompData?.ctpvCd.trim() || ''; setSidoValue(sidoCd); form.setValue('ctpvCd', sidoCd); @@ -145,7 +173,7 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun form.setValue('sggCd', sigunCd); }, 100); - if(rtnCompData?.dmdInfoPk.substring(10,14) === '0000') { + if(rtnCompData?.dmdInfoId.substring(10,14) === '0000') { handleCheckboxChange(true); } else { handleCheckboxChange(false); @@ -165,13 +193,16 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun const bznNoChange = useCallback((e: React.ChangeEvent<HTMLInputElement>, noType: string, field?: ControllerRenderProps<FormSchema, keyof FormSchema>) => { const value = e.target.value; - if(noType === 'dmdInfoPk') { - if(orgBznmNo.substring(0,10) === value) { + if(noType === 'dmdInfoId') { + // 숫자만 추출하여 저장 (포맷팅 제거) + const digitsOnly = value.replace(/\D/g, ""); + + if(orgBznmNo.substring(0,10) === digitsOnly) { setIsDuplicate(true); } else { setIsDuplicate(false); } - field?.onChange(value); + field?.onChange(digitsOnly); } else { if(orgBznmNo.substring(10,14) === value) { setIsDuplicate(true); @@ -184,15 +215,17 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun useEffect(() => { - if(updateList?.dmdInfoPk){ + form.setValue('rgtr', '2d4b151d-1828-41e0-91d9-4cd28ea1cc7e'); + + if(updateList?.dmdInfoId){ form.reset(updateList) - if(updateList?.dmdInfoPk.slice(10,14) !== '0000') { + if(updateList?.dmdInfoId.slice(10,14) !== '0000') { handleCheckboxChange( false); - form.setValue('bznmTotNo', updateList?.dmdInfoPk.slice(10,14)); + form.setValue('bznmTotNo', updateList?.dmdInfoId.slice(10,14)); } else { handleCheckboxChange( true); } - form.setValue('dmdInfoPk', updateList?.dmdInfoPk.slice(0,10)); + form.setValue('dmdInfoId', updateList?.dmdInfoId.slice(0,10)); setUpdateNm(updateList?.dmdCoNm) // setSigunValue(updateList?.sggCd) }else{ @@ -201,6 +234,7 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun }, [updateList,form]); useEffect(() => { + console.log( 'form.formState.errors ', form.formState.errors) },[form.formState.errors]) const formFields: Array<{ @@ -212,14 +246,14 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun name: "ctpvCd", label: "시도", component: (field) => ( - updateList?.dmdInfoPk ? ( + updateList?.dmdInfoId ? ( <Input placeholder="시도" type="text" className="!text-xl text-gray-600" onChange={(e) =>{field.onChange(e.target.value)} } value={ selectSidoData.find(item => item.val === field.value?.trim() )?.text??''} - readOnly={!!updateList?.dmdInfoPk} + readOnly={!!updateList?.dmdInfoId} /> ) : ( <SelectBox @@ -237,14 +271,14 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun name: "sggCd", label: "시군구", component: (field) => ( - updateList?.dmdInfoPk ? ( + updateList?.dmdInfoId ? ( <Input placeholder="시도" type="text" className="!text-xl text-gray-600" onChange={(e) =>{field.onChange(e.target.value)} } value={selectSigunData.find(item => item.val === field.value?.trim() )?.text??''} - readOnly={!!updateList?.dmdInfoPk} + readOnly={!!updateList?.dmdInfoId} /> ) : ( <SelectBox @@ -275,22 +309,22 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun ) }, { - name: "dmdInfoPk", + name: "dmdInfoId", label: "사업자등록번호", component: (field) => ( <Input placeholder="사업자등록번호를 입력하세요" type="text" className="!text-xl text-gray-600" - onChange={(e) => bznNoChange(e, 'dmdInfoPk', field)} - value={field.value} - readOnly={!!updateList?.dmdInfoPk} + onChange={(e) => bznNoChange(e, 'dmdInfoId', field)} + value={formatBusinessNumber(field.value || '')} + readOnly={!!updateList?.dmdInfoId} /> ) }, { name: "bznmTotNo", - label: "종사업자등록번호", + label: "", component: (field) => ( <FormUi label="종사업자등록번호" @@ -305,7 +339,7 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun onChange={(e) => bznNoChange(e, 'bznmTotNo', field)} // {...field} value={ field.value} - readOnly={!!updateList?.dmdInfoPk} + readOnly={!!updateList?.dmdInfoId} /> </FormUi> ) @@ -325,14 +359,23 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun { name: "telno", label: "전화번호", - component: (field) => ( + component: (field) => + + { + const formattedValue = formatPhoneNumber(field.value??''); // 초기 렌더링 시에도 포맷 적용 + + return ( <Input placeholder="전화번호를 입력하세요" type="text" className="!text-xl text-gray-600" - {...field} + value={formattedValue} + onChange={(e) => { + const value = e.target.value.replace(/\D/g, ''); // 숫자만 저장 + field.onChange(value); + }} /> - ) + )} }, { @@ -346,32 +389,60 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun {...field} /> ) + }, + { + name: "dmdAplyPswdConfirm", + label: "패스워드확인", + component: (field) => ( + <Input + placeholder="패스워드를 입력하세요" + type="password" + className="!text-xl text-gray-600" + {...field} + /> + ) } ] return ( <> - {formFields.filter((item)=> - {if(updateList?.dmdInfoPk){ - return item.name !== 'dmdAplyPswd' - }else{ - return item - } - }) - .map(({ name, label, component }) => ( - <FormField - key={String(name)} - control={form.control} - name={name} - render={({ field }) => ( - name === 'bznmTotNo' ? component(field) : ( - <FormUi label={label}> - {component(field)} - </FormUi> - ) - )} - /> - ))} + {formFields + .filter(({ name }) => !(updateList?.dmdInfoId && (name === "dmdAplyPswd" || name === "dmdAplyPswdConfirm"))) + .map(({ name, label, component }) => { + const error = form.formState.errors[name]; + const inputId = `${name}-input`; + const errorId = `${name}-error`; + const isRequired = ["ctpvCd", "sggCd", "dmdCoNm", "dmdInfoId", "daddr", "telno", "dmdAplyPswd", "dmdAplyPswdConfirm"].includes(name); + + return ( + <FormField + key={String(name)} + control={form.control} + name={name} + render={({ field }) => ( + <div className="mb-4"> + <label htmlFor={inputId} className="block text-gray-800 font-medium text-xl"> + {label} + {isRequired && <span className="text-red-500 ml-1">*</span>} + </label> + + {React.cloneElement(component(field), { + id: inputId, + "aria-required": isRequired, + "aria-invalid": !!error, + "aria-describedby": error ? errorId : undefined + })} + + {error?.message && ( + <p id={errorId} role="alert" className="text-red-500 text-lg mt-1"> + {error.message} + </p> + )} + </div> + )} + /> + ); + })} </> ); } \ No newline at end of file diff --git a/packages/common/business_components/src/ui/sdmInsertForm/index.tsx b/packages/common/business_components/src/ui/sdmInsertForm/index.tsx index 13e461910..4091139b9 100644 --- a/packages/common/business_components/src/ui/sdmInsertForm/index.tsx +++ b/packages/common/business_components/src/ui/sdmInsertForm/index.tsx @@ -25,7 +25,7 @@ const createFormSchema = (isBznmTotNoDisabled: boolean, isDuplicate: boolean ,up dmdCoNm: z.string() .min(1, "회사명을 입력해주세요") .max(20, "회사명은 20자 이내로 입력해주세요"), - dmdInfoPk: z.string() + dmdInfoId: z.string() .min(1, "사업자등록번호를 입력해주세요") .regex(/^\d{10}$/, "사업자등록번호는 10자리 숫자만 입력 가능합니다") .refine(() => isBznmTotNoDisabled ? !isDuplicate : true, "이미 등록된 사업자등록번호입니다"), @@ -41,13 +41,25 @@ const createFormSchema = (isBznmTotNoDisabled: boolean, isDuplicate: boolean ,up telno: z.string() .min(8, "전화번호는 8자 이상 입력해주세요") .max(11, "전화번호는 11자 이내로 입력해주세요") - .regex(/^[0-9-]+$/, "전화번호는 숫자와 하이픈(-)만 입력 가능합니다"), - rgtr: z.string().optional(), - dmdAplyPswd : updateList?.dmdInfoPk // 업데이트일경우 + .regex(/^[0-9]+$/, "전화번호는 숫자만 입력 가능합니다"), + rgtr: z.string().optional(), + dmdAplyPswd : updateList?.dmdInfoId // 업데이트일경우 ? z.string().optional() // 패스워드는 선택 사항이며 길이 제한 없음 : z.string().optional() .refine((val) => !val || (val.length >= 8 && val.length <= 16), "패스워드는 8자 이상 16자 이내로 입력해주세요"), - + dmdAplyPswdConfirm: updateList?.dmdInfoId // 업데이트일경우 + ? z.string().optional() // 패스워드는 선택 사항이며 길이 제한 없음 + :z.string() + .min(1, "패스워드확인을 입력해주세요"), +}).refine((data) => { + // 패스워드 확인 검증 + if (data.dmdAplyPswd && data.dmdAplyPswdConfirm) { + return data.dmdAplyPswd === data.dmdAplyPswdConfirm; + } + return true; +}, { + message: "패스워드가 일치하지 않습니다", + path: ["dmdAplyPswdConfirm"], // 에러를 패스워드 확인 필드에 표시 }); type FormSchema = z.infer<ReturnType<typeof createFormSchema>>; @@ -56,12 +68,13 @@ const defaultValues: FormSchema = { ctpvCd: '', sggCd: '', dmdCoNm: '', - dmdInfoPk: '', + dmdInfoId: '', bznmTotNo: '', daddr: '', telno: '', rgtr: '', dmdAplyPswd: '', + dmdAplyPswdConfirm: '', }; /** @@ -139,17 +152,17 @@ export default function SdmInsertForm({selectSidoData, selectSigunData, dataList return; } - const dmdInfoPk = data.dmdInfoPk + (data.bznmTotNo || '0000'); + const dmdInfoId = data.dmdInfoId + (data.bznmTotNo || '0000'); const formData = { ...data, - dmdInfoPk, - bzmnNo: data.dmdInfoPk, + dmdInfoId, + bzmnNo: data.dmdInfoId, }; console.log(' formData ', formData) /* await insertSdmData(formData); */ - rtnInputData(formData as DemandCompType); + rtnInputData(formData as unknown as DemandCompType); //router.push(`/pages/sdmMng/sdmInfoInsert?${queryParams.toString()}`); diff --git a/packages/common/business_components/src/ui/sdmInsertForm/sdmNameBox.tsx b/packages/common/business_components/src/ui/sdmInsertForm/sdmNameBox.tsx index 3c8fd7192..59800b31e 100644 --- a/packages/common/business_components/src/ui/sdmInsertForm/sdmNameBox.tsx +++ b/packages/common/business_components/src/ui/sdmInsertForm/sdmNameBox.tsx @@ -1,14 +1,14 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import type { DemandCompType } from './form'; +import React, { useCallback, useEffect, useRef, useState } from "react"; +import type { DemandCompType } from "./form"; export type thisProps = { label: string | undefined; initValue: string | undefined; - dataList : DemandCompType[]; - rtnValue : (searchText: string, pageCnt: number) => void; - rtnData : (item: DemandCompType) => void; - rtnText : (text: string) => void; -} + dataList: DemandCompType[]; + rtnValue: (searchText: string, pageCnt: number) => void; + rtnData: (item: DemandCompType) => void; + rtnText: (text: string) => void; +}; /** * 수요처 검색 컴포넌트 @@ -18,88 +18,110 @@ export type thisProps = { * @param dataList 검색할 때 넘기는 데이터 리스트 * @param rtnData 검색 결과 반환 함수 */ -export const SdmSearchForm = ({label=undefined, initValue=undefined, rtnValue=()=>{}, dataList=[], rtnData=()=>{}, rtnText=()=>{} } :thisProps) => { - +export const SdmSearchForm = ({ + label = undefined, + initValue = undefined, + rtnValue = () => {}, + dataList = [], + rtnData = () => {}, + rtnText = () => {}, +}: thisProps) => { const [isOpen, setIsOpen] = useState(false); - const [textValue, setTextValue] = useState(initValue || ''); + const [textValue, setTextValue] = useState(initValue || ""); const wrapperRef = useRef<HTMLDivElement>(null); - const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { - const value = e.target.value; - setTextValue(value); - rtnValue(value, 10); - rtnText(value); - if (value) { - setIsOpen(true); - } else { - setIsOpen(false); - } - }, [rtnValue, rtnText]); + const handleInputChange = useCallback( + (e: React.ChangeEvent<HTMLInputElement>) => { + const value = e.target.value; + setTextValue(value); + rtnValue(value, 10); + rtnText(value); + if (value) { + setIsOpen(true); + } else { + setIsOpen(false); + } + }, + [rtnValue, rtnText], + ); - const handleSuggestionClick = useCallback((item: DemandCompType) => { - setTextValue(item.dmdCoNm); - rtnData(item); - setIsOpen(false); - }, [rtnData]); + const handleSuggestionClick = useCallback( + (item: DemandCompType) => { + setTextValue(item.dmdCoNm); + rtnData(item); + setIsOpen(false); + }, + [rtnData], + ); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { - if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) { + if ( + wrapperRef.current && + !wrapperRef.current.contains(event.target as Node) + ) { setIsOpen(false); } }; - document.addEventListener('mousedown', handleClickOutside); + document.addEventListener("mousedown", handleClickOutside); return () => { - document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener("mousedown", handleClickOutside); }; }, []); useEffect(() => { - if(initValue&& initValue?.length>0){ - setTextValue(initValue) + if (initValue && initValue?.length > 0) { + setTextValue(initValue); } - },[initValue]) - + }, [initValue]); + return ( - <div ref={wrapperRef}> + <div className="relative" ref={wrapperRef}> <input type="text" value={textValue} onChange={handleInputChange} /* onFocus={() => setIsOpen(true)} */ - className="w-full h-[3.2rem] !text-xl pl-3 text-gray-600 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + className="h-[3.2rem] w-full rounded-md border border-gray-300 pl-3 !text-xl text-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder={`${label}을(를) 입력하세요`} /> {isOpen && ( - <div className=" z-1 w-full mt-2 bg-white border border-gray-300 rounded-md shadow-lg h-90 overflow-auto"> + <div className="absolute top-full left-0 z-50 mb-2 max-h-72 w-full overflow-auto rounded-md border border-gray-300 bg-white shadow-lg"> <div className="max-h-72"> - {dataList && dataList.length > 0 ? dataList.map((item : DemandCompType, index : number) => ( - <div - key={index} - className="!text-xl w-full text-left px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto" - onClick={() => handleSuggestionClick(item)} - role="button" - tabIndex={0} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - handleSuggestionClick(item); - } - }} - > - {item.dmdInfoPk.substring(10,14) !== '0000' ? ( - <> - <span className="text-gray-500">사업자번호 : </span> {item.dmdInfoPk.substring(0,10)}  - <span className="text-gray-500">종사업자번호 : </span> {item.dmdInfoPk.substring(10,14)}  - </> - ) : ( - <> - <span className="text-gray-500">사업자번호 : </span> {item.dmdInfoPk.substring(0,10)}  - </> - )} - <span className="float-right">{item.dmdCoNm}</span> - </div> - )) : ( - <div className="!text-xl w-full px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto"> + {dataList && dataList.length > 0 ? ( + dataList.map((item: DemandCompType, index: number) => ( + <div + key={index} + className="w-full cursor-pointer overflow-auto px-5 py-3 text-left !text-xl hover:bg-gray-100" + onClick={() => handleSuggestionClick(item)} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + handleSuggestionClick(item); + } + }} + > + {item.dmdInfoId.substring(10, 14) !== "0000" ? ( + <> + <span className="text-gray-500">사업자번호 : </span>{" "} + {item.dmdInfoId.substring(0, 10)}  + <span className="text-gray-500"> + 종사업자번호 :{" "} + </span>{" "} + {item.dmdInfoId.substring(10, 14)}  + </> + ) : ( + <> + <span className="text-gray-500">사업자번호 : </span>{" "} + {item.dmdInfoId.substring(0, 10)}  + </> + )} + <span className="float-right">{item.dmdCoNm}</span> + </div> + )) + ) : ( + <div className="w-full cursor-pointer overflow-auto px-5 py-3 !text-xl hover:bg-gray-100"> 검색 결과가 없습니다. </div> )} @@ -107,5 +129,5 @@ export const SdmSearchForm = ({label=undefined, initValue=undefined, rtnValue=() </div> )} </div> - ) -} \ No newline at end of file + ); +}; diff --git a/packages/common/business_components/src/ui/search_base_form/index.tsx b/packages/common/business_components/src/ui/search_base_form/index.tsx index 2649c67db..f99963f14 100644 --- a/packages/common/business_components/src/ui/search_base_form/index.tsx +++ b/packages/common/business_components/src/ui/search_base_form/index.tsx @@ -21,6 +21,10 @@ type SearchBaseType = { btnClass:string; ref:React.RefObject<ReturnType>; isAdmin:boolean; + rightClassName?: string; + leftClassName?: string; + leftSelectClassName?: string; + buttonOrder?: ("query" | "init" | "excel")[]; } export type ReturnType = { partValue: string; @@ -49,7 +53,11 @@ function SearchBaseForm ({ excelClick, initClick, ref, - isAdmin=false + isAdmin=false, + rightClassName, + leftClassName, + leftSelectClassName, + buttonOrder }: SearchBaseType) { const [partValue, setPartValue] = useState<string>("A"); const [sidoValue, setSidoValue] = useState<string>("0"); @@ -61,6 +69,30 @@ function SearchBaseForm ({ const [middleRegionValue, setMiddleRegionValue] = useState<string>("0"); const [envPartValue, setEnvPartValue] = useState<string>("0"); const [resetTrigger, setResetTrigger] = useState(false); + const renderButton = (type: "query" | "init" | "excel") => { + switch (type) { + case "query": + return ( + <Button className={btnClass} onClick={queryVal} key="query"> + 조회 + </Button> + ); + case "init": + return ( + <Button className={btnClass} onClick={initVal} key="init"> + 초기화 + </Button> + ); + case "excel": + return ( + <Button className={btnClass} color="green" onClick={excelDownload} key="excel"> + {excelName ? excelName : "엑셀다운로드"} + </Button> + ); + default: + return null; + } + }; const queryVal = () => { console.log("조회버튼 클릭"); queryClick({ @@ -133,20 +165,14 @@ function SearchBaseForm ({ setRtnSigunText={setSigunText} resetTrigger={resetTrigger} isAdmin={isAdmin} + leftClassName={leftClassName} + leftSelectClassName={leftSelectClassName} > {leftChildren} </SearchFormLeft> - <SearchFormRight> - <Button className={btnClass} color="green" onClick={excelDownload}> - {excelName?excelName:"엑셀다운로드"} - </Button> + <SearchFormRight className={rightClassName}> <div> - <Button className={btnClass} onClick={initVal}> - 초기화 - </Button> - <Button className={btnClass} onClick={queryVal}> - 조회 - </Button> + {(buttonOrder ?? ["excel", "init", "query"]).map(renderButton)} </div> {rightChildren} </SearchFormRight> diff --git a/packages/common/business_components/src/ui/search_form_left/index.tsx b/packages/common/business_components/src/ui/search_form_left/index.tsx index e7858df2a..a989995ed 100644 --- a/packages/common/business_components/src/ui/search_form_left/index.tsx +++ b/packages/common/business_components/src/ui/search_form_left/index.tsx @@ -44,6 +44,8 @@ type SearchFormLeftProps = { setRtnSigunText?: (value: string) => void; resetTrigger?: boolean; isAdmin?: boolean; + leftClassName?: string; + leftSelectClassName?: string; } export type EnvPartSelectDataType = SelectDataType & { @@ -56,7 +58,7 @@ const selectPartData:SelectDataType[] = [ { text: "환경청별", val: 'G' }, ]; -function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], selectSigunData = [], selectRegionData = [], selectUnitWaterShedData = [], selectMiddleRegionData = [], selectEnvPartData = [], setRtnPartValue, setRtnSidoValue, setRtnSidoText, setRtnSigunValue, setRtnSigunText, setRtnRegionValue, setRtnUnitWaterShedValue, setRtnMiddleRegionValue, setRtnEnvPartValue, setRtnEnvPartSidoValue, setRtnEnvPartSigunValue, resetTrigger = false, isAdmin = false }: SearchFormLeftProps) { +function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], selectSigunData = [], selectRegionData = [], selectUnitWaterShedData = [], selectMiddleRegionData = [], selectEnvPartData = [], setRtnPartValue, setRtnSidoValue, setRtnSidoText, setRtnSigunValue, setRtnSigunText, setRtnRegionValue, setRtnUnitWaterShedValue, setRtnMiddleRegionValue, setRtnEnvPartValue, setRtnEnvPartSidoValue, setRtnEnvPartSigunValue, resetTrigger = false, isAdmin = false, leftClassName, leftSelectClassName }: SearchFormLeftProps) { /******************선택된 데이터******************/ const [partValue, setPartValue] = useState<string>('A'); //행정별 시도 시군구 @@ -199,7 +201,7 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel setRtnSidoText(selectedSido?.text || ''); } initializeSigunData(val); - onSelectValue(selectSigunData[0]?.val || '0', 'sigun'); + onSelectValue('0', 'sigun'); break; case 'sigun': if (setRtnSigunValue) { @@ -301,31 +303,31 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel resetAllValues(); // 리셋 후 트리거를 false로 설정 if (setRtnPartValue) { - setRtnPartValue('0'); + setRtnPartValue('A'); } } }, [resetTrigger]); return ( isShowPart ? ( - <div className="grid grid-cols-12 gap-5 w-11/12 m-4"> - <SelectBox className="col-span-3" label={'구분'} selectArray={isAdmin ? selectPartData.slice(0, 1) : selectPartData} onSelectValue={(val) => onSelectValue(val.toString(), 'part')} value={partValue} selectClass={undefined}/> + <div className={leftClassName?leftClassName:`grid grid-cols-12 gap-5 w-11/12 m-4`}> + <SelectBox className={leftSelectClassName?leftSelectClassName:`col-span-3`} label={'구분'} selectArray={isAdmin ? selectPartData.slice(0, 1) : selectPartData} onSelectValue={(val) => onSelectValue(val.toString(), 'part')} value={partValue} selectClass={undefined}/> {partValue === 'R' ? ( <> - <SelectBox className="col-span-3" label={'유역'} selectArray={selectRegionData} onSelectValue={(val) => onSelectValue(val.toString(), 'region')} value={regionValue} selectClass={undefined}/> - <SelectBox className="col-span-3" label={'단위유역'} selectArray={filteredUnitWaterShedData} onSelectValue={(val) => onSelectValue(val.toString(), 'unitWaterShed')} value={unitWaterShedValue} selectClass={undefined}/> - <SelectBox className="col-span-3" label={'중권역'} selectArray={filteredMiddleRegionData} onSelectValue={(val) => onSelectValue(val.toString(), 'middleRegion')} value={middleRegionValue} selectClass={undefined}/> + <SelectBox className={leftSelectClassName?leftSelectClassName:`col-span-3`} label={'유역'} selectArray={selectRegionData} onSelectValue={(val) => onSelectValue(val.toString(), 'region')} value={regionValue} selectClass={undefined}/> + <SelectBox className={leftSelectClassName?leftSelectClassName:`col-span-3`} label={'단위유역'} selectArray={filteredUnitWaterShedData} onSelectValue={(val) => onSelectValue(val.toString(), 'unitWaterShed')} value={unitWaterShedValue} selectClass={undefined}/> + <SelectBox className={leftSelectClassName?leftSelectClassName:`col-span-3`} label={'중권역'} selectArray={filteredMiddleRegionData} onSelectValue={(val) => onSelectValue(val.toString(), 'middleRegion')} value={middleRegionValue} selectClass={undefined}/> </> ) : partValue === 'G' ? ( <> - <SelectBox className="col-span-3" label={'환경청'} selectArray={filteredEnvPartData} onSelectValue={(val) => onSelectValue(val.toString(), 'envPart')} value={envPartValue} selectClass={undefined}/> - <SelectBox className="col-span-3" label={'시도'} selectArray={filteredEnvPartSidoData} onSelectValue={(val) => onSelectValue(val.toString(), 'envPartSido')} value={sidoValue} selectClass={undefined}/> - <SelectBox className="col-span-3" label={'시군구'} selectArray={filteredEnvPartSigunData} onSelectValue={(val) => onSelectValue(val.toString(), 'envPartSigun')} value={sigunValue} selectClass={undefined}/> + <SelectBox className={leftSelectClassName?leftSelectClassName:`col-span-3`} label={'환경청'} selectArray={filteredEnvPartData} onSelectValue={(val) => onSelectValue(val.toString(), 'envPart')} value={envPartValue} selectClass={undefined}/> + <SelectBox className={leftSelectClassName?leftSelectClassName:`col-span-3`} label={'시도'} selectArray={filteredEnvPartSidoData} onSelectValue={(val) => onSelectValue(val.toString(), 'envPartSido')} value={sidoValue} selectClass={undefined}/> + <SelectBox className={leftSelectClassName?leftSelectClassName:`col-span-3`} label={'시군구'} selectArray={filteredEnvPartSigunData} onSelectValue={(val) => onSelectValue(val.toString(), 'envPartSigun')} value={sigunValue} selectClass={undefined}/> </> ) : ( <> - <SelectBox className="col-span-3" label={'시도'} selectArray={selectSidoData} onSelectValue={(val) => onSelectValue(val.toString(), 'sido')} value={sidoValue} selectClass={undefined}/> - <SelectBox className="col-span-3" label={'시군구'} selectArray={filteredSigunData} onSelectValue={(val) => onSelectValue(val.toString(), 'sigun')} value={sigunValue} selectClass={undefined}/> + <SelectBox className={leftSelectClassName?leftSelectClassName:`col-span-3`} label={'시도'} selectArray={selectSidoData} onSelectValue={(val) => onSelectValue(val.toString(), 'sido')} value={sidoValue} selectClass={undefined}/> + <SelectBox className={leftSelectClassName?leftSelectClassName:`col-span-3`} label={'시군구'} selectArray={filteredSigunData} onSelectValue={(val) => onSelectValue(val.toString(), 'sigun')} value={sigunValue} selectClass={undefined}/> </> )} {children} diff --git a/packages/common/business_components/src/ui/select/index-2.tsx b/packages/common/business_components/src/ui/select/index-2.tsx deleted file mode 100644 index 8ff1f4582..000000000 --- a/packages/common/business_components/src/ui/select/index-2.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @remarks - * 이 버전은 SearchModal에서 사용하기 위해 `SelectContent`의 z-index를 `z-[101]`로 수정하였습니다. - */ -import { Label } from "@common/components"; -import React from "react"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./lib/selectBox"; - -/** 차트 기본 스타일 */ -interface SelectBox1 { - text:string, - val:string|number -} - -const SelectBox2 = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> & { - label:string, - selectClass:string|undefined, - className:string, - selectArray:SelectBox1[], - onSelectValue:(val:string|number)=>void, - value?: string|number - } ->(({ label, className, selectClass, selectArray, onSelectValue, value }, ref)=>{ - const placeHolder = selectArray?.[0]?.text ?? ""; - - return ( - <div className={className+" flex items-center flex-nowrap"} ref={ref}> - {label && <Label className="inline-block w-auto whitespace-nowrap text-xl text-gray-600 mr-2">{label}</Label>} - <Select onValueChange={onSelectValue} value={value !== undefined ? String(value) : undefined}> - <SelectTrigger className={selectClass?selectClass:"flex-1 p-2 border rounded"}> - <SelectValue placeholder={placeHolder} /> - </SelectTrigger> - <SelectContent className="z-[101]"> - {selectArray.map((selectArray) => ( - <SelectItem key={selectArray.val} value={String(selectArray.val)}>{selectArray.text}</SelectItem> - ))} - </SelectContent> - </Select> - </div> - ) -}) - -export { SelectBox2 }; diff --git a/packages/common/business_components/src/ui/select/index.tsx b/packages/common/business_components/src/ui/select/index.tsx index 6fa78ccf2..f9dac92d6 100644 --- a/packages/common/business_components/src/ui/select/index.tsx +++ b/packages/common/business_components/src/ui/select/index.tsx @@ -1,42 +1,107 @@ +// selectBox + import { Label } from "@common/components"; -import React from "react"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./lib/selectBox"; +import React, { useEffect, useState } from "react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "./lib/selectBox"; /** 차트 기본 스타일 */ interface SelectBox1 { - text:string, - val:string|number + text: string; + val: string | number; + hidden?: boolean; } const SelectBox = React.forwardRef< HTMLDivElement, React.HTMLAttributes<HTMLDivElement> & { - label:string, - selectClass:string|undefined, - className:string, - selectArray:SelectBox1[], - onSelectValue:(val:string|number)=>void, - value?: string|number, - readOnly?: boolean // readOnly 속성 추가 + label: string; + selectClass: string | undefined; + className: string; + selectArray: SelectBox1[]; + onSelectValue: (val: string | number) => void; + value?: string | number; + readOnly?: boolean; // readOnly 속성 추가 + selectContentClass?: string; + labelClass?: string; } ->(({ label, className, selectClass, selectArray, onSelectValue, value, readOnly = false }, ref)=>{ - const placeHolder = selectArray?.[0]?.text ?? ""; +>( + ( + { + label, + className, + selectClass, + selectArray, + onSelectValue, + value, + readOnly = false, + selectContentClass, + labelClass = "w-20 whitespace-nowrap text-xl text-gray-600", + }, + ref, + ) => { + // hidden이 true인 첫 번째 항목을 placeholder로 사용 + const hiddenItem = selectArray.find((item) => item.hidden); + const placeHolder = hiddenItem?.text ?? "선택"; + const [isOpen, setIsOpen] = useState(false); + // hidden이 true인 항목만 드롭다운에서 제외 + const visibleItems = selectArray.filter((item) => !item.hidden); - return ( - <div className={className+" flex items-center"} ref={ref}> - {label && <Label className="whitespace-nowrap w-20 text-xl text-gray-600">{label}</Label>} - <Select onValueChange={onSelectValue} value={value !== undefined ? String(value) : undefined}> - <SelectTrigger className={selectClass?selectClass:"flex-1 p-2 border rounded"}> - <SelectValue placeholder={placeHolder} /> - </SelectTrigger> - <SelectContent> - {selectArray.map((selectArray) => ( - <SelectItem key={selectArray.val} value={String(selectArray.val)}>{selectArray.text}</SelectItem> - ))} - </SelectContent> - </Select> - </div> - ) -}) + // 현재 값이 hidden 항목이거나 빈 값인 경우 undefined로 설정하여 placeholder가 표시되도록 함 + const currentValue = + value !== undefined && + hiddenItem && + (String(value) === String(hiddenItem.val) || + value === "" || + value === "0" || + value === 0) + ? undefined + : value !== undefined + ? String(value) + : undefined; + + return ( + <div className={className + " flex items-center"} ref={ref}> + {label && ( + <Label className={labelClass}> + {label} + </Label> + )} + <Select + open={isOpen} + onOpenChange={(open) => { + if (readOnly === true) { + // readOnly 일경우 Select 선택안하게 하기 + setIsOpen(false); + } else { + setIsOpen(open); + } + }} + onValueChange={onSelectValue} + value={currentValue} + // disabled={readOnly} //물재이용 감리 지적사항으로 일단 주석처리 + > + <SelectTrigger + className={selectClass ? selectClass : "flex-1 rounded border p-2"} + > + <SelectValue placeholder={placeHolder} /> + </SelectTrigger> + <SelectContent className={selectContentClass}> + {visibleItems.map((selectArray) => ( + <SelectItem key={selectArray.val} value={String(selectArray.val)}> + {selectArray.text} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + ); + }, +); export { SelectBox }; diff --git a/packages/common/business_components/src/ui/tab/index-2.tsx b/packages/common/business_components/src/ui/tab/index-2.tsx deleted file mode 100644 index 71c103835..000000000 --- a/packages/common/business_components/src/ui/tab/index-2.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Tabs, TabsList, TabsTrigger } from '@common/components/ui'; -import React from 'react'; - -interface TabItem { - value: string; - label: string; - onClick?: () => void; -} - -interface UITabProps extends React.HTMLAttributes<HTMLDivElement> { - defaultValue?: string; - tabList: TabItem[]; - children: React.ReactNode; -} - -const UITab2 = React.forwardRef< - HTMLDivElement, - UITabProps ->(({defaultValue, tabList, children}, ref)=>{ - return ( - <Tabs defaultValue={defaultValue || tabList[0]?.value} className={"w-full"}> - <TabsList className="flex"> - {tabList.map((tab) => ( - <TabsTrigger - key={tab.value} - value={tab.value} - onClick={tab.onClick} - className="py-2 px-4 text-sm font-medium border-b-2 focus:outline-none data-[state=active]:border-blue-500 data-[state=active]:text-blue-600 data-[state=inactive]:border-transparent data-[state=inactive]:text-gray-500 hover:text-gray-700 hover:border-gray-300" - > - {tab.label} - </TabsTrigger> - ))} - </TabsList> - {children} - </Tabs> - ) -}) -export { UITab2 }; diff --git a/packages/common/business_components/src/ui/tab/index.tsx b/packages/common/business_components/src/ui/tab/index.tsx index aee969460..273a4b907 100644 --- a/packages/common/business_components/src/ui/tab/index.tsx +++ b/packages/common/business_components/src/ui/tab/index.tsx @@ -9,7 +9,7 @@ interface TabItem { interface UITabProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'dir'> { defaultValue?: string; tabList: TabItem[]; - children: React.ReactNode; + children?: React.ReactNode; onValueChange?: (value: string) => void; } @@ -20,7 +20,7 @@ const UITab = React.forwardRef< return ( <Tabs defaultValue={defaultValue || tabList[0]?.value} - className={"w-full space-x-1 border-b border-gray-200"} + className={"w-full space-x-1 border-gray-200"} onValueChange={onValueChange} ref={ref} {...props} diff --git a/packages/common/business_components/src/ui/table/index-2.tsx b/packages/common/business_components/src/ui/table/index-2.tsx deleted file mode 100644 index b6a9750b0..000000000 --- a/packages/common/business_components/src/ui/table/index-2.tsx +++ /dev/null @@ -1,157 +0,0 @@ -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; - height?: string; // ✅ 추가된 prop -}; - -type UiHeadProps = { - id: string; - title: string; - align?: string; -}; - -export function UiTable2({ - tableData, - headlist, - pageSize, - total, - children, - headName, - cellClick, - onPageChange, - className = '', - resetPageNo = false, - cur, - height = '45vh', // ✅ 기본값 설정 -}: 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; - - return ( - <> - {headName && ( - <div className="card-header"> - <div className="flex w-full items-center justify-between"> - <h3 className="text-2xl font-medium text-label">{headName}</h3> - </div> - </div> - )} - - <div className={`card !m-[1.2rem] !p-0 relative ${className}`}> - {/* ✅ 외부에서 조절 가능한 높이 적용 */} - <div className="overflow-auto" style={{ height }}> - <Table variant="secondary" className="w-full"> - <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"> - {tableData.length > 0 ? ( - tableData.map((el, index) => ( - <TableRow key={index + "row"}> - {headlist.map((head) => ( - <TableCell - key={head.id + index} - 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)} - > - {typeof el[head.id] === 'number' - ? String(el[head.id]).replace(/\B(?=(\d{3})+(?!\d))/g, ",") - : String(el[head.id] ?? '')} - </TableCell> - ))} - </TableRow> - )) - ) : ( - <TableRow> - <TableCell colSpan={headlist.length} className="p-0"> - {/* ✅ 높이 보장 + 가운데 정렬용 div */} - <div className="flex justify-center items-center text-gray-500" style={{ height: `calc(${height} - 70px)` }}> - <span className="text-xl">🔍 검색 결과가 없습니다.</span> - </div> - </TableCell> - </TableRow> - )} - </TableBody> - </Table> - </div> - - {/* 페이지네이션 하단 고정 */} - { ( - <div className="sticky left-0 right-0 bottom-0 bg-white z-20 mt-5"> - <UiPageNations - total={total} - cur={curState} - pageSize={pageSize} - onPageChange={handlePageChange} - /> - </div> - )} - </div> - </> - ); -} diff --git a/packages/common/business_components/src/ui/table/index.tsx b/packages/common/business_components/src/ui/table/index.tsx index cd25bc8db..c3f5f8a60 100644 --- a/packages/common/business_components/src/ui/table/index.tsx +++ b/packages/common/business_components/src/ui/table/index.tsx @@ -1,40 +1,63 @@ -"use client" +"use client"; -import { Checkbox, Table, TableBody, TableCell, TableHeader, TableRow } from "@common/components"; -import { useEffect, useState } from "react"; +import { + Checkbox, + Table, + TableBody, + TableCell, + TableHeader, + TableRow, +} from "@common/components"; +import { useEffect, useState, useRef } from "react"; import { HeadMakeColSpan } from "./HeadMakeColSpan"; import UiPageNations from "./uiPageNations"; -import { AlignVerticalDistributeCenterIcon, Check } from "lucide-react"; -import { boolean } from "zod"; - +import { Check } from "lucide-react"; +export type ChkClass = { + chk: boolean; + readonly: boolean; +}; type UiTableProps = { headName: React.ReactNode | undefined; - tableData: { [key: string]:any}[]; + tableData: { [key: string]: any }[]; headlist: UiHeadProps[]; pageSize: number; total: number; - cellClick : ((index:number|undefined, headId:string)=>void )| undefined; + cellClick: ((index: number | undefined, headId: string) => void) | undefined; onPageChange: (page: number) => void; className?: string; resetPageNo?: boolean; children?: React.ReactNode | undefined; boldArray?: number[]; resetSelection?: boolean; - index?:number; -} + index?: number; +}; type UiHeadProps = { id: string; title: string; align?: string; - visible?:boolean; -} + visible?: boolean; +}; -export function UiTable({ tableData, headlist, pageSize, total, headName, children, cellClick, onPageChange, className = '', resetPageNo = false, boldArray = [], resetSelection = false, index=-1 }: UiTableProps) { +export function UiTable({ + tableData, + headlist, + pageSize, + total, + headName, + children, + cellClick, + onPageChange, + className = "", + resetPageNo = false, + boldArray = [], + resetSelection = false, + index = -1, +}: UiTableProps) { const [cur, setCur] = useState<number>(1); const [idx, setIdx] = useState<number>(index); - const [isCheck, seIsCheck] = useState<boolean>(false); - + const [headerHeight, setHeaderHeight] = useState<number>(0); + const headerRef = useRef<HTMLTableSectionElement>(null); useEffect(() => { if (resetPageNo) { @@ -49,88 +72,177 @@ export function UiTable({ tableData, headlist, pageSize, total, headName, childr } }, [resetSelection]); + useEffect(() => { + if (headerRef.current) { + setHeaderHeight(headerRef.current.offsetHeight - 5); + } + }, [tableData, headlist]); + const handlePageChange = (page: number) => { setCur(page); onPageChange(page); - } + if (index == 0) + setIdx(0) + }; - const handleCellClick = (index: number,headId:string )=> { + const handleCellClick = (index: number, headId: string) => { if (cellClick) { - cellClick(index,headId); + cellClick(index, headId); setIdx(index); } }; - + return ( <> - {headName ? <div className="card-header"> - <h3 className="text-2xl font-medium text-label">{headName}</h3> - </div> : <></>} - <div className={`card !m-[1.2rem] !p-0r ${className}`}> + {headName ? ( + <div className="card-header"> + <h3 className="text-2xl font-medium text-label">{headName}</h3> + </div> + ) : ( + <></> + )} + <div className={`card !p-0r !m-[1.2rem] ${className}`}> <Table variant="secondary"> - <TableHeader className="font-bold"> + <TableHeader ref={headerRef} className="font-bold"> {children ? children : <HeadMakeColSpan upHeadList={headlist} />} </TableHeader> - <TableBody className='text-center'> - {tableData.length > 0 ? tableData?.map((el: { [key: string]: any}, index) => ( - <TableRow key={`row-${index}`}> - {headlist - .filter((item) => item.visible != false) - .map((head) => ( - head.id && head.id.length > 1 && ( - boldArray.includes(index) - ? - <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' : '' - } !font-bold`} - onClick={() => handleCellClick(index, head.id)} - key={`cell-${head.id}-${index}`} - > - {typeof el[head.id] !== 'number'?String(el[head.id] ?? ''): String(el[head.id]).includes('.') - ? String(el[head.id]).split('.').map((part, index) => - index === 0 ? part.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : part - ).join('.') - : String(el[head.id] ?? '').replace(/\B(?=(\d{3})+(?!\d))/g, ",")} - </TableCell> - : - <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(index, head.id)} - key={`cell-${head.id}-${index}`} - > - {typeof el[head.id] === 'boolean' - ?<Checkbox - checked={el[head.id] === true} - onCheckedChange={(checked) => { - el[head.id]= checked; - }} - > - <Check className="h-6 w-6 text-primary" /> - </Checkbox> - : - typeof el[head.id] !== 'number'? - String(el[head.id] ?? ''): - String(el[head.id]).includes('.') - ? String(el[head.id]).split('.').map((part, index) => - index === 0 ? part.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : part - ).join('.') - : - String(el[head.id] ?? '').replace(/\B(?=(\d{3})+(?!\d))/g, ",")} - </TableCell> - ) - ))} + <TableBody className="text-center"> + {tableData.length > 0 ? ( + tableData?.map((el: Record<string, unknown>, index) => ( + <TableRow key={`row-${index}`}> + {headlist + .filter((item) => item.visible != false) + .map( + (head) => + head.id && + head.id.length > 1 && + (boldArray.includes(index) ? ( + <TableCell + className={`${head.align === "right" ? "text-right" : head.align === "center" ? "text-center" : "text-center"} ${cellClick && idx === index + ? "cursor-pointer !font-[1000]" + : cellClick + ? "cursor-pointer" + : "" + } sticky top-20 z-40 bg-secondary !font-bold`} + style={{ top: `${headerHeight + index * 32 - 4}px` }} + onClick={() => handleCellClick(index, head.id)} + key={`cell-${head.id}-${index}`} + > + {typeof el[head.id] !== "number" + ? String(el[head.id] ?? "") + : String(el[head.id]).includes(".") + ? String(el[head.id]) + .split(".") + .map((part, index) => + index === 0 + ? part.replace( + /\B(?=(\d{3})+(?!\d))/g, + ",", + ) + : part, + ) + .join(".") + : String(el[head.id] ?? "").replace( + /\B(?=(\d{3})+(?!\d))/g, + ",", + )} + </TableCell> + ) : ( + <TableCell + className={`${head.align === "right" ? "text-right" : head.align === "center" ? "text-center" : "text-center"} ${cellClick && idx === index + ? "cursor-pointer !font-[1000]" + : cellClick + ? "cursor-pointer" + : "" + } mt-[${boldArray.length * 40}px]`} + onClick={() => handleCellClick(index, head.id)} + key={`cell-${head.id}-${index}`} + > + { + //체크박스 유형애서 readOnly가 필요한 경우를 위해서 ChkClass 를 사용해서 readOnly 처리함. + typeof el[head.id] === "object" && + el[head.id] !== null && + "chk" in (el[head.id] as object) && + "readonly" in (el[head.id] as object) ? ( + <Checkbox + checked={ + (el[head.id] as ChkClass)?.chk === true + } + onCheckedChange={(checked) => { + { + console.log(el[head.id]); + } + if ( + (el[head.id] as ChkClass).readonly == true + ) + return; + (el[head.id] as ChkClass).chk = + Boolean(checked); + }} + > + <Check className="h-6 w-6 text-primary" /> + </Checkbox> + ) : typeof el[head.id] === "boolean" ? ( + <Checkbox + checked={el[head.id] === true} + onCheckedChange={(checked) => { + el[head.id] = checked; + }} + > + <Check className="h-6 w-6 text-primary" /> + </Checkbox> + ) : typeof el[head.id] !== "number" ? ( + String(el[head.id] ?? "") + ) : String(el[head.id]).includes(".") ? ( + String(el[head.id]) + .split(".") + .map((part, index) => + index === 0 + ? part.replace( + /\B(?=(\d{3})+(?!\d))/g, + ",", + ) + : part, + ) + .join(".") + ) : ( + String(el[head.id] ?? "").replace( + /\B(?=(\d{3})+(?!\d))/g, + ",", + ) + ) + } + </TableCell> + )), + )} + </TableRow> + )) + ) : ( + <TableRow> + <TableCell + colSpan={ + headlist.filter((item) => item.visible != false).length + } + className="h-24 text-center text-gray-500" + > + 조회된 결과가 없습니다. + </TableCell> </TableRow> - )) : <></>} + )} </TableBody> </Table> - {total <= 0 ? <></> : <UiPageNations total={total} cur={cur} pageSize={pageSize} onPageChange={handlePageChange} />} + {total <= 0 ? ( + <></> + ) : ( + <UiPageNations + total={total} + cur={cur} + pageSize={pageSize} + onPageChange={handlePageChange} + /> + )} </div> </> ); -} \ No newline at end of file +} diff --git a/packages/common/business_components/src/ui/table/uiPageNations.tsx b/packages/common/business_components/src/ui/table/uiPageNations.tsx index 891fc4c81..ca1f8e0c0 100644 --- a/packages/common/business_components/src/ui/table/uiPageNations.tsx +++ b/packages/common/business_components/src/ui/table/uiPageNations.tsx @@ -40,7 +40,8 @@ export default function UiPageNations(commonPageNationsProps: CommonPageNationsP <PaginationItem> <PaginationPrevious href="#" - onClick={() => { + onClick={(e) => { + e.preventDefault(); if (startPage > 1) handlePageChange(startPage - 1); }} /> @@ -53,7 +54,9 @@ export default function UiPageNations(commonPageNationsProps: CommonPageNationsP <PaginationLink href="#" isActive={pageNum === pageProps.cur} - onClick={() => handlePageChange(pageNum)} + onClick={(e) => { + e.preventDefault(); + handlePageChange(pageNum)}} > {pageNum} </PaginationLink> @@ -65,7 +68,8 @@ export default function UiPageNations(commonPageNationsProps: CommonPageNationsP <PaginationItem> <PaginationNext href="#" - onClick={() => { + onClick={(e) => { + e.preventDefault(); if (endPage < totalPages) handlePageChange(endPage + 1); }} /> diff --git a/packages/common/components/src/ui/table/index.tsx b/packages/common/components/src/ui/table/index.tsx index 3d4e2251d..83be366fd 100644 --- a/packages/common/components/src/ui/table/index.tsx +++ b/packages/common/components/src/ui/table/index.tsx @@ -31,7 +31,7 @@ const TableHeader = React.forwardRef< >(({ className, ...props }, ref) => ( <thead ref={ref} - className={cn("sticky top-0 z-10 bg-secondary [&_tr]:border-b", className)} + className={cn("sticky -top-3 z-10 bg-secondary [&_tr]:border-b", className)} {...props} /> )); diff --git a/packages/common/network/client.ts b/packages/common/network/client.ts index 1b82d31c6..587fcf541 100644 --- a/packages/common/network/client.ts +++ b/packages/common/network/client.ts @@ -22,6 +22,7 @@ class Client { withCredentials: true, }); + this.axiosInstance.interceptors.request.use((config) => { const token = Cookies.get(CLAIM_NAME); const userId = Cookies.get(AUTH_USER_ID); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index a64e257a3..000000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,10377 +0,0 @@ -lockfileVersion: '6.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -overrides: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - -importers: - - .: - dependencies: - chart.js: - specifier: ^4.4.8 - version: 4.4.8 - react: - specifier: ^18.3.1 - version: 18.3.1 - react-chartjs-2: - specifier: ^5.3.0 - version: 5.3.0(chart.js@4.4.8)(react@18.3.1) - react-dom: - specifier: ^18.3.1 - version: 18.3.1(react@18.3.1) - zustand: - specifier: ^5.0.3 - version: 5.0.3(@types/react@18.3.18)(react@18.3.1) - devDependencies: - '@types/node': - specifier: ^22.13.9 - version: 22.13.9 - '@types/react': - specifier: ^18.3.18 - version: 18.3.18 - '@types/react-dom': - specifier: ^18.3.5 - version: 18.3.5(@types/react@18.3.18) - turbo: - specifier: 2.4.2 - version: 2.4.2 - - apps/common/features: - dependencies: - '@common/assets': - specifier: workspace:* - version: link:../../../packages/common/assets - '@common/components': - specifier: workspace:* - version: link:../../../packages/common/components - '@common/config': - specifier: workspace:* - version: link:../../../packages/common/config - '@common/network': - specifier: workspace:* - version: link:../../../packages/common/network - '@fontsource-variable/noto-sans-kr': - specifier: ^5.2.5 - version: 5.2.5 - '@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) - '@minoru/react-dnd-treeview': - specifier: ^3.5.0 - version: 3.5.0(dnd-core@16.0.1)(react-dnd@16.0.1)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-icons': - specifier: ^1.3.2 - version: 1.3.2(react@18.3.1) - '@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-query-devtools': - specifier: ^5.75.5 - version: 5.75.5(@tanstack/react-query@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) - '@tinymce/tinymce-react': - specifier: ^6.1.0 - version: 6.1.0(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.3.0 - version: 15.3.0(react-dom@18.3.1)(react@18.3.1) - next-themes: - specifier: ^0.4.6 - version: 0.4.6(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-dnd: - specifier: ^16.0.1 - version: 16.0.1(@types/node@22.13.9)(@types/react@18.3.18)(react@18.3.1) - react-hook-form: - specifier: ^7.54.2 - version: 7.54.2(react@18.3.1) - sonner: - specifier: ^2.0.1 - version: 2.0.1(react-dom@18.3.1)(react@18.3.1) - tailwindcss-animate: - specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.17) - tus-js-client: - specifier: ^4.3.1 - version: 4.3.1 - 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 - '@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/cs: - dependencies: - '@common/assets': - specifier: workspace:* - version: link:../../packages/common/assets - '@common/components': - specifier: workspace:* - version: link:../../packages/common/components - '@common/config': - specifier: workspace:* - version: link:../../packages/common/config - '@fontsource-variable/noto-sans-kr': - specifier: ^5.2.5 - version: 5.2.5 - '@frontend-opensource/use-react-hooks': - specifier: ^1.1.4 - version: 1.1.4 - '@fullcalendar/core': - specifier: ^6.1.17 - version: 6.1.17 - '@fullcalendar/daygrid': - specifier: ^6.1.17 - version: 6.1.17(@fullcalendar/core@6.1.17) - '@fullcalendar/interaction': - specifier: ^6.1.17 - version: 6.1.17(@fullcalendar/core@6.1.17) - '@fullcalendar/react': - specifier: ^6.1.17 - version: 6.1.17(@fullcalendar/core@6.1.17)(react-dom@18.3.1)(react@18.3.1) - '@fullcalendar/timegrid': - specifier: ^6.1.17 - version: 6.1.17(@fullcalendar/core@6.1.17) - '@hookform/resolvers': - specifier: ^4.1.3 - version: 4.1.3(react-hook-form@7.54.2) - '@minoru/react-dnd-treeview': - specifier: 3.5.1-alpha.1 - version: 3.5.1-alpha.1(dnd-core@16.0.1)(react-dnd@16.0.1)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-icons': - specifier: ^1.3.2 - version: 1.3.2(react@18.3.1) - '@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) - '@radix-ui/react-tooltip': - specifier: ^1.1.8 - version: 1.1.8(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@tanstack/react-query': - specifier: ^5.67.1 - version: 5.67.1(react@18.3.1) - '@tanstack/react-query-devtools': - specifier: ^5.75.5 - version: 5.75.5(@tanstack/react-query@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) - '@tinymce/tinymce-react': - specifier: ^6.1.0 - version: 6.1.0(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 - lucide-react: - specifier: ^0.474.0 - version: 0.474.0(react@18.3.1) - next: - specifier: 15.3.0 - version: 15.3.0(react-dom@18.3.1)(react@18.3.1) - next-qrcode: - specifier: ^2.5.1 - version: 2.5.1(react@18.3.1) - next-themes: - specifier: ^0.4.6 - version: 0.4.6(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-dnd: - specifier: ^16.0.1 - version: 16.0.1(@types/node@22.13.5)(@types/react@18.3.18)(react@18.3.1) - react-hook-form: - specifier: ^7.54.2 - version: 7.54.2(react@18.3.1) - sonner: - specifier: ^2.0.1 - version: 2.0.1(react-dom@18.3.1)(react@18.3.1) - tailwindcss-animate: - specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.17) - tus-js-client: - specifier: ^4.3.1 - version: 4.3.1 - 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 - '@types/js-cookie': - specifier: ^3.0.6 - version: 3.0.6 - '@types/node': - specifier: ^22.13.1 - version: 22.13.5 - '@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/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) - ol: - specifier: ^7.3.0 - version: 7.5.2 - ol-ext: - specifier: ^4.0.11 - version: 4.0.31(ol@7.5.2) - proj4: - specifier: ^2.9.0 - version: 2.15.0 - 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) - tus-js-client: - specifier: ^4.3.1 - version: 4.3.1 - 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/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 - '@hookform/resolvers': - specifier: ^4.1.3 - version: 4.1.3(react-hook-form@7.54.2) - '@tanstack/react-table': - specifier: ^8.21.2 - version: 8.21.2(react-dom@18.3.1)(react@18.3.1) - class-variance-authority: - specifier: ^0.7.1 - version: 0.7.1 - clsx: - specifier: ^2.1.1 - version: 2.1.1 - lucide-react: - specifier: ^0.474.0 - version: 0.474.0(react@18.3.1) - moment: - specifier: ^2.30.1 - version: 2.30.1 - next: - specifier: 15.1.7 - version: 15.1.7(react-dom@18.3.1)(react@18.3.1) - react-hook-form: - specifier: ^7.54.2 - version: 7.54.2(react@18.3.1) - react-hot-toast: - specifier: ^2.5.2 - version: 2.5.2(react-dom@18.3.1)(react@18.3.1) - tailwind-merge: - specifier: ^3.0.1 - version: 3.0.2 - tailwindcss-animate: - specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.17) - tus-js-client: - specifier: ^4.3.1 - version: 4.3.1 - zod: - specifier: ^3.24.2 - version: 3.24.2 - devDependencies: - '@eslint/eslintrc': - specifier: ^3 - version: 3.3.0 - '@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 - tailwindcss: - specifier: ^3.4.1 - version: 3.4.17 - typescript: - specifier: ^5 - version: 5.6.3 - - packages/common/assets: - devDependencies: - '@types/node': - specifier: ^20.11.0 - version: 20.17.19 - tsup: - specifier: ^8.0.1 - version: 8.3.6(postcss@8.5.3)(typescript@5.6.3) - typescript: - specifier: ^5.3.3 - version: 5.6.3 - - packages/common/business_components: - dependencies: - '@common/components': - specifier: workspace:* - version: link:../components - '@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-scroll-area': - specifier: ^1.2.3 - version: 1.2.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-select': - specifier: ^2.1.6 - version: 2.1.6(@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) - axios: - specifier: 1.8.1 - version: 1.8.1 - class-variance-authority: - specifier: ^0.7.1 - version: 0.7.1 - clsx: - specifier: ^2.1.1 - version: 2.1.1 - date-fns: - specifier: ^3.6.0 - version: 3.6.0 - lucide-react: - specifier: ^0.474.0 - version: 0.474.0(react@18.3.1) - path: - specifier: ^0.12.7 - version: 0.12.7 - react-day-picker: - specifier: ^8.10.1 - version: 8.10.1(date-fns@3.6.0)(react@18.3.1) - react-dropzone: - specifier: ^14.3.8 - version: 14.3.8(react@18.3.1) - react-hook-form: - specifier: ^7.54.2 - version: 7.54.2(react@18.3.1) - recharts: - specifier: ^2.15.1 - version: 2.15.1(react-dom@18.3.1)(react@18.3.1) - sonner: - specifier: ^2.0.1 - version: 2.0.1(react-dom@18.3.1)(react@18.3.1) - tailwind-merge: - specifier: ^3.0.1 - version: 3.0.2 - xlsx: - specifier: ^0.18.5 - version: 0.18.5 - xlsx-populate: - specifier: ^1.21.0 - version: 1.21.0 - zod: - specifier: ^3.24.2 - version: 3.24.2 - devDependencies: - '@eslint/js': - specifier: ^9.17.0 - version: 9.21.0 - '@storybook/addon-essentials': - specifier: ^8.5.3 - version: 8.6.0(@types/react@18.3.18)(storybook@8.6.0) - '@storybook/addon-interactions': - specifier: ^8.5.3 - version: 8.6.0(storybook@8.6.0) - '@storybook/addon-onboarding': - specifier: ^8.5.3 - version: 8.6.0(storybook@8.6.0) - '@storybook/addon-themes': - specifier: ^8.5.4 - version: 8.6.0(storybook@8.6.0) - '@storybook/blocks': - specifier: ^8.5.3 - version: 8.6.0(react-dom@18.3.1)(react@18.3.1)(storybook@8.6.0) - '@storybook/manager-api': - specifier: ^8.5.5 - version: 8.6.0(storybook@8.6.0) - '@storybook/react': - specifier: ^8.5.3 - version: 8.6.0(@storybook/test@8.6.0)(react-dom@18.3.1)(react@18.3.1)(storybook@8.6.0)(typescript@5.6.3) - '@storybook/react-vite': - specifier: ^8.5.3 - version: 8.6.0(@storybook/test@8.6.0)(react-dom@18.3.1)(react@18.3.1)(storybook@8.6.0)(typescript@5.6.3)(vite@6.2.0) - '@storybook/test': - specifier: ^8.5.3 - version: 8.6.0(storybook@8.6.0) - '@storybook/theming': - specifier: ^8.5.5 - version: 8.6.0(storybook@8.6.0) - '@vitejs/plugin-react': - specifier: ^4.3.4 - version: 4.3.4(vite@6.2.0) - '@xlsx/xlsx-populate': - specifier: ^0.0.8 - version: 0.0.8 - autoprefixer: - specifier: ^10.4.20 - version: 10.4.20(postcss@8.5.3) - eslint: - specifier: ^9.17.0 - version: 9.21.0 - eslint-plugin-react-hooks: - specifier: ^5.0.0 - version: 5.1.0(eslint@9.21.0) - eslint-plugin-react-refresh: - specifier: ^0.4.16 - version: 0.4.19(eslint@9.21.0) - eslint-plugin-storybook: - specifier: ^0.11.2 - version: 0.11.3(eslint@9.21.0)(typescript@5.6.3) - globals: - specifier: ^15.14.0 - version: 15.15.0 - postcss: - specifier: ^8.5.3 - 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) - storybook: - specifier: ^8.5.3 - version: 8.6.0(prettier@3.5.2) - tailwindcss: - specifier: ^3.4.17 - version: 3.4.17 - tsup: - specifier: 8.3.6 - version: 8.3.6(postcss@8.5.3)(typescript@5.6.3) - typescript: - specifier: ~5.6.2 - version: 5.6.3 - typescript-eslint: - specifier: ^8.18.2 - version: 8.25.0(eslint@9.21.0)(typescript@5.6.3) - vite: - specifier: ^6.0.5 - version: 6.2.0(@types/node@22.13.9) - vite-tsconfig-paths: - specifier: 5.1.4 - version: 5.1.4(typescript@5.6.3)(vite@6.2.0) - - packages/common/components: - dependencies: - '@common/assets': - specifier: workspace:* - version: link:../assets - '@common/config': - specifier: workspace:* - version: link:../config - '@frontend-opensource/use-react-hooks': - specifier: ^1.1.4 - version: 1.1.4 - '@minoru/react-dnd-treeview': - specifier: ^3.5.0 - version: 3.5.0(dnd-core@16.0.1)(react-dnd@16.0.1)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-accordion': - specifier: ^1.2.3 - version: 1.2.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-checkbox': - specifier: ^1.1.4 - version: 1.1.4(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-collapsible': - specifier: ^1.1.3 - version: 1.1.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-dialog': - specifier: ^1.1.6 - version: 1.1.6(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-dropdown-menu': - specifier: ^2.1.6 - version: 2.1.6(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-icons': - specifier: ^1.3.2 - version: 1.3.2(react@18.3.1) - '@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-popover': - specifier: ^1.1.6 - version: 1.1.6(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-progress': - specifier: ^1.1.2 - version: 1.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-radio-group': - specifier: ^1.2.2 - version: 1.2.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-scroll-area': - specifier: ^1.2.3 - version: 1.2.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-select': - specifier: ^2.1.6 - version: 2.1.6(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-separator': - specifier: ^1.1.2 - version: 1.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-slider': - specifier: ^1.2.3 - version: 1.2.3(@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) - '@radix-ui/react-switch': - specifier: ^1.1.3 - version: 1.1.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-tabs': - specifier: ^1.1.3 - version: 1.1.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-tooltip': - specifier: ^1.1.8 - version: 1.1.8(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-visually-hidden': - specifier: ^1.1.2 - version: 1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(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) - '@types/js-cookie': - specifier: ^3.0.6 - version: 3.0.6 - axios: - specifier: ^1.8.1 - version: 1.8.1 - chart.js: - specifier: ^4.4.8 - version: 4.4.8 - class-variance-authority: - specifier: ^0.7.1 - version: 0.7.1 - clsx: - specifier: ^2.1.1 - version: 2.1.1 - date-fns: - specifier: ^3.6.0 - version: 3.6.0 - dayjs: - specifier: ^1.11.13 - version: 1.11.13 - embla-carousel-react: - specifier: ^8.6.0 - version: 8.6.0(react@18.3.1) - js-cookie: - specifier: ^3.0.5 - version: 3.0.5 - lucide-react: - specifier: ^0.474.0 - version: 0.474.0(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-day-picker: - specifier: ^9.7.0 - version: 9.7.0(react@18.3.1) - react-dnd: - specifier: ^16.0.1 - version: 16.0.1(@types/node@22.13.5)(@types/react@18.3.18)(react@18.3.1) - react-dnd-html5-backend: - specifier: ^16.0.1 - version: 16.0.1 - tailwind-merge: - specifier: ^3.0.1 - version: 3.0.2 - tailwindcss-animate: - specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.17) - devDependencies: - '@eslint/js': - specifier: ^9.17.0 - version: 9.21.0 - '@faker-js/faker': - specifier: ^9.5.0 - version: 9.5.0 - '@storybook/addon-essentials': - specifier: ^8.5.3 - version: 8.6.0(@types/react@18.3.18)(storybook@8.6.0) - '@storybook/addon-interactions': - specifier: ^8.5.3 - version: 8.6.0(storybook@8.6.0) - '@storybook/addon-onboarding': - specifier: ^8.5.3 - version: 8.6.0(storybook@8.6.0) - '@storybook/addon-themes': - specifier: ^8.5.4 - version: 8.6.0(storybook@8.6.0) - '@storybook/blocks': - specifier: ^8.5.3 - version: 8.6.0(react-dom@18.3.1)(react@18.3.1)(storybook@8.6.0) - '@storybook/manager-api': - specifier: ^8.5.5 - version: 8.6.0(storybook@8.6.0) - '@storybook/react': - specifier: ^8.5.3 - version: 8.6.0(@storybook/test@8.6.0)(react-dom@18.3.1)(react@18.3.1)(storybook@8.6.0)(typescript@5.6.3) - '@storybook/react-vite': - specifier: ^8.5.3 - version: 8.6.0(@storybook/test@8.6.0)(react-dom@18.3.1)(react@18.3.1)(storybook@8.6.0)(typescript@5.6.3)(vite@6.2.0) - '@storybook/test': - specifier: ^8.5.3 - version: 8.6.0(storybook@8.6.0) - '@storybook/theming': - specifier: ^8.5.5 - version: 8.6.0(storybook@8.6.0) - '@types/node': - specifier: ^22.13.1 - version: 22.13.5 - '@vitejs/plugin-react': - specifier: ^4.3.4 - version: 4.3.4(vite@6.2.0) - autoprefixer: - specifier: ^10.4.20 - version: 10.4.20(postcss@8.5.3) - eslint: - specifier: ^9.17.0 - version: 9.21.0 - eslint-plugin-react-hooks: - specifier: ^5.0.0 - version: 5.1.0(eslint@9.21.0) - eslint-plugin-react-refresh: - specifier: ^0.4.16 - version: 0.4.19(eslint@9.21.0) - eslint-plugin-storybook: - specifier: ^0.11.2 - version: 0.11.3(eslint@9.21.0)(typescript@5.6.3) - globals: - specifier: ^15.14.0 - version: 15.15.0 - postcss: - specifier: ^8.5.1 - 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) - storybook: - specifier: ^8.5.3 - version: 8.6.0(prettier@3.5.2) - tailwindcss: - specifier: ^3.4.17 - version: 3.4.17 - tsup: - specifier: 8.3.6 - version: 8.3.6(postcss@8.5.3)(typescript@5.6.3) - typescript: - specifier: ~5.6.2 - version: 5.6.3 - typescript-eslint: - specifier: ^8.18.2 - version: 8.25.0(eslint@9.21.0)(typescript@5.6.3) - vite: - specifier: ^6.0.5 - version: 6.2.0(@types/node@22.13.5) - vite-tsconfig-paths: - specifier: 5.1.4 - version: 5.1.4(typescript@5.6.3)(vite@6.2.0) - - packages/common/config: - devDependencies: - eslint: - specifier: ^9.8.0 - version: 9.21.0 - tsup: - specifier: ^8.2.3 - version: 8.3.6(postcss@8.5.3)(typescript@5.6.3) - typescript: - specifier: ^5.5.3 - version: 5.6.3 - vite: - specifier: ^5.3.5 - version: 5.4.18(@types/node@22.13.9) - - packages/common/network: - dependencies: - '@types/js-cookie': - specifier: ^3.0.6 - version: 3.0.6 - axios: - specifier: ^1.8.1 - version: 1.8.1 - js-cookie: - specifier: ^3.0.5 - version: 3.0.5 - devDependencies: - '@types/node': - specifier: ^20.11.19 - version: 20.17.19 - typescript: - specifier: ^5.3.3 - version: 5.6.3 - -packages: - - /@adobe/css-tools@4.4.2: - resolution: {integrity: sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==} - dev: true - - /@alloc/quick-lru@5.2.0: - resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} - engines: {node: '>=10'} - - /@ampproject/remapping@2.3.0: - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 - dev: true - - /@babel/code-frame@7.26.2: - resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-validator-identifier': 7.25.9 - js-tokens: 4.0.0 - picocolors: 1.1.1 - dev: true - - /@babel/compat-data@7.26.8: - resolution: {integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/core@7.26.9: - resolution: {integrity: sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==} - engines: {node: '>=6.9.0'} - dependencies: - '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.26.9 - '@babel/helper-compilation-targets': 7.26.5 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.9) - '@babel/helpers': 7.26.9 - '@babel/parser': 7.26.9 - '@babel/template': 7.26.9 - '@babel/traverse': 7.26.9 - '@babel/types': 7.26.9 - convert-source-map: 2.0.0 - debug: 4.4.0 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/generator@7.26.9: - resolution: {integrity: sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/parser': 7.26.9 - '@babel/types': 7.26.9 - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 - jsesc: 3.1.0 - dev: true - - /@babel/helper-compilation-targets@7.26.5: - resolution: {integrity: sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/compat-data': 7.26.8 - '@babel/helper-validator-option': 7.25.9 - browserslist: 4.24.4 - lru-cache: 5.1.1 - semver: 6.3.1 - dev: true - - /@babel/helper-module-imports@7.25.9: - resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/traverse': 7.26.9 - '@babel/types': 7.26.9 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/helper-module-transforms@7.26.0(@babel/core@7.26.9): - resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.26.9 - '@babel/helper-module-imports': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 - '@babel/traverse': 7.26.9 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/helper-plugin-utils@7.26.5: - resolution: {integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/helper-string-parser@7.25.9: - resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/helper-validator-identifier@7.25.9: - resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/helper-validator-option@7.25.9: - resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/helpers@7.26.9: - resolution: {integrity: sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.26.9 - '@babel/types': 7.26.9 - dev: true - - /@babel/parser@7.26.9: - resolution: {integrity: sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==} - engines: {node: '>=6.0.0'} - hasBin: true - dependencies: - '@babel/types': 7.26.9 - dev: true - - /@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.26.9): - resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.26.9 - '@babel/helper-plugin-utils': 7.26.5 - dev: true - - /@babel/plugin-transform-react-jsx-source@7.25.9(@babel/core@7.26.9): - resolution: {integrity: sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.26.9 - '@babel/helper-plugin-utils': 7.26.5 - dev: true - - /@babel/runtime@7.26.9: - resolution: {integrity: sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==} - engines: {node: '>=6.9.0'} - dependencies: - regenerator-runtime: 0.14.1 - - /@babel/template@7.26.9: - resolution: {integrity: sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/parser': 7.26.9 - '@babel/types': 7.26.9 - dev: true - - /@babel/traverse@7.26.9: - resolution: {integrity: sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.26.9 - '@babel/parser': 7.26.9 - '@babel/template': 7.26.9 - '@babel/types': 7.26.9 - debug: 4.4.0 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/types@7.26.9: - resolution: {integrity: sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-string-parser': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 - dev: true - - /@clack/core@0.3.5: - resolution: {integrity: sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ==} - dependencies: - picocolors: 1.1.1 - sisteransi: 1.0.5 - dev: true - - /@clack/prompts@0.8.2: - resolution: {integrity: sha512-6b9Ab2UiZwJYA9iMyboYyW9yJvAO9V753ZhS+DHKEjZRKAxPPOb7MXXu84lsPFG+vZt6FRFniZ8rXi+zCIw4yQ==} - dependencies: - '@clack/core': 0.3.5 - picocolors: 1.1.1 - sisteransi: 1.0.5 - dev: true - - /@date-fns/tz@1.2.0: - resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==} - dev: false - - /@dependents/detective-less@5.0.1: - resolution: {integrity: sha512-Y6+WUMsTFWE5jb20IFP4YGa5IrGY/+a/FbOSjDF/wz9gepU2hwCYSXRHP/vPwBvwcY3SVMASt4yXxbXNXigmZQ==} - engines: {node: '>=18'} - dependencies: - gonzales-pe: 4.3.0 - node-source-walk: 7.0.1 - dev: true - - /@emnapi/runtime@1.4.3: - resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} - requiresBuild: true - dependencies: - tslib: 2.8.1 - dev: false - optional: true - - /@esbuild/aix-ppc64@0.21.5: - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - requiresBuild: true - dev: true - optional: true - - /@esbuild/aix-ppc64@0.24.2: - resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - requiresBuild: true - dev: true - optional: true - - /@esbuild/aix-ppc64@0.25.0: - resolution: {integrity: sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm64@0.21.5: - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm64@0.24.2: - resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm64@0.25.0: - resolution: {integrity: sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm@0.21.5: - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm@0.24.2: - resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm@0.25.0: - resolution: {integrity: sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-x64@0.21.5: - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-x64@0.24.2: - resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-x64@0.25.0: - resolution: {integrity: sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-arm64@0.21.5: - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-arm64@0.24.2: - resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-arm64@0.25.0: - resolution: {integrity: sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-x64@0.21.5: - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-x64@0.24.2: - resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-x64@0.25.0: - resolution: {integrity: sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-arm64@0.21.5: - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-arm64@0.24.2: - resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-arm64@0.25.0: - resolution: {integrity: sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-x64@0.21.5: - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-x64@0.24.2: - resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-x64@0.25.0: - resolution: {integrity: sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm64@0.21.5: - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm64@0.24.2: - resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm64@0.25.0: - resolution: {integrity: sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm@0.21.5: - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm@0.24.2: - resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm@0.25.0: - resolution: {integrity: sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ia32@0.21.5: - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ia32@0.24.2: - resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ia32@0.25.0: - resolution: {integrity: sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-loong64@0.21.5: - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-loong64@0.24.2: - resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-loong64@0.25.0: - resolution: {integrity: sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-mips64el@0.21.5: - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-mips64el@0.24.2: - resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-mips64el@0.25.0: - resolution: {integrity: sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ppc64@0.21.5: - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ppc64@0.24.2: - resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ppc64@0.25.0: - resolution: {integrity: sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-riscv64@0.21.5: - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-riscv64@0.24.2: - resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-riscv64@0.25.0: - resolution: {integrity: sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-s390x@0.21.5: - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-s390x@0.24.2: - resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-s390x@0.25.0: - resolution: {integrity: sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-x64@0.21.5: - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-x64@0.24.2: - resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-x64@0.25.0: - resolution: {integrity: sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/netbsd-arm64@0.24.2: - resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/netbsd-arm64@0.25.0: - resolution: {integrity: sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/netbsd-x64@0.21.5: - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/netbsd-x64@0.24.2: - resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/netbsd-x64@0.25.0: - resolution: {integrity: sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/openbsd-arm64@0.24.2: - resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/openbsd-arm64@0.25.0: - resolution: {integrity: sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/openbsd-x64@0.21.5: - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/openbsd-x64@0.24.2: - resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/openbsd-x64@0.25.0: - resolution: {integrity: sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/sunos-x64@0.21.5: - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: true - optional: true - - /@esbuild/sunos-x64@0.24.2: - resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: true - optional: true - - /@esbuild/sunos-x64@0.25.0: - resolution: {integrity: sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-arm64@0.21.5: - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-arm64@0.24.2: - resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-arm64@0.25.0: - resolution: {integrity: sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-ia32@0.21.5: - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-ia32@0.24.2: - resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-ia32@0.25.0: - resolution: {integrity: sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-x64@0.21.5: - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-x64@0.24.2: - resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-x64@0.25.0: - resolution: {integrity: sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@eslint-community/eslint-utils@4.4.1(eslint@9.21.0): - resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - dependencies: - eslint: 9.21.0 - eslint-visitor-keys: 3.4.3 - dev: true - - /@eslint-community/regexpp@4.12.1: - resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - dev: true - - /@eslint/config-array@0.19.2: - resolution: {integrity: sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dependencies: - '@eslint/object-schema': 2.1.6 - debug: 4.4.0 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@eslint/core@0.12.0: - resolution: {integrity: sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dependencies: - '@types/json-schema': 7.0.15 - dev: true - - /@eslint/eslintrc@3.3.0: - resolution: {integrity: sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dependencies: - ajv: 6.12.6 - debug: 4.4.0 - espree: 10.3.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - dev: true - - /@eslint/js@9.21.0: - resolution: {integrity: sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dev: true - - /@eslint/object-schema@2.1.6: - resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dev: true - - /@eslint/plugin-kit@0.2.7: - resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dependencies: - '@eslint/core': 0.12.0 - levn: 0.4.1 - dev: true - - /@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: - resolution: {integrity: sha512-aOFdPACNvkFXYb+gNHId8UCRD5fm7Z9/RV6JgBPlgEvm1QtLS4idg2XeHMDGnHrNRCS4ki+0RauxVZxm+g9pvw==} - dependencies: - typescript: 5.6.3 - dev: true - - /@feature-sliced/steiger-plugin@0.5.4(typescript@5.6.3): - resolution: {integrity: sha512-QzkIQUqNq94fv7aV0GY8wKmlFij9CtSQEIV+/0ed33SZGrv1FjUaySO4eHPlSCcH3Y+fH9nnpTaULtqRwwT7sw==} - dependencies: - '@feature-sliced/filesystem': 2.4.0 - fastest-levenshtein: 1.0.16 - lodash-es: 4.17.21 - pluralize: 8.0.0 - precinct: 12.2.0 - tsconfck: 3.1.5(typescript@5.6.3) - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /@floating-ui/core@1.6.9: - resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==} - dependencies: - '@floating-ui/utils': 0.2.9 - dev: false - - /@floating-ui/dom@1.6.13: - resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==} - dependencies: - '@floating-ui/core': 1.6.9 - '@floating-ui/utils': 0.2.9 - dev: false - - /@floating-ui/react-dom@2.1.2(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==} - peerDependencies: - react: ^18.3.1 - react-dom: ^18.3.1 - dependencies: - '@floating-ui/dom': 1.6.13 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@floating-ui/utils@0.2.9: - resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} - dev: false - - /@fontsource-variable/noto-sans-kr@5.2.5: - resolution: {integrity: sha512-dIJyVPIBRJXtvaFmQ6GKCV93J9Fx3O2Ym5C3gDRcz5zLpEigrnlEfBnsXBUv4aDBHEF/gm/Q07d40dddGxFytg==} - dev: false - - /@frontend-opensource/use-react-hooks@1.1.4: - resolution: {integrity: sha512-otCSyciL5MVCNNYmdDdg2lEL6rbZ3XX1meYQCzapUSC4V6qx2YxMk1kUbhwEM/+vdxhYzBHsVBGD0rZWl5cgrw==} - dependencies: - async-wave: 1.8.6 - fast-deep-equal: 3.1.3 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@fullcalendar/core@6.1.17: - resolution: {integrity: sha512-0W7lnIrv18ruJ5zeWBeNZXO8qCWlzxDdp9COFEsZnyNjiEhUVnrW/dPbjRKYpL0edGG0/Lhs0ghp1z/5ekt8ZA==} - dependencies: - preact: 10.12.1 - dev: false - - /@fullcalendar/daygrid@6.1.17(@fullcalendar/core@6.1.17): - resolution: {integrity: sha512-K7m+pd7oVJ9fW4h7CLDdDGJbc9szJ1xDU1DZ2ag+7oOo1aCNLv44CehzkkknM6r8EYlOOhgaelxQpKAI4glj7A==} - peerDependencies: - '@fullcalendar/core': ~6.1.17 - dependencies: - '@fullcalendar/core': 6.1.17 - dev: false - - /@fullcalendar/interaction@6.1.17(@fullcalendar/core@6.1.17): - resolution: {integrity: sha512-AudvQvgmJP2FU89wpSulUUjeWv24SuyCx8FzH2WIPVaYg+vDGGYarI7K6PcM3TH7B/CyaBjm5Rqw9lXgnwt5YA==} - peerDependencies: - '@fullcalendar/core': ~6.1.17 - dependencies: - '@fullcalendar/core': 6.1.17 - dev: false - - /@fullcalendar/react@6.1.17(@fullcalendar/core@6.1.17)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-AA8soHhlfRH5dUeqHnfAtzDiXa2vrgWocJSK/F5qzw/pOxc9MqpuoS/nQBROWtHHg6yQUg3DoGqOOhi7dmylXQ==} - peerDependencies: - '@fullcalendar/core': ~6.1.17 - react: ^18.3.1 - react-dom: ^18.3.1 - dependencies: - '@fullcalendar/core': 6.1.17 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@fullcalendar/timegrid@6.1.17(@fullcalendar/core@6.1.17): - resolution: {integrity: sha512-K4PlA3L3lclLOs3IX8cvddeiJI9ZVMD7RA9IqaWwbvac771971foc9tFze9YY+Pqesf6S+vhS2dWtEVlERaGlQ==} - peerDependencies: - '@fullcalendar/core': ~6.1.17 - dependencies: - '@fullcalendar/core': 6.1.17 - '@fullcalendar/daygrid': 6.1.17(@fullcalendar/core@6.1.17) - dev: false - - /@hookform/resolvers@4.1.3(react-hook-form@7.54.2): - resolution: {integrity: sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ==} - peerDependencies: - react-hook-form: ^7.0.0 - dependencies: - '@standard-schema/utils': 0.3.0 - react-hook-form: 7.54.2(react@18.3.1) - dev: false - - /@humanfs/core@0.19.1: - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} - engines: {node: '>=18.18.0'} - dev: true - - /@humanfs/node@0.16.6: - resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} - engines: {node: '>=18.18.0'} - dependencies: - '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.3.1 - dev: true - - /@humanwhocodes/module-importer@1.0.1: - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - dev: true - - /@humanwhocodes/retry@0.3.1: - resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} - engines: {node: '>=18.18'} - dev: true - - /@humanwhocodes/retry@0.4.2: - resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} - engines: {node: '>=18.18'} - dev: true - - /@img/sharp-darwin-arm64@0.33.5: - resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.0.4 - dev: false - optional: true - - /@img/sharp-darwin-arm64@0.34.1: - resolution: {integrity: sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.1.0 - dev: false - optional: true - - /@img/sharp-darwin-x64@0.33.5: - resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.0.4 - dev: false - optional: true - - /@img/sharp-darwin-x64@0.34.1: - resolution: {integrity: sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.1.0 - dev: false - optional: true - - /@img/sharp-libvips-darwin-arm64@1.0.4: - resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-darwin-arm64@1.1.0: - resolution: {integrity: sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-darwin-x64@1.0.4: - resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-darwin-x64@1.1.0: - resolution: {integrity: sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-linux-arm64@1.0.4: - resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-linux-arm64@1.1.0: - resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-linux-arm@1.0.5: - resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-linux-arm@1.1.0: - resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-linux-ppc64@1.1.0: - resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-linux-s390x@1.0.4: - resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-linux-s390x@1.1.0: - resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-linux-x64@1.0.4: - resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-linux-x64@1.1.0: - resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-linuxmusl-arm64@1.0.4: - resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-linuxmusl-arm64@1.1.0: - resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-linuxmusl-x64@1.0.4: - resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-linuxmusl-x64@1.1.0: - resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-linux-arm64@0.33.5: - resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.0.4 - dev: false - optional: true - - /@img/sharp-linux-arm64@0.34.1: - resolution: {integrity: sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.1.0 - dev: false - optional: true - - /@img/sharp-linux-arm@0.33.5: - resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.0.5 - dev: false - optional: true - - /@img/sharp-linux-arm@0.34.1: - resolution: {integrity: sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.1.0 - dev: false - optional: true - - /@img/sharp-linux-s390x@0.33.5: - resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.0.4 - dev: false - optional: true - - /@img/sharp-linux-s390x@0.34.1: - resolution: {integrity: sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.1.0 - dev: false - optional: true - - /@img/sharp-linux-x64@0.33.5: - resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.0.4 - dev: false - optional: true - - /@img/sharp-linux-x64@0.34.1: - resolution: {integrity: sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.1.0 - dev: false - optional: true - - /@img/sharp-linuxmusl-arm64@0.33.5: - resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 - dev: false - optional: true - - /@img/sharp-linuxmusl-arm64@0.34.1: - resolution: {integrity: sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 - dev: false - optional: true - - /@img/sharp-linuxmusl-x64@0.33.5: - resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 - dev: false - optional: true - - /@img/sharp-linuxmusl-x64@0.34.1: - resolution: {integrity: sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.1.0 - dev: false - optional: true - - /@img/sharp-wasm32@0.33.5: - resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] - requiresBuild: true - dependencies: - '@emnapi/runtime': 1.4.3 - dev: false - optional: true - - /@img/sharp-wasm32@0.34.1: - resolution: {integrity: sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] - requiresBuild: true - dependencies: - '@emnapi/runtime': 1.4.3 - dev: false - optional: true - - /@img/sharp-win32-ia32@0.33.5: - resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-win32-ia32@0.34.1: - resolution: {integrity: sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-win32-x64@0.33.5: - resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-win32-x64@0.34.1: - resolution: {integrity: sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@isaacs/cliui@8.0.2: - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - dependencies: - string-width: 5.1.2 - string-width-cjs: /string-width@4.2.3 - strip-ansi: 7.1.0 - strip-ansi-cjs: /strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: /wrap-ansi@7.0.0 - - /@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@5.6.3)(vite@6.2.0): - resolution: {integrity: sha512-qYDdL7fPwLRI+bJNurVcis+tNgJmvWjH4YTBGXTA8xMuxFrnAz6E5o35iyzyKbq5J5Lr8mJGfrR5GXl+WGwhgQ==} - peerDependencies: - typescript: '>= 4.3.x' - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 - peerDependenciesMeta: - typescript: - optional: true - dependencies: - glob: 10.4.5 - magic-string: 0.27.0 - react-docgen-typescript: 2.2.2(typescript@5.6.3) - typescript: 5.6.3 - vite: 6.2.0(@types/node@22.13.9) - dev: true - - /@jridgewell/gen-mapping@0.3.8: - resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} - engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 - - /@jridgewell/resolve-uri@3.1.2: - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - /@jridgewell/set-array@1.2.1: - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - - /@jridgewell/source-map@0.3.6: - resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} - dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 - dev: true - - /@jridgewell/sourcemap-codec@1.5.0: - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - - /@jridgewell/trace-mapping@0.3.25: - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 - - /@juggle/resize-observer@3.4.0: - resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} - dev: false - - /@kurkle/color@0.3.4: - resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} - dev: false - - /@mapbox/jsonlint-lines-primitives@2.0.2: - resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==} - engines: {node: '>= 0.6'} - dev: false - - /@mapbox/mapbox-gl-style-spec@13.28.0: - resolution: {integrity: sha512-B8xM7Fp1nh5kejfIl4SWeY0gtIeewbuRencqO3cJDrCHZpaPg7uY+V8abuR+esMeuOjRl5cLhVTP40v+1ywxbg==} - hasBin: true - dependencies: - '@mapbox/jsonlint-lines-primitives': 2.0.2 - '@mapbox/point-geometry': 0.1.0 - '@mapbox/unitbezier': 0.0.0 - csscolorparser: 1.0.3 - json-stringify-pretty-compact: 2.0.0 - minimist: 1.2.8 - rw: 1.3.3 - sort-object: 0.3.2 - dev: false - - /@mapbox/point-geometry@0.1.0: - resolution: {integrity: sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==} - dev: false - - /@mapbox/unitbezier@0.0.0: - resolution: {integrity: sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==} - dev: false - - /@mdx-js/react@3.1.0(@types/react@18.3.18)(react@18.3.1): - resolution: {integrity: sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==} - peerDependencies: - '@types/react': ^18.3.18 - react: ^18.3.1 - dependencies: - '@types/mdx': 2.0.13 - '@types/react': 18.3.18 - react: 18.3.1 - dev: true - - /@minoru/react-dnd-treeview@3.5.0(dnd-core@16.0.1)(react-dnd@16.0.1)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-kDHGQ0UK18w6ie5EH68NynATSP9BAzNLmWBXuVSoaGkraWWFdEdAWqQxAZiMorAixFu40MzYkn2N9/Gam6q17w==} - peerDependencies: - react: ^18.3.1 - react-dnd: 15.x || 16.x - react-dom: ^18.3.1 - dependencies: - '@juggle/resize-observer': 3.4.0 - dnd-multi-backend: 8.1.2(dnd-core@16.0.1) - framer-motion: 11.18.2(react-dom@18.3.1)(react@18.3.1) - react: 18.3.1 - react-dnd: 16.0.1(@types/node@22.13.9)(@types/react@18.3.18)(react@18.3.1) - react-dnd-html5-backend: 16.0.1 - react-dnd-touch-backend: 16.0.1 - react-dom: 18.3.1(react@18.3.1) - react-use-measure: 2.1.7(react-dom@18.3.1)(react@18.3.1) - transitivePeerDependencies: - - '@emotion/is-prop-valid' - - dnd-core - dev: false - - /@minoru/react-dnd-treeview@3.5.1-alpha.1(dnd-core@16.0.1)(react-dnd@16.0.1)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-XEKBjhyltG0gzn+ioZRwhlV7+Q/uVZqTMS/tAO3On/rPNuNgqWHROFsyGdHQS/DYoENGOG8JX03chCxExOgEpA==} - peerDependencies: - react: ^18.3.1 - react-dnd: 15.x || 16.x - react-dom: ^18.3.1 - dependencies: - '@juggle/resize-observer': 3.4.0 - dnd-multi-backend: 8.1.2(dnd-core@16.0.1) - framer-motion: 11.18.2(react-dom@18.3.1)(react@18.3.1) - react: 18.3.1 - react-dnd: 16.0.1(@types/node@22.13.5)(@types/react@18.3.18)(react@18.3.1) - react-dnd-html5-backend: 16.0.1 - react-dnd-touch-backend: 16.0.1 - react-dom: 18.3.1(react@18.3.1) - react-use-measure: 2.1.7(react-dom@18.3.1)(react@18.3.1) - transitivePeerDependencies: - - '@emotion/is-prop-valid' - - dnd-core - dev: false - - /@next/env@15.1.7: - resolution: {integrity: sha512-d9jnRrkuOH7Mhi+LHav2XW91HOgTAWHxjMPkXMGBc9B2b7614P7kjt8tAplRvJpbSt4nbO1lugcT/kAaWzjlLQ==} - dev: false - - /@next/env@15.3.0: - resolution: {integrity: sha512-6mDmHX24nWlHOlbwUiAOmMyY7KELimmi+ed8qWcJYjqXeC+G6JzPZ3QosOAfjNwgMIzwhXBiRiCgdh8axTTdTA==} - dev: false - - /@next/eslint-plugin-next@15.1.7: - resolution: {integrity: sha512-kRP7RjSxfTO13NE317ek3mSGzoZlI33nc/i5hs1KaWpK+egs85xg0DJ4p32QEiHnR0mVjuUfhRIun7awqfL7pQ==} - dependencies: - fast-glob: 3.3.1 - dev: true - - /@next/swc-darwin-arm64@15.1.7: - resolution: {integrity: sha512-hPFwzPJDpA8FGj7IKV3Yf1web3oz2YsR8du4amKw8d+jAOHfYHYFpMkoF6vgSY4W6vB29RtZEklK9ayinGiCmQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@next/swc-darwin-arm64@15.3.0: - resolution: {integrity: sha512-PDQcByT0ZfF2q7QR9d+PNj3wlNN4K6Q8JoHMwFyk252gWo4gKt7BF8Y2+KBgDjTFBETXZ/TkBEUY7NIIY7A/Kw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@next/swc-darwin-x64@15.1.7: - resolution: {integrity: sha512-2qoas+fO3OQKkU0PBUfwTiw/EYpN+kdAx62cePRyY1LqKtP09Vp5UcUntfZYajop5fDFTjSxCHfZVRxzi+9FYQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@next/swc-darwin-x64@15.3.0: - resolution: {integrity: sha512-m+eO21yg80En8HJ5c49AOQpFDq+nP51nu88ZOMCorvw3g//8g1JSUsEiPSiFpJo1KCTQ+jm9H0hwXK49H/RmXg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@next/swc-linux-arm64-gnu@15.1.7: - resolution: {integrity: sha512-sKLLwDX709mPdzxMnRIXLIT9zaX2w0GUlkLYQnKGoXeWUhcvpCrK+yevcwCJPdTdxZEUA0mOXGLdPsGkudGdnA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@next/swc-linux-arm64-gnu@15.3.0: - resolution: {integrity: sha512-H0Kk04ZNzb6Aq/G6e0un4B3HekPnyy6D+eUBYPJv9Abx8KDYgNMWzKt4Qhj57HXV3sTTjsfc1Trc1SxuhQB+Tg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@next/swc-linux-arm64-musl@15.1.7: - resolution: {integrity: sha512-zblK1OQbQWdC8fxdX4fpsHDw+VSpBPGEUX4PhSE9hkaWPrWoeIJn+baX53vbsbDRaDKd7bBNcXRovY1hEhFd7w==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@next/swc-linux-arm64-musl@15.3.0: - resolution: {integrity: sha512-k8GVkdMrh/+J9uIv/GpnHakzgDQhrprJ/FbGQvwWmstaeFG06nnAoZCJV+wO/bb603iKV1BXt4gHG+s2buJqZA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@next/swc-linux-x64-gnu@15.1.7: - resolution: {integrity: sha512-GOzXutxuLvLHFDAPsMP2zDBMl1vfUHHpdNpFGhxu90jEzH6nNIgmtw/s1MDwpTOiM+MT5V8+I1hmVFeAUhkbgQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@next/swc-linux-x64-gnu@15.3.0: - resolution: {integrity: sha512-ZMQ9yzDEts/vkpFLRAqfYO1wSpIJGlQNK9gZ09PgyjBJUmg8F/bb8fw2EXKgEaHbCc4gmqMpDfh+T07qUphp9A==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@next/swc-linux-x64-musl@15.1.7: - resolution: {integrity: sha512-WrZ7jBhR7ATW1z5iEQ0ZJfE2twCNSXbpCSaAunF3BKcVeHFADSI/AW1y5Xt3DzTqPF1FzQlwQTewqetAABhZRQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@next/swc-linux-x64-musl@15.3.0: - resolution: {integrity: sha512-RFwq5VKYTw9TMr4T3e5HRP6T4RiAzfDJ6XsxH8j/ZeYq2aLsBqCkFzwMI0FmnSsLaUbOb46Uov0VvN3UciHX5A==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@next/swc-win32-arm64-msvc@15.1.7: - resolution: {integrity: sha512-LDnj1f3OVbou1BqvvXVqouJZKcwq++mV2F+oFHptToZtScIEnhNRJAhJzqAtTE2dB31qDYL45xJwrc+bLeKM2Q==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@next/swc-win32-arm64-msvc@15.3.0: - resolution: {integrity: sha512-a7kUbqa/k09xPjfCl0RSVAvEjAkYBYxUzSVAzk2ptXiNEL+4bDBo9wNC43G/osLA/EOGzG4CuNRFnQyIHfkRgQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@next/swc-win32-x64-msvc@15.1.7: - resolution: {integrity: sha512-dC01f1quuf97viOfW05/K8XYv2iuBgAxJZl7mbCKEjMgdQl5JjAKJ0D2qMKZCgPWDeFbFT0Q0nYWwytEW0DWTQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@next/swc-win32-x64-msvc@15.3.0: - resolution: {integrity: sha512-vHUQS4YVGJPmpjn7r5lEZuMhK5UQBNBRSB+iGDvJjaNk649pTIcRluDWNb9siunyLLiu/LDPHfvxBtNamyuLTw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@nodelib/fs.scandir@2.1.5: - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - /@nodelib/fs.stat@2.0.5: - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - /@nodelib/fs.walk@1.2.8: - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 - - /@nolyfill/is-core-module@1.0.39: - resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} - engines: {node: '>=12.4.0'} - dev: true - - /@petamoriken/float16@3.9.2: - resolution: {integrity: sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog==} - dev: false - - /@pkgjs/parseargs@0.11.0: - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - requiresBuild: true - optional: true - - /@radix-ui/number@1.1.0: - resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} - dev: false - - /@radix-ui/primitive@1.1.1: - resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} - dev: false - - /@radix-ui/react-accordion@1.2.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-RIQ15mrcvqIkDARJeERSuXSry2N8uYnxkdDetpfmalT/+0ntOXLkFOsh9iwlAsCv+qcmhZjbdJogIm6WBa6c4A==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/primitive': 1.1.1 - '@radix-ui/react-collapsible': 1.1.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-collection': 1.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-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@radix-ui/react-arrow@1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@radix-ui/react-checkbox@1.1.4(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/primitive': 1.1.1 - '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-presence': 1.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-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@radix-ui/react-collapsible@1.1.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-jFSerheto1X03MUC0g6R7LedNW9EEGWdg9W1+MlpkMLwGkgkbUXLPBH/KIuWKXUoeYRVY11llqbTBDzuLg7qrw==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/primitive': 1.1.1 - '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-presence': 1.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-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@radix-ui/react-collection@1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.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': 1.1.2(@types/react@18.3.18)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@radix-ui/react-compose-refs@1.1.1(@types/react@18.3.18)(react@18.3.1): - resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==} - peerDependencies: - '@types/react': ^18.3.18 - react: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.3.18 - react: 18.3.1 - dev: false - - /@radix-ui/react-context@1.1.1(@types/react@18.3.18)(react@18.3.1): - resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} - peerDependencies: - '@types/react': ^18.3.18 - react: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.3.18 - react: 18.3.1 - dev: false - - /@radix-ui/react-dialog@1.1.6(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/primitive': 1.1.1 - '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.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-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-portal': 1.1.4(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-presence': 1.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-primitive': 2.0.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': 1.1.2(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - aria-hidden: 1.2.4 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.6.3(@types/react@18.3.18)(react@18.3.1) - dev: false - - /@radix-ui/react-direction@1.1.0(@types/react@18.3.18)(react@18.3.1): - resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} - peerDependencies: - '@types/react': ^18.3.18 - react: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.3.18 - react: 18.3.1 - dev: false - - /@radix-ui/react-dismissable-layer@1.1.5(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/primitive': 1.1.1 - '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@radix-ui/react-dropdown-menu@2.1.6(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/primitive': 1.1.1 - '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-menu': 2.1.6(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@radix-ui/react-focus-guards@1.1.1(@types/react@18.3.18)(react@18.3.1): - resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} - peerDependencies: - '@types/react': ^18.3.18 - react: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.3.18 - react: 18.3.1 - dev: false - - /@radix-ui/react-focus-scope@1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@radix-ui/react-icons@1.3.2(react@18.3.1): - resolution: {integrity: sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==} - peerDependencies: - react: ^18.3.1 - dependencies: - react: 18.3.1 - dev: false - - /@radix-ui/react-id@1.1.0(@types/react@18.3.18)(react@18.3.1): - resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} - peerDependencies: - '@types/react': ^18.3.18 - react: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@types/react': 18.3.18 - react: 18.3.1 - dev: false - - /@radix-ui/react-label@2.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@radix-ui/react-menu@2.1.6(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/primitive': 1.1.1 - '@radix-ui/react-collection': 1.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-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.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-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-popper': 1.2.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-portal': 1.1.4(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-presence': 1.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-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-roving-focus': 1.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': 1.1.2(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - aria-hidden: 1.2.4 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.6.3(@types/react@18.3.18)(react@18.3.1) - dev: false - - /@radix-ui/react-popover@1.1.6(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/primitive': 1.1.1 - '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.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-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-popper': 1.2.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-portal': 1.1.4(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-presence': 1.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-primitive': 2.0.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': 1.1.2(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - aria-hidden: 1.2.4 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.6.3(@types/react@18.3.18)(react@18.3.1) - dev: false - - /@radix-ui/react-popper@1.2.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-arrow': 1.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-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-rect': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/rect': 1.1.0 - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@radix-ui/react-portal@1.1.4(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@radix-ui/react-presence@1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@radix-ui/react-primitive@2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/react-slot': 1.1.2(@types/react@18.3.18)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@radix-ui/react-progress@1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@radix-ui/react-radio-group@1.2.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-xtCsqt8Rp09FK50ItqEqTJ7Sxanz8EM8dnkVIhJrc/wkMMomSmXHvYbhv3E7Zx4oXh98aaLt9W679SUYXg4IDA==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/primitive': 1.1.1 - '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-presence': 1.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-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-roving-focus': 1.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-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@radix-ui/react-roving-focus@1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/primitive': 1.1.1 - '@radix-ui/react-collection': 1.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-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@radix-ui/react-scroll-area@1.2.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-l7+NNBfBYYJa9tNqVcP2AGvxdE3lmE6kFTBXdvHgUaZuy+4wGCL1Cl2AfaR7RKyimj7lZURGLwFO59k4eBnDJQ==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/number': 1.1.0 - '@radix-ui/primitive': 1.1.1 - '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-presence': 1.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-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@radix-ui/react-select@2.1.6(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/number': 1.1.0 - '@radix-ui/primitive': 1.1.1 - '@radix-ui/react-collection': 1.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-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.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-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-popper': 1.2.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-portal': 1.1.4(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.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': 1.1.2(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - aria-hidden: 1.2.4 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.6.3(@types/react@18.3.18)(react@18.3.1) - dev: false - - /@radix-ui/react-separator@1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@radix-ui/react-slider@1.2.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-nNrLAWLjGESnhqBqcCNW4w2nn7LxudyMzeB6VgdyAnFLC6kfQgnAjSL2v6UkQTnDctJBlxrmxfplWS4iYjdUTw==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/number': 1.1.0 - '@radix-ui/primitive': 1.1.1 - '@radix-ui/react-collection': 1.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-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@radix-ui/react-slot@1.1.2(@types/react@18.3.18)(react@18.3.1): - resolution: {integrity: sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==} - peerDependencies: - '@types/react': ^18.3.18 - react: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@types/react': 18.3.18 - react: 18.3.1 - dev: false - - /@radix-ui/react-switch@1.1.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/primitive': 1.1.1 - '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@radix-ui/react-tabs@1.1.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/primitive': 1.1.1 - '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-presence': 1.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-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-roving-focus': 1.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-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@radix-ui/react-tooltip@1.1.8(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/primitive': 1.1.1 - '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-popper': 1.2.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-portal': 1.1.4(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-presence': 1.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-primitive': 2.0.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': 1.1.2(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.18)(react@18.3.1): - resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} - peerDependencies: - '@types/react': ^18.3.18 - react: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.3.18 - react: 18.3.1 - dev: false - - /@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.18)(react@18.3.1): - resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} - peerDependencies: - '@types/react': ^18.3.18 - react: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@types/react': 18.3.18 - react: 18.3.1 - dev: false - - /@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.3.18)(react@18.3.1): - resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==} - peerDependencies: - '@types/react': ^18.3.18 - react: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@types/react': 18.3.18 - react: 18.3.1 - dev: false - - /@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.18)(react@18.3.1): - resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} - peerDependencies: - '@types/react': ^18.3.18 - react: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.3.18 - react: 18.3.1 - dev: false - - /@radix-ui/react-use-previous@1.1.0(@types/react@18.3.18)(react@18.3.1): - resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} - peerDependencies: - '@types/react': ^18.3.18 - react: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.3.18 - react: 18.3.1 - dev: false - - /@radix-ui/react-use-rect@1.1.0(@types/react@18.3.18)(react@18.3.1): - resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} - peerDependencies: - '@types/react': ^18.3.18 - react: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@radix-ui/rect': 1.1.0 - '@types/react': 18.3.18 - react: 18.3.1 - dev: false - - /@radix-ui/react-use-size@1.1.0(@types/react@18.3.18)(react@18.3.1): - resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} - peerDependencies: - '@types/react': ^18.3.18 - react: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) - '@types/react': 18.3.18 - react: 18.3.1 - dev: false - - /@radix-ui/react-visually-hidden@1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==} - peerDependencies: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - dependencies: - '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) - '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@radix-ui/rect@1.1.0: - resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} - dev: false - - /@react-dnd/asap@5.0.2: - resolution: {integrity: sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==} - dev: false - - /@react-dnd/invariant@4.0.2: - resolution: {integrity: sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==} - dev: false - - /@react-dnd/shallowequal@4.0.2: - resolution: {integrity: sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==} - dev: false - - /@rollup/pluginutils@5.1.4: - resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - dependencies: - '@types/estree': 1.0.6 - estree-walker: 2.0.2 - picomatch: 4.0.2 - dev: true - - /@rollup/rollup-android-arm-eabi@4.34.8: - resolution: {integrity: sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-android-arm64@4.34.8: - resolution: {integrity: sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-darwin-arm64@4.34.8: - resolution: {integrity: sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-darwin-x64@4.34.8: - resolution: {integrity: sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-freebsd-arm64@4.34.8: - resolution: {integrity: sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-freebsd-x64@4.34.8: - resolution: {integrity: sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm-gnueabihf@4.34.8: - resolution: {integrity: sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm-musleabihf@4.34.8: - resolution: {integrity: sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm64-gnu@4.34.8: - resolution: {integrity: sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm64-musl@4.34.8: - resolution: {integrity: sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-loongarch64-gnu@4.34.8: - resolution: {integrity: sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-powerpc64le-gnu@4.34.8: - resolution: {integrity: sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-riscv64-gnu@4.34.8: - resolution: {integrity: sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-s390x-gnu@4.34.8: - resolution: {integrity: sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-x64-gnu@4.34.8: - resolution: {integrity: sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-x64-musl@4.34.8: - resolution: {integrity: sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-win32-arm64-msvc@4.34.8: - resolution: {integrity: sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-win32-ia32-msvc@4.34.8: - resolution: {integrity: sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-win32-x64-msvc@4.34.8: - resolution: {integrity: sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@rtsao/scc@1.1.0: - resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - dev: true - - /@rushstack/eslint-patch@1.10.5: - resolution: {integrity: sha512-kkKUDVlII2DQiKy7UstOR1ErJP8kUKAQ4oa+SQtM0K+lPdmmjj0YnnxBgtTVYH7mUKtbsxeFC9y0AmK7Yb78/A==} - dev: true - - /@sindresorhus/merge-streams@2.3.0: - resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} - engines: {node: '>=18'} - dev: true - - /@standard-schema/utils@0.3.0: - resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} - dev: false - - /@storybook/addon-actions@8.6.0(storybook@8.6.0): - resolution: {integrity: sha512-lI03stcI28wN8trjp7zx0PzN1NRxSpU/551slzEhSM0tghcLxcfLZR/wh4Jp0T68Obj7z//YXPttZ9PBFajs6w==} - peerDependencies: - storybook: ^8.6.0 - dependencies: - '@storybook/global': 5.0.0 - '@types/uuid': 9.0.8 - dequal: 2.0.3 - polished: 4.3.1 - storybook: 8.6.0(prettier@3.5.2) - uuid: 9.0.1 - dev: true - - /@storybook/addon-backgrounds@8.6.0(storybook@8.6.0): - resolution: {integrity: sha512-MbMPjuC19jJBXqQext8rENBACLKujDn+MuxIbNYMmn2ljzeYFp87tVxzlCB7XY7rV0zvkHxH8N/zDnPl7pBTcQ==} - peerDependencies: - storybook: ^8.6.0 - dependencies: - '@storybook/global': 5.0.0 - memoizerific: 1.11.3 - storybook: 8.6.0(prettier@3.5.2) - ts-dedent: 2.2.0 - dev: true - - /@storybook/addon-controls@8.6.0(storybook@8.6.0): - resolution: {integrity: sha512-oc+mssxmNY+D26jyJ+IZ1u8bmzveCO2wqpUl6vtLM5MruchZAHzHqa2ZIn+H9KOEqcKlYVDF5n6oEuprcnrsdg==} - peerDependencies: - storybook: ^8.6.0 - dependencies: - '@storybook/global': 5.0.0 - dequal: 2.0.3 - storybook: 8.6.0(prettier@3.5.2) - ts-dedent: 2.2.0 - dev: true - - /@storybook/addon-docs@8.6.0(@types/react@18.3.18)(storybook@8.6.0): - resolution: {integrity: sha512-ytYn5ec2Tp62t9emjXgm4Ds+eG7SiSg/vNeOwM6L1lM2UCV2/XdzntbqlUb/FHehSXIv9eRJDSe5BBzPieUUaw==} - peerDependencies: - storybook: ^8.6.0 - dependencies: - '@mdx-js/react': 3.1.0(@types/react@18.3.18)(react@18.3.1) - '@storybook/blocks': 8.6.0(react-dom@18.3.1)(react@18.3.1)(storybook@8.6.0) - '@storybook/csf-plugin': 8.6.0(storybook@8.6.0) - '@storybook/react-dom-shim': 8.6.0(react-dom@18.3.1)(react@18.3.1)(storybook@8.6.0) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - storybook: 8.6.0(prettier@3.5.2) - ts-dedent: 2.2.0 - transitivePeerDependencies: - - '@types/react' - dev: true - - /@storybook/addon-essentials@8.6.0(@types/react@18.3.18)(storybook@8.6.0): - resolution: {integrity: sha512-T3R9Q1hq6eJKy758gQgYEuAYxcddGZ8eGBLIyIJEgzpwsUnN5mF+rIqx8LoWxpWu/xiKzg17LqCBnJbLGkRZUA==} - peerDependencies: - storybook: ^8.6.0 - dependencies: - '@storybook/addon-actions': 8.6.0(storybook@8.6.0) - '@storybook/addon-backgrounds': 8.6.0(storybook@8.6.0) - '@storybook/addon-controls': 8.6.0(storybook@8.6.0) - '@storybook/addon-docs': 8.6.0(@types/react@18.3.18)(storybook@8.6.0) - '@storybook/addon-highlight': 8.6.0(storybook@8.6.0) - '@storybook/addon-measure': 8.6.0(storybook@8.6.0) - '@storybook/addon-outline': 8.6.0(storybook@8.6.0) - '@storybook/addon-toolbars': 8.6.0(storybook@8.6.0) - '@storybook/addon-viewport': 8.6.0(storybook@8.6.0) - storybook: 8.6.0(prettier@3.5.2) - ts-dedent: 2.2.0 - transitivePeerDependencies: - - '@types/react' - dev: true - - /@storybook/addon-highlight@8.6.0(storybook@8.6.0): - resolution: {integrity: sha512-xgSUzNV8ZpNDJ35bLz7wfg/q7/O/C5845wbZsn+PobUOWpOMOKeZwbGkO3Fh4+CWKeFuMhUpFiZSWkNgT+bV0g==} - peerDependencies: - storybook: ^8.6.0 - dependencies: - '@storybook/global': 5.0.0 - storybook: 8.6.0(prettier@3.5.2) - dev: true - - /@storybook/addon-interactions@8.6.0(storybook@8.6.0): - resolution: {integrity: sha512-AgobFrUxmKgGHKeOismuLZdXlndP3ZDquHztiomfMiu01xOqNPxG0+xoCcegvftU7hKE+TlBRzhjCWg/bFz4LQ==} - peerDependencies: - storybook: ^8.6.0 - dependencies: - '@storybook/global': 5.0.0 - '@storybook/instrumenter': 8.6.0(storybook@8.6.0) - '@storybook/test': 8.6.0(storybook@8.6.0) - polished: 4.3.1 - storybook: 8.6.0(prettier@3.5.2) - ts-dedent: 2.2.0 - dev: true - - /@storybook/addon-measure@8.6.0(storybook@8.6.0): - resolution: {integrity: sha512-ogc+VNLyXq8hGfI4Fkmh9i3JbxfiIViiINdBoldD6K0gId3yaqqrSwQ2vWhYIbfUuz+PsMxsoNS7MD4EiRXhPg==} - peerDependencies: - storybook: ^8.6.0 - dependencies: - '@storybook/global': 5.0.0 - storybook: 8.6.0(prettier@3.5.2) - tiny-invariant: 1.3.3 - dev: true - - /@storybook/addon-onboarding@8.6.0(storybook@8.6.0): - resolution: {integrity: sha512-UOOvYQlHKdY31YJwR+ECX7e/BNuhp4alcJmx1fuI7m6tXqTGLtioeMJEreh2jV3FDaaDqL4DL/1UfYXqNRMp+A==} - peerDependencies: - storybook: ^8.6.0 - dependencies: - storybook: 8.6.0(prettier@3.5.2) - dev: true - - /@storybook/addon-outline@8.6.0(storybook@8.6.0): - resolution: {integrity: sha512-IWxrNNZOk5rtOdnLNyK7gpqi0g0rMGYl5ju37Glbfa7iG2J1VKofG2CbkUpVyVY0uiTXqkKkhOKlc9zELhLQSg==} - peerDependencies: - storybook: ^8.6.0 - dependencies: - '@storybook/global': 5.0.0 - storybook: 8.6.0(prettier@3.5.2) - ts-dedent: 2.2.0 - dev: true - - /@storybook/addon-themes@8.6.0(storybook@8.6.0): - resolution: {integrity: sha512-dKUjFgHP9Fd2fxrXcUUjev5X3cGqVvVogzh+nCRA/QlZgf3NzA8J2VPtqtFMX3ex+vaozGsrEKrk+LQTK1B7SA==} - peerDependencies: - storybook: ^8.6.0 - dependencies: - storybook: 8.6.0(prettier@3.5.2) - ts-dedent: 2.2.0 - dev: true - - /@storybook/addon-toolbars@8.6.0(storybook@8.6.0): - resolution: {integrity: sha512-Mg0V9RrnDt/IQe+zjggGS+4Otkv4YFqyjL/ygwnV6aKGgzL5zFpC+5rf8MeBEvNeol9+THC/XltN/Xf+jbtLEQ==} - peerDependencies: - storybook: ^8.6.0 - dependencies: - storybook: 8.6.0(prettier@3.5.2) - dev: true - - /@storybook/addon-viewport@8.6.0(storybook@8.6.0): - resolution: {integrity: sha512-sVDF3gIjGdvfBdCM/jEBu6Nd5GlqITKBtdySiQdNDl5JI4ak77ZM+T+VQmJxB4sIqvCjWzCbQqUC45nymVObug==} - peerDependencies: - storybook: ^8.6.0 - dependencies: - memoizerific: 1.11.3 - storybook: 8.6.0(prettier@3.5.2) - dev: true - - /@storybook/blocks@8.6.0(react-dom@18.3.1)(react@18.3.1)(storybook@8.6.0): - resolution: {integrity: sha512-3PNxlB5Ooj8CIhttbDxeV6kW7ui+2GEdTngtqhnsUHVjzeTKpilsk2lviOeUzqlyq5FDK+rhpZ3L3DJ9pDvioA==} - peerDependencies: - react: ^18.3.1 - react-dom: ^18.3.1 - storybook: ^8.6.0 - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true - dependencies: - '@storybook/icons': 1.3.2(react-dom@18.3.1)(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - storybook: 8.6.0(prettier@3.5.2) - ts-dedent: 2.2.0 - dev: true - - /@storybook/builder-vite@8.6.0(storybook@8.6.0)(vite@6.2.0): - resolution: {integrity: sha512-Bdc5fXLJbPdQo2eJ3dDNhfhWuQOME2KWvrixnsfo57IOhDa5B81jVcDBQIIgB9K1NQDRGyaWfYCcKZqy16yPlQ==} - peerDependencies: - storybook: ^8.6.0 - vite: ^4.0.0 || ^5.0.0 || ^6.0.0 - dependencies: - '@storybook/csf-plugin': 8.6.0(storybook@8.6.0) - browser-assert: 1.2.1 - storybook: 8.6.0(prettier@3.5.2) - ts-dedent: 2.2.0 - vite: 6.2.0(@types/node@22.13.9) - dev: true - - /@storybook/components@8.6.0(storybook@8.6.0): - resolution: {integrity: sha512-k5MQAuLPt5KOaa5J4QvX/WKucaiFTMJiEX5lsSaY6qON6Sx8PtnLQxVwWF7BIMW/jLpd94BUxrVjVrQKlwoLKQ==} - peerDependencies: - storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - dependencies: - storybook: 8.6.0(prettier@3.5.2) - dev: true - - /@storybook/core@8.6.0(prettier@3.5.2)(storybook@8.6.0): - resolution: {integrity: sha512-UzVo2sNz8PZuDp++/R9NBtjupSXhvxS8lptYcwtk51b6MvcM4AL/b4V3PGryOVJDru9sMwz+pzaj7Jg8RVFMJA==} - peerDependencies: - prettier: ^2 || ^3 - peerDependenciesMeta: - prettier: - optional: true - dependencies: - '@storybook/theming': 8.6.0(storybook@8.6.0) - better-opn: 3.0.2 - browser-assert: 1.2.1 - esbuild: 0.25.0 - esbuild-register: 3.6.0(esbuild@0.25.0) - jsdoc-type-pratt-parser: 4.1.0 - prettier: 3.5.2 - process: 0.11.10 - recast: 0.23.10 - semver: 7.7.1 - util: 0.12.5 - ws: 8.18.1 - transitivePeerDependencies: - - bufferutil - - storybook - - supports-color - - utf-8-validate - dev: true - - /@storybook/csf-plugin@8.6.0(storybook@8.6.0): - resolution: {integrity: sha512-Cd7LL4XQ8ccUDzJGxVyuzgsYuRYS5LVL4/ZNmAxikz9LtVrVFI/4RgFa5MvlQZc0twreoLtztFskziny8OiJUQ==} - peerDependencies: - storybook: ^8.6.0 - dependencies: - storybook: 8.6.0(prettier@3.5.2) - unplugin: 1.16.1 - dev: true - - /@storybook/csf@0.1.13: - resolution: {integrity: sha512-7xOOwCLGB3ebM87eemep89MYRFTko+D8qE7EdAAq74lgdqRR5cOUtYWJLjO2dLtP94nqoOdHJo6MdLLKzg412Q==} - dependencies: - type-fest: 2.19.0 - dev: true - - /@storybook/global@5.0.0: - resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} - dev: true - - /@storybook/icons@1.3.2(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-t3xcbCKkPvqyef8urBM0j/nP6sKtnlRkVgC+8JTbTAZQjaTmOjes3byEgzs89p4B/K6cJsg9wLW2k3SknLtYJw==} - engines: {node: '>=14.0.0'} - peerDependencies: - react: ^18.3.1 - react-dom: ^18.3.1 - dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: true - - /@storybook/instrumenter@8.6.0(storybook@8.6.0): - resolution: {integrity: sha512-eEY/Hfa3Vj5Nv4vHRHlSqjoyW6oAKNK3rKIXfL/eawQwb7rKhzijDLG5YBH44Hh7dEPIqUp0LEdgpyIY7GXezg==} - peerDependencies: - storybook: ^8.6.0 - dependencies: - '@storybook/global': 5.0.0 - '@vitest/utils': 2.1.9 - storybook: 8.6.0(prettier@3.5.2) - dev: true - - /@storybook/manager-api@8.6.0(storybook@8.6.0): - resolution: {integrity: sha512-ddBibYrO3yQCdtZBqQYqfABjR9eS+llvt2NFDajfyQw+zmuNIZfw4BDGKH1WHE4wm/kXmlFFhPHXaHgAPTRPfQ==} - peerDependencies: - storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - dependencies: - storybook: 8.6.0(prettier@3.5.2) - dev: true - - /@storybook/preview-api@8.6.0(storybook@8.6.0): - resolution: {integrity: sha512-c/sZj4Tb+DtdCndcjoFUneUwl2XUdQckqN4nf1SuIJ/BXRbAexlD04fvPJ4wzmwUNg8EQF7BhTWVNDa1RvJz2w==} - peerDependencies: - storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - dependencies: - storybook: 8.6.0(prettier@3.5.2) - dev: true - - /@storybook/react-dom-shim@8.6.0(react-dom@18.3.1)(react@18.3.1)(storybook@8.6.0): - resolution: {integrity: sha512-5Y+vMHhcx0xnaNsLQMbkmjc3zkDn/fGBNsiLH2e4POvW3ZQvOxjoyxAsEQaKwLtFgsdCFSd2tR89F6ItYrA2JQ==} - peerDependencies: - react: ^18.3.1 - react-dom: ^18.3.1 - storybook: ^8.6.0 - dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - storybook: 8.6.0(prettier@3.5.2) - dev: true - - /@storybook/react-vite@8.6.0(@storybook/test@8.6.0)(react-dom@18.3.1)(react@18.3.1)(storybook@8.6.0)(typescript@5.6.3)(vite@6.2.0): - resolution: {integrity: sha512-uDQyf2u0Zki4qIavOu2jyGIlvmUOsgCpHXY+7KHr542ZIkXI3mGOi5/rwUxgNSHGD3JloYGdlt+4Md+9i5xEbA==} - engines: {node: '>=18.0.0'} - peerDependencies: - '@storybook/test': 8.6.0 - react: ^18.3.1 - react-dom: ^18.3.1 - storybook: ^8.6.0 - vite: ^4.0.0 || ^5.0.0 || ^6.0.0 - peerDependenciesMeta: - '@storybook/test': - optional: true - dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.5.0(typescript@5.6.3)(vite@6.2.0) - '@rollup/pluginutils': 5.1.4 - '@storybook/builder-vite': 8.6.0(storybook@8.6.0)(vite@6.2.0) - '@storybook/react': 8.6.0(@storybook/test@8.6.0)(react-dom@18.3.1)(react@18.3.1)(storybook@8.6.0)(typescript@5.6.3) - '@storybook/test': 8.6.0(storybook@8.6.0) - find-up: 5.0.0 - magic-string: 0.30.17 - react: 18.3.1 - react-docgen: 7.1.1 - react-dom: 18.3.1(react@18.3.1) - resolve: 1.22.10 - storybook: 8.6.0(prettier@3.5.2) - tsconfig-paths: 4.2.0 - vite: 6.2.0(@types/node@22.13.9) - transitivePeerDependencies: - - rollup - - supports-color - - typescript - dev: true - - /@storybook/react@8.6.0(@storybook/test@8.6.0)(react-dom@18.3.1)(react@18.3.1)(storybook@8.6.0)(typescript@5.6.3): - resolution: {integrity: sha512-go0KhCFcOhxkTdaMCxeuJxmc/5CaiaO76+SBzIoPIMWEh9tZQmKb+vojdKcFZswHjZV3zA1uJFUzyympspRffA==} - engines: {node: '>=18.0.0'} - peerDependencies: - '@storybook/test': 8.6.0 - react: ^18.3.1 - react-dom: ^18.3.1 - storybook: ^8.6.0 - typescript: '>= 4.2.x' - peerDependenciesMeta: - '@storybook/test': - optional: true - typescript: - optional: true - dependencies: - '@storybook/components': 8.6.0(storybook@8.6.0) - '@storybook/global': 5.0.0 - '@storybook/manager-api': 8.6.0(storybook@8.6.0) - '@storybook/preview-api': 8.6.0(storybook@8.6.0) - '@storybook/react-dom-shim': 8.6.0(react-dom@18.3.1)(react@18.3.1)(storybook@8.6.0) - '@storybook/test': 8.6.0(storybook@8.6.0) - '@storybook/theming': 8.6.0(storybook@8.6.0) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - storybook: 8.6.0(prettier@3.5.2) - typescript: 5.6.3 - dev: true - - /@storybook/test@8.6.0(storybook@8.6.0): - resolution: {integrity: sha512-qiiv4wh4FoxTb1PoiA/noo9HcLDqfTwL0svMoNpTy2Bxm6xajgg5zYmmIT4pKyQkMd9pfKKQKILok31VxAj8vw==} - peerDependencies: - storybook: ^8.6.0 - dependencies: - '@storybook/global': 5.0.0 - '@storybook/instrumenter': 8.6.0(storybook@8.6.0) - '@testing-library/dom': 10.4.0 - '@testing-library/jest-dom': 6.5.0 - '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0) - '@vitest/expect': 2.0.5 - '@vitest/spy': 2.0.5 - storybook: 8.6.0(prettier@3.5.2) - dev: true - - /@storybook/theming@8.6.0(storybook@8.6.0): - resolution: {integrity: sha512-te7dl0ZXnEyKS4LW+QHc3MP7nurWhWhmClY7G2WKNW9eHenb9zvXkcAQt6c4OWhrJi01CAYoXmZNJP0jEzxRRQ==} - peerDependencies: - storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - dependencies: - storybook: 8.6.0(prettier@3.5.2) - dev: true - - /@swc/counter@0.1.3: - resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - dev: false - - /@swc/helpers@0.5.15: - resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - dependencies: - tslib: 2.8.1 - dev: false - - /@tanstack/query-core@5.67.1: - resolution: {integrity: sha512-AkFmuukVejyqVIjEQoFhLb3q+xHl7JG8G9cANWTMe3s8iKzD9j1VBSYXgCjy6vm6xM8cUCR9zP2yqWxY9pTWOA==} - dev: false - - /@tanstack/query-devtools@5.74.7: - resolution: {integrity: sha512-nSNlfuGdnHf4yB0S+BoNYOE1o3oAH093weAYZolIHfS2stulyA/gWfSk/9H4ZFk5mAAHb5vNqAeJOmbdcGPEQw==} - dev: false - - /@tanstack/react-query-devtools@5.75.5(@tanstack/react-query@5.67.1)(react@18.3.1): - resolution: {integrity: sha512-S31U00nJOQIbxydRH1kOwdLRaLBrda8O5QjzmgkRg60UZzPGdbI6+873Qa0YGUfPeILDbR2ukgWyg7CJQPy4iA==} - peerDependencies: - '@tanstack/react-query': ^5.75.5 - react: ^18.3.1 - dependencies: - '@tanstack/query-devtools': 5.74.7 - '@tanstack/react-query': 5.67.1(react@18.3.1) - react: 18.3.1 - dev: false - - /@tanstack/react-query@5.67.1(react@18.3.1): - resolution: {integrity: sha512-fH5u4JLwB6A+wLFdi8wWBWAYoJV5deYif2OveJ26ktAWjU499uvVFS1wPWnyEyq5LvZX1MZInvv9QRaIZANRaQ==} - peerDependencies: - react: ^18.3.1 - dependencies: - '@tanstack/query-core': 5.67.1 - react: 18.3.1 - dev: false - - /@tanstack/react-table@8.21.2(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-11tNlEDTdIhMJba2RBH+ecJ9l1zgS2kjmexDPAraulc8jeNA4xocSNeyzextT0XJyASil4XsCYlJmf5jEWAtYg==} - engines: {node: '>=12'} - peerDependencies: - react: ^18.3.1 - react-dom: ^18.3.1 - dependencies: - '@tanstack/table-core': 8.21.2 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@tanstack/table-core@8.21.2: - resolution: {integrity: sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==} - engines: {node: '>=12'} - dev: false - - /@testing-library/dom@10.4.0: - resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} - engines: {node: '>=18'} - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/runtime': 7.26.9 - '@types/aria-query': 5.0.4 - aria-query: 5.3.0 - chalk: 4.1.2 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 - pretty-format: 27.5.1 - dev: true - - /@testing-library/jest-dom@6.5.0: - resolution: {integrity: sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==} - engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - dependencies: - '@adobe/css-tools': 4.4.2 - aria-query: 5.3.2 - chalk: 3.0.0 - css.escape: 1.5.1 - dom-accessibility-api: 0.6.3 - lodash: 4.17.21 - redent: 3.0.0 - dev: true - - /@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0): - resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==} - engines: {node: '>=12', npm: '>=6'} - peerDependencies: - '@testing-library/dom': '>=7.21.4' - dependencies: - '@testing-library/dom': 10.4.0 - dev: true - - /@tinymce/tinymce-react@6.1.0(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-K0MP3yYVKe8+etUwsg6zyRq+q9TGLaVf005WiBHiB8JZEomAwbBPERGunhU9uOqNQ5gJs8yVOPZ68Xcd1UHclA==} - peerDependencies: - react: ^18.3.1 - react-dom: ^18.3.1 - tinymce: ^7.0.0 || ^6.0.0 || ^5.5.1 - peerDependenciesMeta: - tinymce: - optional: true - dependencies: - prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@types/aria-query@5.0.4: - resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} - dev: true - - /@types/babel__core@7.20.5: - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - dependencies: - '@babel/parser': 7.26.9 - '@babel/types': 7.26.9 - '@types/babel__generator': 7.6.8 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.20.6 - dev: true - - /@types/babel__generator@7.6.8: - resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} - dependencies: - '@babel/types': 7.26.9 - dev: true - - /@types/babel__template@7.4.4: - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - dependencies: - '@babel/parser': 7.26.9 - '@babel/types': 7.26.9 - dev: true - - /@types/babel__traverse@7.20.6: - resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} - dependencies: - '@babel/types': 7.26.9 - dev: true - - /@types/d3-array@3.2.1: - resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} - dev: false - - /@types/d3-color@3.1.3: - resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} - dev: false - - /@types/d3-ease@3.0.2: - resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} - dev: false - - /@types/d3-interpolate@3.0.4: - resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} - dependencies: - '@types/d3-color': 3.1.3 - dev: false - - /@types/d3-path@3.1.1: - resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} - dev: false - - /@types/d3-scale@4.0.9: - resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} - dependencies: - '@types/d3-time': 3.0.4 - dev: false - - /@types/d3-shape@3.1.7: - resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} - dependencies: - '@types/d3-path': 3.1.1 - dev: false - - /@types/d3-time@3.0.4: - resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} - dev: false - - /@types/d3-timer@3.0.2: - resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} - dev: false - - /@types/doctrine@0.0.9: - resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} - dev: true - - /@types/eslint-scope@3.7.7: - resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} - dependencies: - '@types/eslint': 9.6.1 - '@types/estree': 1.0.6 - dev: true - - /@types/eslint@9.6.1: - resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} - dependencies: - '@types/estree': 1.0.6 - '@types/json-schema': 7.0.15 - dev: true - - /@types/estree@1.0.6: - resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - dev: true - - /@types/js-cookie@3.0.6: - resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} - - /@types/json-schema@7.0.15: - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - dev: true - - /@types/json5@0.0.29: - resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - dev: true - - /@types/mdx@2.0.13: - resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} - dev: true - - /@types/node@20.17.19: - resolution: {integrity: sha512-LEwC7o1ifqg/6r2gn9Dns0f1rhK+fPFDoMiceTJ6kWmVk6bgXBI/9IOWfVan4WiAavK9pIVWdX0/e3J+eEUh5A==} - dependencies: - undici-types: 6.19.8 - dev: true - - /@types/node@22.13.5: - resolution: {integrity: sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==} - dependencies: - undici-types: 6.20.0 - - /@types/node@22.13.9: - resolution: {integrity: sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==} - dependencies: - undici-types: 6.20.0 - - /@types/prop-types@15.7.14: - resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} - - /@types/react-dom@18.3.5(@types/react@18.3.18): - resolution: {integrity: sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==} - peerDependencies: - '@types/react': ^18.3.18 - dependencies: - '@types/react': 18.3.18 - - /@types/react@18.3.18: - resolution: {integrity: sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==} - dependencies: - '@types/prop-types': 15.7.14 - csstype: 3.1.3 - - /@types/resolve@1.20.6: - resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} - dev: true - - /@types/uuid@9.0.8: - resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} - dev: true - - /@types/webpack@5.28.5: - resolution: {integrity: sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==} - dependencies: - '@types/node': 22.13.9 - tapable: 2.2.1 - webpack: 5.98.0 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - - webpack-cli - dev: true - - /@typescript-eslint/eslint-plugin@8.25.0(@typescript-eslint/parser@8.25.0)(eslint@9.21.0)(typescript@5.6.3): - resolution: {integrity: sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' - dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.25.0(eslint@9.21.0)(typescript@5.6.3) - '@typescript-eslint/scope-manager': 8.25.0 - '@typescript-eslint/type-utils': 8.25.0(eslint@9.21.0)(typescript@5.6.3) - '@typescript-eslint/utils': 8.25.0(eslint@9.21.0)(typescript@5.6.3) - '@typescript-eslint/visitor-keys': 8.25.0 - eslint: 9.21.0 - graphemer: 1.4.0 - ignore: 5.3.2 - natural-compare: 1.4.0 - ts-api-utils: 2.0.1(typescript@5.6.3) - typescript: 5.6.3 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/parser@8.25.0(eslint@9.21.0)(typescript@5.6.3): - resolution: {integrity: sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' - dependencies: - '@typescript-eslint/scope-manager': 8.25.0 - '@typescript-eslint/types': 8.25.0 - '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.6.3) - '@typescript-eslint/visitor-keys': 8.25.0 - debug: 4.4.0 - eslint: 9.21.0 - typescript: 5.6.3 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/scope-manager@8.25.0: - resolution: {integrity: sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dependencies: - '@typescript-eslint/types': 8.25.0 - '@typescript-eslint/visitor-keys': 8.25.0 - dev: true - - /@typescript-eslint/type-utils@8.25.0(eslint@9.21.0)(typescript@5.6.3): - resolution: {integrity: sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' - dependencies: - '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.6.3) - '@typescript-eslint/utils': 8.25.0(eslint@9.21.0)(typescript@5.6.3) - debug: 4.4.0 - eslint: 9.21.0 - ts-api-utils: 2.0.1(typescript@5.6.3) - typescript: 5.6.3 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/types@8.25.0: - resolution: {integrity: sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dev: true - - /@typescript-eslint/typescript-estree@8.25.0(typescript@5.6.3): - resolution: {integrity: sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.8.0' - dependencies: - '@typescript-eslint/types': 8.25.0 - '@typescript-eslint/visitor-keys': 8.25.0 - debug: 4.4.0 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.1 - ts-api-utils: 2.0.1(typescript@5.6.3) - typescript: 5.6.3 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/typescript-estree@8.25.0(typescript@5.7.3): - resolution: {integrity: sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.8.0' - dependencies: - '@typescript-eslint/types': 8.25.0 - '@typescript-eslint/visitor-keys': 8.25.0 - debug: 4.4.0 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.1 - ts-api-utils: 2.0.1(typescript@5.7.3) - typescript: 5.7.3 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/utils@8.25.0(eslint@9.21.0)(typescript@5.6.3): - resolution: {integrity: sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' - dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0) - '@typescript-eslint/scope-manager': 8.25.0 - '@typescript-eslint/types': 8.25.0 - '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.6.3) - eslint: 9.21.0 - typescript: 5.6.3 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/visitor-keys@8.25.0: - resolution: {integrity: sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dependencies: - '@typescript-eslint/types': 8.25.0 - eslint-visitor-keys: 4.2.0 - dev: true - - /@vitejs/plugin-react@4.3.4(vite@6.2.0): - resolution: {integrity: sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 - dependencies: - '@babel/core': 7.26.9 - '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.9) - '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.9) - '@types/babel__core': 7.20.5 - react-refresh: 0.14.2 - vite: 6.2.0(@types/node@22.13.9) - transitivePeerDependencies: - - supports-color - dev: true - - /@vitest/expect@2.0.5: - resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} - dependencies: - '@vitest/spy': 2.0.5 - '@vitest/utils': 2.0.5 - chai: 5.2.0 - tinyrainbow: 1.2.0 - dev: true - - /@vitest/pretty-format@2.0.5: - resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==} - dependencies: - tinyrainbow: 1.2.0 - dev: true - - /@vitest/pretty-format@2.1.9: - resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} - dependencies: - tinyrainbow: 1.2.0 - dev: true - - /@vitest/spy@2.0.5: - resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} - dependencies: - tinyspy: 3.0.2 - dev: true - - /@vitest/utils@2.0.5: - resolution: {integrity: sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==} - dependencies: - '@vitest/pretty-format': 2.0.5 - estree-walker: 3.0.3 - loupe: 3.1.3 - tinyrainbow: 1.2.0 - dev: true - - /@vitest/utils@2.1.9: - resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} - dependencies: - '@vitest/pretty-format': 2.1.9 - loupe: 3.1.3 - tinyrainbow: 1.2.0 - dev: true - - /@vue/compiler-core@3.5.13: - resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} - dependencies: - '@babel/parser': 7.26.9 - '@vue/shared': 3.5.13 - entities: 4.5.0 - estree-walker: 2.0.2 - source-map-js: 1.2.1 - dev: true - - /@vue/compiler-dom@3.5.13: - resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==} - dependencies: - '@vue/compiler-core': 3.5.13 - '@vue/shared': 3.5.13 - dev: true - - /@vue/compiler-sfc@3.5.13: - resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==} - dependencies: - '@babel/parser': 7.26.9 - '@vue/compiler-core': 3.5.13 - '@vue/compiler-dom': 3.5.13 - '@vue/compiler-ssr': 3.5.13 - '@vue/shared': 3.5.13 - estree-walker: 2.0.2 - magic-string: 0.30.17 - postcss: 8.5.3 - source-map-js: 1.2.1 - dev: true - - /@vue/compiler-ssr@3.5.13: - resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==} - dependencies: - '@vue/compiler-dom': 3.5.13 - '@vue/shared': 3.5.13 - dev: true - - /@vue/shared@3.5.13: - resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} - dev: true - - /@webassemblyjs/ast@1.14.1: - resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} - dependencies: - '@webassemblyjs/helper-numbers': 1.13.2 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - dev: true - - /@webassemblyjs/floating-point-hex-parser@1.13.2: - resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} - dev: true - - /@webassemblyjs/helper-api-error@1.13.2: - resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} - dev: true - - /@webassemblyjs/helper-buffer@1.14.1: - resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} - dev: true - - /@webassemblyjs/helper-numbers@1.13.2: - resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} - dependencies: - '@webassemblyjs/floating-point-hex-parser': 1.13.2 - '@webassemblyjs/helper-api-error': 1.13.2 - '@xtuc/long': 4.2.2 - dev: true - - /@webassemblyjs/helper-wasm-bytecode@1.13.2: - resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} - dev: true - - /@webassemblyjs/helper-wasm-section@1.14.1: - resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/wasm-gen': 1.14.1 - dev: true - - /@webassemblyjs/ieee754@1.13.2: - resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} - dependencies: - '@xtuc/ieee754': 1.2.0 - dev: true - - /@webassemblyjs/leb128@1.13.2: - resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} - dependencies: - '@xtuc/long': 4.2.2 - dev: true - - /@webassemblyjs/utf8@1.13.2: - resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} - dev: true - - /@webassemblyjs/wasm-edit@1.14.1: - resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/helper-wasm-section': 1.14.1 - '@webassemblyjs/wasm-gen': 1.14.1 - '@webassemblyjs/wasm-opt': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - '@webassemblyjs/wast-printer': 1.14.1 - dev: true - - /@webassemblyjs/wasm-gen@1.14.1: - resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/ieee754': 1.13.2 - '@webassemblyjs/leb128': 1.13.2 - '@webassemblyjs/utf8': 1.13.2 - dev: true - - /@webassemblyjs/wasm-opt@1.14.1: - resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/wasm-gen': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - dev: true - - /@webassemblyjs/wasm-parser@1.14.1: - resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-api-error': 1.13.2 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/ieee754': 1.13.2 - '@webassemblyjs/leb128': 1.13.2 - '@webassemblyjs/utf8': 1.13.2 - dev: true - - /@webassemblyjs/wast-printer@1.14.1: - resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@xtuc/long': 4.2.2 - dev: true - - /@xlsx/xlsx-populate@0.0.8: - resolution: {integrity: sha512-6yu24ePaO3cnAuti77FhVPZvKkzUt6s6x3zJ9wiuBSjR0ImWu7l/SgePelZaf8Oty4SP4rg4vznQVCGQtfzu2A==} - engines: {node: '>=16'} - dev: true - - /@xtuc/ieee754@1.2.0: - resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} - dev: true - - /@xtuc/long@4.2.2: - resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - dev: true - - /acorn-jsx@5.3.2(acorn@8.14.0): - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - acorn: 8.14.0 - dev: true - - /acorn@8.14.0: - resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: true - - /adler-32@1.3.1: - resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} - engines: {node: '>=0.8'} - dev: false - - /ajv-formats@2.1.1(ajv@8.17.1): - resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - dependencies: - ajv: 8.17.1 - dev: true - - /ajv-keywords@5.1.0(ajv@8.17.1): - resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} - peerDependencies: - ajv: ^8.8.2 - dependencies: - ajv: 8.17.1 - fast-deep-equal: 3.1.3 - dev: true - - /ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - dev: true - - /ajv@8.17.1: - resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.0.6 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - dev: true - - /ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - /ansi-regex@6.1.0: - resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} - engines: {node: '>=12'} - - /ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - dependencies: - color-convert: 2.0.1 - - /ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - dev: true - - /ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} - engines: {node: '>=12'} - - /any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - - /anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - - /arg@5.0.2: - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - - /argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: true - - /aria-hidden@1.2.4: - resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} - engines: {node: '>=10'} - dependencies: - tslib: 2.8.1 - dev: false - - /aria-query@5.3.0: - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - dependencies: - dequal: 2.0.3 - dev: true - - /aria-query@5.3.2: - resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} - engines: {node: '>= 0.4'} - dev: true - - /array-buffer-byte-length@1.0.2: - resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.3 - is-array-buffer: 3.0.5 - dev: true - - /array-includes@3.1.8: - resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - is-string: 1.1.1 - dev: true - - /array.prototype.findlast@1.2.5: - resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-shim-unscopables: 1.1.0 - dev: true - - /array.prototype.findlastindex@1.2.5: - resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-shim-unscopables: 1.1.0 - dev: true - - /array.prototype.flat@1.3.3: - resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-shim-unscopables: 1.1.0 - dev: true - - /array.prototype.flatmap@1.3.3: - resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-shim-unscopables: 1.1.0 - dev: true - - /array.prototype.tosorted@1.1.4: - resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-errors: 1.3.0 - es-shim-unscopables: 1.1.0 - dev: true - - /arraybuffer.prototype.slice@1.0.4: - resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} - engines: {node: '>= 0.4'} - dependencies: - array-buffer-byte-length: 1.0.2 - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - is-array-buffer: 3.0.5 - dev: true - - /assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - dev: true - - /ast-module-types@6.0.1: - resolution: {integrity: sha512-WHw67kLXYbZuHTmcdbIrVArCq5wxo6NEuj3hiYAWr8mwJeC+C2mMCIBIWCiDoCye/OF/xelc+teJ1ERoWmnEIA==} - engines: {node: '>=18'} - dev: true - - /ast-types-flow@0.0.8: - resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} - dev: true - - /ast-types@0.16.1: - resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} - engines: {node: '>=4'} - dependencies: - tslib: 2.8.1 - dev: true - - /async-function@1.0.0: - resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} - engines: {node: '>= 0.4'} - dev: true - - /async-wave@1.8.6: - resolution: {integrity: sha512-wqWvr0Mrc1TrY92hN9MV0Xebxa6kt+T7QtjZ7FEMv+RUBa/Hfpz/UOgz0rB+7uqVK8TuBSXykOyU/k9gogdLkw==} - dependencies: - is-promise: 4.0.0 - lodash.add: 3.7.0 - lodash.clonedeep: 4.5.0 - lodash.isequal: 4.5.0 - lodash.isfunction: 3.0.9 - lodash.size: 4.2.0 - dev: false - - /asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - dev: false - - /attr-accept@2.2.5: - resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} - engines: {node: '>=4'} - dev: false - - /autoprefixer@10.4.20(postcss@8.5.3): - resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 - dependencies: - browserslist: 4.24.4 - caniuse-lite: 1.0.30001701 - fraction.js: 4.3.7 - normalize-range: 0.1.2 - picocolors: 1.1.1 - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - dev: true - - /available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} - dependencies: - possible-typed-array-names: 1.1.0 - dev: true - - /axe-core@4.10.2: - resolution: {integrity: sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==} - engines: {node: '>=4'} - dev: true - - /axios@1.8.1: - resolution: {integrity: sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==} - dependencies: - follow-redirects: 1.15.9 - form-data: 4.0.2 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - dev: false - - /axobject-query@4.1.0: - resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} - engines: {node: '>= 0.4'} - dev: true - - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - /better-opn@3.0.2: - resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} - engines: {node: '>=12.0.0'} - dependencies: - open: 8.4.2 - dev: true - - /binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - - /brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - dev: true - - /brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - dependencies: - balanced-match: 1.0.2 - - /braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - dependencies: - fill-range: 7.1.1 - - /browser-assert@1.2.1: - resolution: {integrity: sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==} - dev: true - - /browserslist@4.24.4: - resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - dependencies: - caniuse-lite: 1.0.30001701 - electron-to-chromium: 1.5.107 - node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.24.4) - dev: true - - /buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - - /bundle-require@5.1.0(esbuild@0.24.2): - resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - peerDependencies: - esbuild: '>=0.18' - dependencies: - esbuild: 0.24.2 - load-tsconfig: 0.2.5 - dev: true - - /busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} - dependencies: - streamsearch: 1.1.0 - dev: false - - /cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - dev: true - - /call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - /call-bind@1.0.8: - resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} - engines: {node: '>= 0.4'} - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - get-intrinsic: 1.3.0 - set-function-length: 1.2.2 - dev: true - - /call-bound@1.0.3: - resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - dev: true - - /callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - dev: true - - /camelcase-css@2.0.1: - resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} - engines: {node: '>= 6'} - - /camelcase@5.3.1: - resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} - engines: {node: '>=6'} - dev: false - - /caniuse-lite@1.0.30001701: - resolution: {integrity: sha512-faRs/AW3jA9nTwmJBSO1PQ6L/EOgsB5HMQQq4iCu5zhPgVVgO/pZRHlmatwijZKetFw8/Pr4q6dEN8sJuq8qTw==} - - /cfb@1.2.2: - resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} - engines: {node: '>=0.8'} - dependencies: - adler-32: 1.3.1 - crc-32: 1.2.2 - dev: false - - /chai@5.2.0: - resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} - engines: {node: '>=12'} - dependencies: - assertion-error: 2.0.1 - check-error: 2.1.1 - deep-eql: 5.0.2 - loupe: 3.1.3 - pathval: 2.0.0 - dev: true - - /chalk@3.0.0: - resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} - engines: {node: '>=8'} - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - dev: true - - /chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - dev: true - - /chart.js@4.4.8: - resolution: {integrity: sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==} - engines: {pnpm: '>=8'} - dependencies: - '@kurkle/color': 0.3.4 - dev: false - - /check-error@2.1.1: - resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} - engines: {node: '>= 16'} - dev: true - - /chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - - /chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} - dependencies: - readdirp: 4.1.2 - dev: true - - /chrome-trace-event@1.0.4: - resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} - engines: {node: '>=6.0'} - dev: true - - /class-variance-authority@0.7.1: - resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} - dependencies: - clsx: 2.1.1 - dev: false - - /client-only@0.0.1: - resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - dev: false - - /cliui@6.0.0: - resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 6.2.0 - dev: false - - /cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - dev: true - - /clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} - dev: false - - /codepage@1.15.0: - resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} - engines: {node: '>=0.8'} - dev: false - - /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - dependencies: - color-name: 1.1.4 - - /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - /color-string@1.9.1: - resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} - requiresBuild: true - dependencies: - color-name: 1.1.4 - simple-swizzle: 0.2.2 - dev: false - optional: true - - /color@4.2.3: - resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} - engines: {node: '>=12.5.0'} - requiresBuild: true - dependencies: - color-convert: 2.0.1 - color-string: 1.9.1 - dev: false - optional: true - - /combine-errors@3.0.3: - resolution: {integrity: sha512-C8ikRNRMygCwaTx+Ek3Yr+OuZzgZjduCOfSQBjbM8V3MfgcjSTeto/GXP6PAwKvJz/v15b7GHZvx5rOlczFw/Q==} - dependencies: - custom-error-instance: 2.1.1 - lodash.uniqby: 4.5.0 - dev: false - - /combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - dependencies: - delayed-stream: 1.0.0 - dev: false - - /commander@12.1.0: - resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} - engines: {node: '>=18'} - dev: true - - /commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - dev: true - - /commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - - /concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: true - - /consola@3.4.0: - resolution: {integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==} - engines: {node: ^14.18.0 || >=16.10.0} - dev: true - - /convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - dev: true - - /core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - dev: false - - /cosmiconfig@9.0.0(typescript@5.6.3): - resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} - engines: {node: '>=14'} - peerDependencies: - typescript: '>=4.9.5' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - env-paths: 2.2.1 - import-fresh: 3.3.1 - js-yaml: 4.1.0 - parse-json: 5.2.0 - typescript: 5.6.3 - dev: true - - /crc-32@1.2.2: - resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} - engines: {node: '>=0.8'} - hasBin: true - dev: false - - /cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - /css.escape@1.5.1: - resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} - dev: true - - /csscolorparser@1.0.3: - resolution: {integrity: sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==} - dev: false - - /cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true - - /csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - - /custom-error-instance@2.1.1: - resolution: {integrity: sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg==} - dev: false - - /d3-array@3.2.4: - resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} - engines: {node: '>=12'} - dependencies: - internmap: 2.0.3 - dev: false - - /d3-color@3.1.0: - resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} - engines: {node: '>=12'} - dev: false - - /d3-ease@3.0.1: - resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} - engines: {node: '>=12'} - dev: false - - /d3-format@3.1.0: - resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} - engines: {node: '>=12'} - dev: false - - /d3-interpolate@3.0.1: - resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} - engines: {node: '>=12'} - dependencies: - d3-color: 3.1.0 - dev: false - - /d3-path@3.1.0: - resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} - engines: {node: '>=12'} - dev: false - - /d3-scale@4.0.2: - resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} - engines: {node: '>=12'} - dependencies: - d3-array: 3.2.4 - d3-format: 3.1.0 - d3-interpolate: 3.0.1 - d3-time: 3.1.0 - d3-time-format: 4.1.0 - dev: false - - /d3-shape@3.2.0: - resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} - engines: {node: '>=12'} - dependencies: - d3-path: 3.1.0 - dev: false - - /d3-time-format@4.1.0: - resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} - engines: {node: '>=12'} - dependencies: - d3-time: 3.1.0 - dev: false - - /d3-time@3.1.0: - resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} - engines: {node: '>=12'} - dependencies: - d3-array: 3.2.4 - dev: false - - /d3-timer@3.0.1: - resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} - engines: {node: '>=12'} - dev: false - - /damerau-levenshtein@1.0.8: - resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} - dev: true - - /data-view-buffer@1.0.2: - resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.3 - es-errors: 1.3.0 - is-data-view: 1.0.2 - dev: true - - /data-view-byte-length@1.0.2: - resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.3 - es-errors: 1.3.0 - is-data-view: 1.0.2 - dev: true - - /data-view-byte-offset@1.0.1: - resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.3 - es-errors: 1.3.0 - is-data-view: 1.0.2 - dev: true - - /date-fns-jalali@4.1.0-0: - resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==} - dev: false - - /date-fns@3.6.0: - resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} - dev: false - - /date-fns@4.1.0: - resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} - dev: false - - /dayjs@1.11.13: - resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} - dev: false - - /debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.3 - dev: true - - /debug@4.4.0: - resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.3 - dev: true - - /decamelize@1.2.0: - resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} - engines: {node: '>=0.10.0'} - dev: false - - /decimal.js-light@2.5.1: - resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} - dev: false - - /deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} - dev: true - - /deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - dev: true - - /define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - dev: true - - /define-lazy-prop@2.0.0: - resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} - engines: {node: '>=8'} - dev: true - - /define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} - dependencies: - define-data-property: 1.1.4 - has-property-descriptors: 1.0.2 - object-keys: 1.1.1 - dev: true - - /delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - dev: false - - /dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - dev: true - - /detect-libc@2.0.3: - resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} - engines: {node: '>=8'} - requiresBuild: true - dev: false - optional: true - - /detect-node-es@1.1.0: - resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} - dev: false - - /detective-amd@6.0.1: - resolution: {integrity: sha512-TtyZ3OhwUoEEIhTFoc1C9IyJIud3y+xYkSRjmvCt65+ycQuc3VcBrPRTMWoO/AnuCyOB8T5gky+xf7Igxtjd3g==} - engines: {node: '>=18'} - hasBin: true - dependencies: - ast-module-types: 6.0.1 - escodegen: 2.1.0 - get-amd-module-type: 6.0.1 - node-source-walk: 7.0.1 - dev: true - - /detective-cjs@6.0.1: - resolution: {integrity: sha512-tLTQsWvd2WMcmn/60T2inEJNhJoi7a//PQ7DwRKEj1yEeiQs4mrONgsUtEJKnZmrGWBBmE0kJ1vqOG/NAxwaJw==} - engines: {node: '>=18'} - dependencies: - ast-module-types: 6.0.1 - node-source-walk: 7.0.1 - dev: true - - /detective-es6@5.0.1: - resolution: {integrity: sha512-XusTPuewnSUdoxRSx8OOI6xIA/uld/wMQwYsouvFN2LAg7HgP06NF1lHRV3x6BZxyL2Kkoih4ewcq8hcbGtwew==} - engines: {node: '>=18'} - dependencies: - node-source-walk: 7.0.1 - dev: true - - /detective-postcss@7.0.1(postcss@8.5.3): - resolution: {integrity: sha512-bEOVpHU9picRZux5XnwGsmCN4+8oZo7vSW0O0/Enq/TO5R2pIAP2279NsszpJR7ocnQt4WXU0+nnh/0JuK4KHQ==} - engines: {node: ^14.0.0 || >=16.0.0} - peerDependencies: - postcss: ^8.4.47 - dependencies: - is-url: 1.2.4 - postcss: 8.5.3 - postcss-values-parser: 6.0.2(postcss@8.5.3) - dev: true - - /detective-sass@6.0.1: - resolution: {integrity: sha512-jSGPO8QDy7K7pztUmGC6aiHkexBQT4GIH+mBAL9ZyBmnUIOFbkfZnO8wPRRJFP/QP83irObgsZHCoDHZ173tRw==} - engines: {node: '>=18'} - dependencies: - gonzales-pe: 4.3.0 - node-source-walk: 7.0.1 - dev: true - - /detective-scss@5.0.1: - resolution: {integrity: sha512-MAyPYRgS6DCiS6n6AoSBJXLGVOydsr9huwXORUlJ37K3YLyiN0vYHpzs3AdJOgHobBfispokoqrEon9rbmKacg==} - engines: {node: '>=18'} - dependencies: - gonzales-pe: 4.3.0 - node-source-walk: 7.0.1 - dev: true - - /detective-stylus@5.0.1: - resolution: {integrity: sha512-Dgn0bUqdGbE3oZJ+WCKf8Dmu7VWLcmRJGc6RCzBgG31DLIyai9WAoEhYRgIHpt/BCRMrnXLbGWGPQuBUrnF0TA==} - engines: {node: '>=18'} - dev: true - - /detective-typescript@14.0.0(typescript@5.7.3): - resolution: {integrity: sha512-pgN43/80MmWVSEi5LUuiVvO/0a9ss5V7fwVfrJ4QzAQRd3cwqU1SfWGXJFcNKUqoD5cS+uIovhw5t/0rSeC5Mw==} - engines: {node: '>=18'} - peerDependencies: - typescript: ^5.4.4 - dependencies: - '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.7.3) - ast-module-types: 6.0.1 - node-source-walk: 7.0.1 - typescript: 5.7.3 - transitivePeerDependencies: - - supports-color - dev: true - - /detective-vue2@2.2.0(typescript@5.7.3): - resolution: {integrity: sha512-sVg/t6O2z1zna8a/UIV6xL5KUa2cMTQbdTIIvqNM0NIPswp52fe43Nwmbahzj3ww4D844u/vC2PYfiGLvD3zFA==} - engines: {node: '>=18'} - peerDependencies: - typescript: ^5.4.4 - dependencies: - '@dependents/detective-less': 5.0.1 - '@vue/compiler-sfc': 3.5.13 - detective-es6: 5.0.1 - detective-sass: 6.0.1 - detective-scss: 5.0.1 - detective-stylus: 5.0.1 - detective-typescript: 14.0.0(typescript@5.7.3) - typescript: 5.7.3 - transitivePeerDependencies: - - supports-color - dev: true - - /didyoumean@1.2.2: - resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} - - /dijkstrajs@1.0.3: - resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} - dev: false - - /dlv@1.1.3: - resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - - /dnd-core@16.0.1: - resolution: {integrity: sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==} - dependencies: - '@react-dnd/asap': 5.0.2 - '@react-dnd/invariant': 4.0.2 - redux: 4.2.1 - dev: false - - /dnd-multi-backend@8.1.2(dnd-core@16.0.1): - resolution: {integrity: sha512-KPDVEsiM+6gNEegqZYTWJQgJxYV4vB91tUrvoKJjaS0wwWqT/jNU0P7xJAwCue/cbasJNvk2dFZH7tC+bjX1Rg==} - peerDependencies: - dnd-core: ^16.0.1 - dependencies: - dnd-core: 16.0.1 - dev: false - - /doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} - dependencies: - esutils: 2.0.3 - dev: true - - /doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - dependencies: - esutils: 2.0.3 - dev: true - - /dom-accessibility-api@0.5.16: - resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} - dev: true - - /dom-accessibility-api@0.6.3: - resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} - dev: true - - /dom-helpers@5.2.1: - resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} - dependencies: - '@babel/runtime': 7.26.9 - csstype: 3.1.3 - dev: false - - /dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - - /earcut@2.2.4: - resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==} - dev: false - - /eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - - /effector@23.3.0: - resolution: {integrity: sha512-ZnQ3POaNARlxT9+kxrK58PO/xmStBdxfPq0rceglENg8Ryxx/yx+1RsV/ziznrFPhLkZYc7NdDA1OKxnMW98/g==} - engines: {node: '>=11.0.0'} - dev: true - - /electron-to-chromium@1.5.107: - resolution: {integrity: sha512-dJr1o6yCntRkXElnhsHh1bAV19bo/hKyFf7tCcWgpXbuFIF0Lakjgqv5LRfSDaNzAII8Fnxg2tqgHkgCvxdbxw==} - dev: true - - /embla-carousel-react@8.6.0(react@18.3.1): - resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==} - peerDependencies: - react: ^18.3.1 - dependencies: - embla-carousel: 8.6.0 - embla-carousel-reactive-utils: 8.6.0(embla-carousel@8.6.0) - react: 18.3.1 - dev: false - - /embla-carousel-reactive-utils@8.6.0(embla-carousel@8.6.0): - resolution: {integrity: sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==} - peerDependencies: - embla-carousel: 8.6.0 - dependencies: - embla-carousel: 8.6.0 - dev: false - - /embla-carousel@8.6.0: - resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==} - dev: false - - /emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - /emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - - /empathic@1.0.0: - resolution: {integrity: sha512-qtKgI1Mv8rTacvpaTkh28HM2Lbf+IOjXb7rhpt/42kZxRm8TBb/IVlo5iL2ztT19kc/EHAFN0fZ641avlXAgdg==} - engines: {node: '>=16'} - dev: true - - /enhanced-resolve@5.18.1: - resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} - engines: {node: '>=10.13.0'} - dependencies: - graceful-fs: 4.2.11 - tapable: 2.2.1 - dev: true - - /entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - dev: true - - /env-paths@2.2.1: - resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} - engines: {node: '>=6'} - dev: true - - /error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - dependencies: - is-arrayish: 0.2.1 - dev: true - - /es-abstract@1.23.9: - resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} - engines: {node: '>= 0.4'} - dependencies: - array-buffer-byte-length: 1.0.2 - arraybuffer.prototype.slice: 1.0.4 - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - call-bound: 1.0.3 - data-view-buffer: 1.0.2 - data-view-byte-length: 1.0.2 - data-view-byte-offset: 1.0.1 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.0 - function.prototype.name: 1.1.8 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - get-symbol-description: 1.1.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - internal-slot: 1.1.0 - is-array-buffer: 3.0.5 - is-callable: 1.2.7 - is-data-view: 1.0.2 - is-regex: 1.2.1 - is-shared-array-buffer: 1.0.4 - is-string: 1.1.1 - is-typed-array: 1.1.15 - is-weakref: 1.1.1 - math-intrinsics: 1.1.0 - object-inspect: 1.13.4 - object-keys: 1.1.1 - object.assign: 4.1.7 - own-keys: 1.0.1 - regexp.prototype.flags: 1.5.4 - safe-array-concat: 1.1.3 - safe-push-apply: 1.0.0 - safe-regex-test: 1.1.0 - set-proto: 1.0.0 - string.prototype.trim: 1.2.10 - string.prototype.trimend: 1.0.9 - string.prototype.trimstart: 1.0.8 - typed-array-buffer: 1.0.3 - typed-array-byte-length: 1.0.3 - typed-array-byte-offset: 1.0.4 - typed-array-length: 1.0.7 - unbox-primitive: 1.1.0 - which-typed-array: 1.1.18 - dev: true - - /es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - /es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - - /es-iterator-helpers@1.2.1: - resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.3 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-errors: 1.3.0 - es-set-tostringtag: 2.1.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - internal-slot: 1.1.0 - iterator.prototype: 1.1.5 - safe-array-concat: 1.1.3 - dev: true - - /es-module-lexer@1.6.0: - resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} - dev: true - - /es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - dependencies: - es-errors: 1.3.0 - - /es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - - /es-shim-unscopables@1.1.0: - resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} - engines: {node: '>= 0.4'} - dependencies: - hasown: 2.0.2 - dev: true - - /es-to-primitive@1.3.0: - resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} - engines: {node: '>= 0.4'} - dependencies: - is-callable: 1.2.7 - is-date-object: 1.1.0 - is-symbol: 1.1.1 - dev: true - - /esbuild-register@3.6.0(esbuild@0.25.0): - resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} - peerDependencies: - esbuild: '>=0.12 <1' - dependencies: - debug: 4.4.0 - esbuild: 0.25.0 - transitivePeerDependencies: - - supports-color - dev: true - - /esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 - dev: true - - /esbuild@0.24.2: - resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} - engines: {node: '>=18'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/aix-ppc64': 0.24.2 - '@esbuild/android-arm': 0.24.2 - '@esbuild/android-arm64': 0.24.2 - '@esbuild/android-x64': 0.24.2 - '@esbuild/darwin-arm64': 0.24.2 - '@esbuild/darwin-x64': 0.24.2 - '@esbuild/freebsd-arm64': 0.24.2 - '@esbuild/freebsd-x64': 0.24.2 - '@esbuild/linux-arm': 0.24.2 - '@esbuild/linux-arm64': 0.24.2 - '@esbuild/linux-ia32': 0.24.2 - '@esbuild/linux-loong64': 0.24.2 - '@esbuild/linux-mips64el': 0.24.2 - '@esbuild/linux-ppc64': 0.24.2 - '@esbuild/linux-riscv64': 0.24.2 - '@esbuild/linux-s390x': 0.24.2 - '@esbuild/linux-x64': 0.24.2 - '@esbuild/netbsd-arm64': 0.24.2 - '@esbuild/netbsd-x64': 0.24.2 - '@esbuild/openbsd-arm64': 0.24.2 - '@esbuild/openbsd-x64': 0.24.2 - '@esbuild/sunos-x64': 0.24.2 - '@esbuild/win32-arm64': 0.24.2 - '@esbuild/win32-ia32': 0.24.2 - '@esbuild/win32-x64': 0.24.2 - dev: true - - /esbuild@0.25.0: - resolution: {integrity: sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==} - engines: {node: '>=18'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/aix-ppc64': 0.25.0 - '@esbuild/android-arm': 0.25.0 - '@esbuild/android-arm64': 0.25.0 - '@esbuild/android-x64': 0.25.0 - '@esbuild/darwin-arm64': 0.25.0 - '@esbuild/darwin-x64': 0.25.0 - '@esbuild/freebsd-arm64': 0.25.0 - '@esbuild/freebsd-x64': 0.25.0 - '@esbuild/linux-arm': 0.25.0 - '@esbuild/linux-arm64': 0.25.0 - '@esbuild/linux-ia32': 0.25.0 - '@esbuild/linux-loong64': 0.25.0 - '@esbuild/linux-mips64el': 0.25.0 - '@esbuild/linux-ppc64': 0.25.0 - '@esbuild/linux-riscv64': 0.25.0 - '@esbuild/linux-s390x': 0.25.0 - '@esbuild/linux-x64': 0.25.0 - '@esbuild/netbsd-arm64': 0.25.0 - '@esbuild/netbsd-x64': 0.25.0 - '@esbuild/openbsd-arm64': 0.25.0 - '@esbuild/openbsd-x64': 0.25.0 - '@esbuild/sunos-x64': 0.25.0 - '@esbuild/win32-arm64': 0.25.0 - '@esbuild/win32-ia32': 0.25.0 - '@esbuild/win32-x64': 0.25.0 - dev: true - - /escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - dev: true - - /escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - dev: true - - /escodegen@2.1.0: - resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} - engines: {node: '>=6.0'} - hasBin: true - dependencies: - esprima: 4.0.1 - estraverse: 5.3.0 - esutils: 2.0.3 - optionalDependencies: - source-map: 0.6.1 - dev: true - - /eslint-config-next@15.1.7(eslint@9.21.0)(typescript@5.6.3): - resolution: {integrity: sha512-zXoMnYUIy3XHaAoOhrcYkT9UQWvXqWju2K7NNsmb5wd/7XESDwof61eUdW4QhERr3eJ9Ko/vnXqIrj8kk/drYw==} - peerDependencies: - eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 - typescript: '>=3.3.1' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@next/eslint-plugin-next': 15.1.7 - '@rushstack/eslint-patch': 1.10.5 - '@typescript-eslint/eslint-plugin': 8.25.0(@typescript-eslint/parser@8.25.0)(eslint@9.21.0)(typescript@5.6.3) - '@typescript-eslint/parser': 8.25.0(eslint@9.21.0)(typescript@5.6.3) - eslint: 9.21.0 - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@9.21.0) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.25.0)(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.21.0) - eslint-plugin-react: 7.37.4(eslint@9.21.0) - eslint-plugin-react-hooks: 5.1.0(eslint@9.21.0) - typescript: 5.6.3 - transitivePeerDependencies: - - eslint-import-resolver-webpack - - eslint-plugin-import-x - - supports-color - dev: true - - /eslint-import-resolver-node@0.3.9: - resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} - dependencies: - debug: 3.2.7 - is-core-module: 2.16.1 - resolve: 1.22.10 - transitivePeerDependencies: - - supports-color - dev: true - - /eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0)(eslint@9.21.0): - resolution: {integrity: sha512-A0bu4Ks2QqDWNpeEgTQMPTngaMhuDu4yv6xpftBMAf+1ziXnpx+eSR1WRfoPTe2BAiAjHFZ7kSNx1fvr5g5pmQ==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - eslint: '*' - eslint-plugin-import: '*' - eslint-plugin-import-x: '*' - peerDependenciesMeta: - eslint-plugin-import: - optional: true - eslint-plugin-import-x: - optional: true - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.0 - enhanced-resolve: 5.18.1 - eslint: 9.21.0 - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.25.0)(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0) - get-tsconfig: 4.10.0 - is-bun-module: 1.3.0 - stable-hash: 0.0.4 - tinyglobby: 0.2.12 - transitivePeerDependencies: - - supports-color - dev: true - - /eslint-module-utils@2.12.0(@typescript-eslint/parser@8.25.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0): - resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true - dependencies: - '@typescript-eslint/parser': 8.25.0(eslint@9.21.0)(typescript@5.6.3) - debug: 3.2.7 - eslint: 9.21.0 - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@9.21.0) - transitivePeerDependencies: - - supports-color - dev: true - - /eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.25.0)(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0): - resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - dependencies: - '@rtsao/scc': 1.1.0 - '@typescript-eslint/parser': 8.25.0(eslint@9.21.0)(typescript@5.6.3) - array-includes: 3.1.8 - array.prototype.findlastindex: 1.2.5 - array.prototype.flat: 1.3.3 - array.prototype.flatmap: 1.3.3 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 9.21.0 - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.25.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0) - hasown: 2.0.2 - is-core-module: 2.16.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.1 - semver: 6.3.1 - string.prototype.trimend: 1.0.9 - tsconfig-paths: 3.15.0 - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - dev: true - - /eslint-plugin-jsx-a11y@6.10.2(eslint@9.21.0): - resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} - engines: {node: '>=4.0'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - dependencies: - aria-query: 5.3.2 - array-includes: 3.1.8 - array.prototype.flatmap: 1.3.3 - ast-types-flow: 0.0.8 - axe-core: 4.10.2 - axobject-query: 4.1.0 - damerau-levenshtein: 1.0.8 - emoji-regex: 9.2.2 - eslint: 9.21.0 - hasown: 2.0.2 - jsx-ast-utils: 3.3.5 - language-tags: 1.0.9 - minimatch: 3.1.2 - object.fromentries: 2.0.8 - safe-regex-test: 1.1.0 - string.prototype.includes: 2.0.1 - dev: true - - /eslint-plugin-react-hooks@5.1.0(eslint@9.21.0): - resolution: {integrity: sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==} - engines: {node: '>=10'} - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - dependencies: - eslint: 9.21.0 - dev: true - - /eslint-plugin-react-refresh@0.4.19(eslint@9.21.0): - resolution: {integrity: sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==} - peerDependencies: - eslint: '>=8.40' - dependencies: - eslint: 9.21.0 - dev: true - - /eslint-plugin-react@7.37.4(eslint@9.21.0): - resolution: {integrity: sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==} - engines: {node: '>=4'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - dependencies: - array-includes: 3.1.8 - array.prototype.findlast: 1.2.5 - array.prototype.flatmap: 1.3.3 - array.prototype.tosorted: 1.1.4 - doctrine: 2.1.0 - es-iterator-helpers: 1.2.1 - eslint: 9.21.0 - estraverse: 5.3.0 - hasown: 2.0.2 - jsx-ast-utils: 3.3.5 - minimatch: 3.1.2 - object.entries: 1.1.8 - object.fromentries: 2.0.8 - object.values: 1.2.1 - prop-types: 15.8.1 - resolve: 2.0.0-next.5 - semver: 6.3.1 - string.prototype.matchall: 4.0.12 - string.prototype.repeat: 1.0.0 - dev: true - - /eslint-plugin-storybook@0.11.3(eslint@9.21.0)(typescript@5.6.3): - resolution: {integrity: sha512-gDBnBZiyk4ZG7OMSJRaHBcuJ8TMCXgMIQ3HB/XvtN0SvSio2ZOIeYD3yGj39g/DbyCe/Bg02j/ip9tWlDEs82A==} - engines: {node: '>= 18'} - peerDependencies: - eslint: '>=8' - typescript: '>=4.8.4 <5.8.0' - dependencies: - '@storybook/csf': 0.1.13 - '@typescript-eslint/utils': 8.25.0(eslint@9.21.0)(typescript@5.6.3) - eslint: 9.21.0 - ts-dedent: 2.2.0 - typescript: 5.6.3 - transitivePeerDependencies: - - supports-color - dev: true - - /eslint-scope@5.1.1: - resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} - engines: {node: '>=8.0.0'} - dependencies: - esrecurse: 4.3.0 - estraverse: 4.3.0 - dev: true - - /eslint-scope@8.2.0: - resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - dev: true - - /eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - - /eslint-visitor-keys@4.2.0: - resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dev: true - - /eslint@9.21.0: - resolution: {integrity: sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0) - '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.19.2 - '@eslint/core': 0.12.0 - '@eslint/eslintrc': 3.3.0 - '@eslint/js': 9.21.0 - '@eslint/plugin-kit': 0.2.7 - '@humanfs/node': 0.16.6 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.2 - '@types/estree': 1.0.6 - '@types/json-schema': 7.0.15 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.0 - escape-string-regexp: 4.0.0 - eslint-scope: 8.2.0 - eslint-visitor-keys: 4.2.0 - espree: 10.3.0 - esquery: 1.6.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.4 - transitivePeerDependencies: - - supports-color - dev: true - - /espree@10.3.0: - resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dependencies: - acorn: 8.14.0 - acorn-jsx: 5.3.2(acorn@8.14.0) - eslint-visitor-keys: 4.2.0 - dev: true - - /esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - dev: true - - /esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} - engines: {node: '>=0.10'} - dependencies: - estraverse: 5.3.0 - dev: true - - /esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - dependencies: - estraverse: 5.3.0 - dev: true - - /estraverse@4.3.0: - resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} - engines: {node: '>=4.0'} - dev: true - - /estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - dev: true - - /estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - dev: true - - /estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - dependencies: - '@types/estree': 1.0.6 - dev: true - - /esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - dev: true - - /eventemitter3@4.0.7: - resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - dev: false - - /events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} - dev: true - - /fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - /fast-equals@5.2.2: - resolution: {integrity: sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==} - engines: {node: '>=6.0.0'} - dev: false - - /fast-glob@3.3.1: - resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} - engines: {node: '>=8.6.0'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - dev: true - - /fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - - /fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - dev: true - - /fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - dev: true - - /fast-uri@3.0.6: - resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} - dev: true - - /fastest-levenshtein@1.0.16: - resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} - engines: {node: '>= 4.9.1'} - dev: true - - /fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} - dependencies: - reusify: 1.1.0 - - /fdir@6.4.3(picomatch@4.0.2): - resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - dependencies: - picomatch: 4.0.2 - dev: true - - /file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} - dependencies: - flat-cache: 4.0.1 - dev: true - - /file-selector@2.1.2: - resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==} - engines: {node: '>= 12'} - dependencies: - tslib: 2.8.1 - dev: false - - /fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - dependencies: - to-regex-range: 5.0.1 - - /find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - dev: false - - /find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - dev: true - - /flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} - dependencies: - flatted: 3.3.3 - keyv: 4.5.4 - dev: true - - /flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - dev: true - - /follow-redirects@1.15.9: - resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - dev: false - - /for-each@0.3.5: - resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} - engines: {node: '>= 0.4'} - dependencies: - is-callable: 1.2.7 - dev: true - - /foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - - /form-data@4.0.2: - resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} - engines: {node: '>= 6'} - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - mime-types: 2.1.35 - dev: false - - /frac@1.1.2: - resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} - engines: {node: '>=0.8'} - dev: false - - /fraction.js@4.3.7: - resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} - dev: true - - /framer-motion@11.18.2(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==} - peerDependencies: - '@emotion/is-prop-valid': '*' - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@emotion/is-prop-valid': - optional: true - react: - optional: true - react-dom: - optional: true - dependencies: - motion-dom: 11.18.1 - motion-utils: 11.18.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - tslib: 2.8.1 - dev: false - - /fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - requiresBuild: true - optional: true - - /function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - /function.prototype.name@1.1.8: - resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.3 - define-properties: 1.2.1 - functions-have-names: 1.2.3 - hasown: 2.0.2 - is-callable: 1.2.7 - dev: true - - /functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - dev: true - - /gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - dev: true - - /geotiff@2.1.3: - resolution: {integrity: sha512-PT6uoF5a1+kbC3tHmZSUsLHBp2QJlHasxxxxPW47QIY1VBKpFB+FcDvX+MxER6UzgLQZ0xDzJ9s48B9JbOCTqA==} - engines: {node: '>=10.19'} - dependencies: - '@petamoriken/float16': 3.9.2 - lerc: 3.0.0 - pako: 2.1.0 - parse-headers: 2.0.6 - quick-lru: 6.1.2 - web-worker: 1.5.0 - xml-utils: 1.10.2 - zstddec: 0.1.0 - dev: false - - /get-amd-module-type@6.0.1: - resolution: {integrity: sha512-MtjsmYiCXcYDDrGqtNbeIYdAl85n+5mSv2r3FbzER/YV3ZILw4HNNIw34HuV5pyl0jzs6GFYU1VHVEefhgcNHQ==} - engines: {node: '>=18'} - dependencies: - ast-module-types: 6.0.1 - node-source-walk: 7.0.1 - dev: true - - /get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - - /get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 - - /get-nonce@1.0.1: - resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} - engines: {node: '>=6'} - dev: false - - /get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - - /get-symbol-description@1.1.0: - resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.3 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - dev: true - - /get-tsconfig@4.10.0: - resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} - dependencies: - resolve-pkg-maps: 1.0.0 - dev: true - - /glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - dependencies: - is-glob: 4.0.3 - - /glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - dependencies: - is-glob: 4.0.3 - - /glob-to-regexp@0.4.1: - resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - dev: true - - /glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} - hasBin: true - dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.5 - minipass: 7.1.2 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - - /globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - dev: true - - /globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - dev: true - - /globals@15.15.0: - resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} - engines: {node: '>=18'} - dev: true - - /globalthis@1.0.4: - resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} - engines: {node: '>= 0.4'} - dependencies: - define-properties: 1.2.1 - gopd: 1.2.0 - dev: true - - /globby@14.1.0: - resolution: {integrity: sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==} - engines: {node: '>=18'} - dependencies: - '@sindresorhus/merge-streams': 2.3.0 - fast-glob: 3.3.3 - ignore: 7.0.3 - path-type: 6.0.0 - slash: 5.1.0 - unicorn-magic: 0.3.0 - dev: true - - /globrex@0.1.2: - resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - dev: true - - /gonzales-pe@4.3.0: - resolution: {integrity: sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==} - engines: {node: '>=0.6.0'} - hasBin: true - dependencies: - minimist: 1.2.8 - dev: true - - /goober@2.1.16(csstype@3.1.3): - resolution: {integrity: sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==} - peerDependencies: - csstype: ^3.0.10 - dependencies: - csstype: 3.1.3 - dev: false - - /gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - - /graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - /graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - dev: true - - /has-bigints@1.1.0: - resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} - engines: {node: '>= 0.4'} - dev: true - - /has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - dev: true - - /has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - dependencies: - es-define-property: 1.0.1 - dev: true - - /has-proto@1.2.0: - resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} - engines: {node: '>= 0.4'} - dependencies: - dunder-proto: 1.0.1 - dev: true - - /has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - /has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - dependencies: - has-symbols: 1.1.0 - - /hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - dependencies: - function-bind: 1.1.2 - - /hoist-non-react-statics@3.3.2: - resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - dependencies: - react-is: 16.13.1 - dev: false - - /ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - dev: false - - /ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - dev: true - - /ignore@7.0.3: - resolution: {integrity: sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==} - engines: {node: '>= 4'} - dev: true - - /immediate@3.0.6: - resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} - dev: false - - /immer@10.1.1: - resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} - dev: true - - /import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - dev: true - - /imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - dev: true - - /indent-string@4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} - dev: true - - /inherits@2.0.3: - resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} - dev: false - - /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - /internal-slot@1.1.0: - resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} - engines: {node: '>= 0.4'} - dependencies: - es-errors: 1.3.0 - hasown: 2.0.2 - side-channel: 1.1.0 - dev: true - - /internmap@2.0.3: - resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} - engines: {node: '>=12'} - dev: false - - /is-arguments@1.2.0: - resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.3 - has-tostringtag: 1.0.2 - dev: true - - /is-array-buffer@3.0.5: - resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.3 - get-intrinsic: 1.3.0 - dev: true - - /is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - dev: true - - /is-arrayish@0.3.2: - resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} - requiresBuild: true - dev: false - optional: true - - /is-async-function@2.1.1: - resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} - engines: {node: '>= 0.4'} - dependencies: - async-function: 1.0.0 - call-bound: 1.0.3 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - dev: true - - /is-bigint@1.1.0: - resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} - engines: {node: '>= 0.4'} - dependencies: - has-bigints: 1.1.0 - dev: true - - /is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - dependencies: - binary-extensions: 2.3.0 - - /is-boolean-object@1.2.2: - resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.3 - has-tostringtag: 1.0.2 - dev: true - - /is-bun-module@1.3.0: - resolution: {integrity: sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==} - dependencies: - semver: 7.7.1 - dev: true - - /is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - dev: true - - /is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} - dependencies: - hasown: 2.0.2 - - /is-data-view@1.0.2: - resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.3 - get-intrinsic: 1.3.0 - is-typed-array: 1.1.15 - dev: true - - /is-date-object@1.1.0: - resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.3 - has-tostringtag: 1.0.2 - dev: true - - /is-docker@2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} - hasBin: true - dev: true - - /is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - /is-finalizationregistry@1.1.1: - resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.3 - dev: true - - /is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - /is-generator-function@1.1.0: - resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.3 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - dev: true - - /is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - dependencies: - is-extglob: 2.1.1 - - /is-map@2.0.3: - resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} - engines: {node: '>= 0.4'} - dev: true - - /is-number-object@1.1.1: - resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.3 - has-tostringtag: 1.0.2 - dev: true - - /is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - /is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - dev: false - - /is-regex@1.2.1: - resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.3 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - dev: true - - /is-set@2.0.3: - resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} - engines: {node: '>= 0.4'} - dev: true - - /is-shared-array-buffer@1.0.4: - resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.3 - dev: true - - /is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - dev: false - - /is-string@1.1.1: - resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.3 - has-tostringtag: 1.0.2 - dev: true - - /is-symbol@1.1.1: - resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.3 - has-symbols: 1.1.0 - safe-regex-test: 1.1.0 - dev: true - - /is-typed-array@1.1.15: - resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} - engines: {node: '>= 0.4'} - dependencies: - which-typed-array: 1.1.18 - dev: true - - /is-url-superb@4.0.0: - resolution: {integrity: sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==} - engines: {node: '>=10'} - dev: true - - /is-url@1.2.4: - resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} - dev: true - - /is-weakmap@2.0.2: - resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} - engines: {node: '>= 0.4'} - dev: true - - /is-weakref@1.1.1: - resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.3 - dev: true - - /is-weakset@2.0.4: - resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.3 - get-intrinsic: 1.3.0 - dev: true - - /is-wsl@2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} - dependencies: - is-docker: 2.2.1 - dev: true - - /isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - dev: false - - /isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - dev: true - - /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - /iterator.prototype@1.1.5: - resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} - engines: {node: '>= 0.4'} - dependencies: - define-data-property: 1.1.4 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - has-symbols: 1.1.0 - set-function-name: 2.0.2 - dev: true - - /jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - - /jest-worker@27.5.1: - resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} - engines: {node: '>= 10.13.0'} - dependencies: - '@types/node': 22.13.9 - merge-stream: 2.0.0 - supports-color: 8.1.1 - dev: true - - /jiti@1.21.7: - resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} - hasBin: true - - /joycon@3.1.1: - resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} - engines: {node: '>=10'} - dev: true - - /js-base64@3.7.7: - resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} - dev: false - - /js-cookie@3.0.5: - resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} - engines: {node: '>=14'} - dev: false - - /js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - /js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - dependencies: - argparse: 2.0.1 - dev: true - - /jsdoc-type-pratt-parser@4.1.0: - resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==} - engines: {node: '>=12.0.0'} - dev: true - - /jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true - dev: true - - /json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - dev: true - - /json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - dev: true - - /json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - dev: true - - /json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - dev: true - - /json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - dev: true - - /json-stringify-pretty-compact@2.0.0: - resolution: {integrity: sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ==} - dev: false - - /json5@1.0.2: - resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} - hasBin: true - dependencies: - minimist: 1.2.8 - dev: true - - /json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - dev: true - - /jsx-ast-utils@3.3.5: - resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} - engines: {node: '>=4.0'} - dependencies: - array-includes: 3.1.8 - array.prototype.flat: 1.3.3 - object.assign: 4.1.7 - object.values: 1.2.1 - dev: true - - /jszip@3.10.1: - resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} - dependencies: - lie: 3.3.0 - pako: 1.0.11 - readable-stream: 2.3.8 - setimmediate: 1.0.5 - dev: false - - /keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - dependencies: - json-buffer: 3.0.1 - dev: true - - /language-subtag-registry@0.3.23: - resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} - dev: true - - /language-tags@1.0.9: - resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} - engines: {node: '>=0.10'} - dependencies: - language-subtag-registry: 0.3.23 - dev: true - - /lerc@3.0.0: - resolution: {integrity: sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==} - dev: false - - /levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - dev: true - - /lie@3.3.0: - resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} - dependencies: - immediate: 3.0.6 - dev: false - - /lilconfig@3.1.3: - resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} - engines: {node: '>=14'} - - /lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - /load-tsconfig@0.2.5: - resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true - - /loader-runner@4.3.0: - resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} - engines: {node: '>=6.11.5'} - dev: true - - /locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} - dependencies: - p-locate: 4.1.0 - dev: false - - /locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - dependencies: - p-locate: 5.0.0 - dev: true - - /lodash-es@4.17.21: - resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} - dev: true - - /lodash._baseiteratee@4.7.0: - resolution: {integrity: sha512-nqB9M+wITz0BX/Q2xg6fQ8mLkyfF7MU7eE+MNBNjTHFKeKaZAPEzEg+E8LWxKWf1DQVflNEn9N49yAuqKh2mWQ==} - dependencies: - lodash._stringtopath: 4.8.0 - dev: false - - /lodash._basetostring@4.12.0: - resolution: {integrity: sha512-SwcRIbyxnN6CFEEK4K1y+zuApvWdpQdBHM/swxP962s8HIxPO3alBH5t3m/dl+f4CMUug6sJb7Pww8d13/9WSw==} - dev: false - - /lodash._baseuniq@4.6.0: - resolution: {integrity: sha512-Ja1YevpHZctlI5beLA7oc5KNDhGcPixFhcqSiORHNsp/1QTv7amAXzw+gu4YOvErqVlMVyIJGgtzeepCnnur0A==} - dependencies: - lodash._createset: 4.0.3 - lodash._root: 3.0.1 - dev: false - - /lodash._createset@4.0.3: - resolution: {integrity: sha512-GTkC6YMprrJZCYU3zcqZj+jkXkrXzq3IPBcF/fIPpNEAB4hZEtXU8zp/RwKOvZl43NUmwDbyRk3+ZTbeRdEBXA==} - dev: false - - /lodash._root@3.0.1: - resolution: {integrity: sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ==} - dev: false - - /lodash._stringtopath@4.8.0: - resolution: {integrity: sha512-SXL66C731p0xPDC5LZg4wI5H+dJo/EO4KTqOMwLYCH3+FmmfAKJEZCm6ohGpI+T1xwsDsJCfL4OnhorllvlTPQ==} - dependencies: - lodash._basetostring: 4.12.0 - dev: false - - /lodash.add@3.7.0: - resolution: {integrity: sha512-/uxUZkB6vYiPoGSXADJJE+Ee4W2vU+bxZOFdvJdx+mDgv+qyAO76sWD84Q4FxsMgTjC1+gVFC9YNhdG243QRDw==} - dev: false - - /lodash.clonedeep@4.5.0: - resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} - dev: false - - /lodash.isequal@4.5.0: - resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} - deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. - dev: false - - /lodash.isfunction@3.0.9: - resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==} - dev: false - - /lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - dev: true - - /lodash.size@4.2.0: - resolution: {integrity: sha512-wbu3SF1XC5ijqm0piNxw59yCbuUf2kaShumYBLWUrcCvwh6C8odz6SY/wGVzCWTQTFL/1Ygbvqg2eLtspUVVAQ==} - dev: false - - /lodash.sortby@4.7.0: - resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} - dev: true - - /lodash.throttle@4.1.1: - resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} - dev: false - - /lodash.uniqby@4.5.0: - resolution: {integrity: sha512-IRt7cfTtHy6f1aRVA5n7kT8rgN3N1nH6MOWLcHfpWG2SH19E3JksLK38MktLxZDhlAjCP9jpIXkOnRXlu6oByQ==} - dependencies: - lodash._baseiteratee: 4.7.0 - lodash._baseuniq: 4.6.0 - dev: false - - /lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - - /loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true - dependencies: - js-tokens: 4.0.0 - - /loupe@3.1.3: - resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} - dev: true - - /lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - - /lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - dependencies: - yallist: 3.1.1 - dev: true - - /lucide-react@0.474.0(react@18.3.1): - resolution: {integrity: sha512-CmghgHkh0OJNmxGKWc0qfPJCYHASPMVSyGY8fj3xgk4v84ItqDg64JNKFZn5hC6E0vHi6gxnbCgwhyVB09wQtA==} - peerDependencies: - react: ^18.3.1 - dependencies: - react: 18.3.1 - dev: false - - /lz-string@1.5.0: - resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} - hasBin: true - dev: true - - /magic-string@0.27.0: - resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} - engines: {node: '>=12'} - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - dev: true - - /magic-string@0.30.17: - resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - dev: true - - /map-or-similar@1.5.0: - resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} - dev: true - - /mapbox-to-css-font@2.4.5: - resolution: {integrity: sha512-VJ6nB8emkO9VODI0Fk+TQ/0zKBTqmf/Pkt8Xv0kHstoc0iXRajA00DAid4Kc3K5xeFIOoiZrVxijEzj0GLVO2w==} - dev: false - - /math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - /memoizerific@1.11.3: - resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==} - dependencies: - map-or-similar: 1.5.0 - dev: true - - /merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - dev: true - - /merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - /mgrs@1.0.0: - resolution: {integrity: sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA==} - dev: false - - /micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - - /mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - - /mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - dependencies: - mime-db: 1.52.0 - - /min-indent@1.0.1: - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} - engines: {node: '>=4'} - dev: true - - /minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - dependencies: - brace-expansion: 1.1.11 - dev: true - - /minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: '>=16 || 14 >=14.17'} - dependencies: - brace-expansion: 2.0.1 - - /minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - - /minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} - - /module-definition@6.0.1: - resolution: {integrity: sha512-FeVc50FTfVVQnolk/WQT8MX+2WVcDnTGiq6Wo+/+lJ2ET1bRVi3HG3YlJUfqagNMc/kUlFSoR96AJkxGpKz13g==} - engines: {node: '>=18'} - hasBin: true - dependencies: - ast-module-types: 6.0.1 - node-source-walk: 7.0.1 - dev: true - - /moment@2.30.1: - resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} - dev: false - - /motion-dom@11.18.1: - resolution: {integrity: sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==} - dependencies: - motion-utils: 11.18.1 - dev: false - - /motion-utils@11.18.1: - resolution: {integrity: sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==} - dev: false - - /ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - dev: true - - /mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 - - /nanoid@3.3.8: - resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - /natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - dev: true - - /neo-async@2.6.2: - resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - dev: true - - /next-qrcode@2.5.1(react@18.3.1): - resolution: {integrity: sha512-ibiS1+p+myjurC1obVIgo7UCjFhxm0SNYTkikahQVmnyzlfREOdUCf2f77Vbz6W6q2zfx5Eww2/20vbgkLNdLw==} - engines: {node: '>=8', npm: '>=5'} - peerDependencies: - react: ^18.3.1 - dependencies: - qrcode: 1.5.4 - react: 18.3.1 - dev: false - - /next-themes@0.4.6(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} - peerDependencies: - react: ^18.3.1 - react-dom: ^18.3.1 - dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /next@15.1.7(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-GNeINPGS9c6OZKCvKypbL8GTsT5GhWPp4DM0fzkXJuXMilOO2EeFxuAY6JZbtk6XIl6Ws10ag3xRINDjSO5+wg==} - engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.41.2 - babel-plugin-react-compiler: '*' - react: ^18.3.1 - react-dom: ^18.3.1 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - '@playwright/test': - optional: true - babel-plugin-react-compiler: - optional: true - sass: - optional: true - dependencies: - '@next/env': 15.1.7 - '@swc/counter': 0.1.3 - '@swc/helpers': 0.5.15 - busboy: 1.6.0 - caniuse-lite: 1.0.30001701 - postcss: 8.4.31 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.6(react@18.3.1) - optionalDependencies: - '@next/swc-darwin-arm64': 15.1.7 - '@next/swc-darwin-x64': 15.1.7 - '@next/swc-linux-arm64-gnu': 15.1.7 - '@next/swc-linux-arm64-musl': 15.1.7 - '@next/swc-linux-x64-gnu': 15.1.7 - '@next/swc-linux-x64-musl': 15.1.7 - '@next/swc-win32-arm64-msvc': 15.1.7 - '@next/swc-win32-x64-msvc': 15.1.7 - sharp: 0.33.5 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - dev: false - - /next@15.3.0(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-k0MgP6BsK8cZ73wRjMazl2y2UcXj49ZXLDEgx6BikWuby/CN+nh81qFFI16edgd7xYpe/jj2OZEIwCoqnzz0bQ==} - engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.41.2 - babel-plugin-react-compiler: '*' - react: ^18.3.1 - react-dom: ^18.3.1 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - '@playwright/test': - optional: true - babel-plugin-react-compiler: - optional: true - sass: - optional: true - dependencies: - '@next/env': 15.3.0 - '@swc/counter': 0.1.3 - '@swc/helpers': 0.5.15 - busboy: 1.6.0 - caniuse-lite: 1.0.30001701 - postcss: 8.4.31 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.6(react@18.3.1) - optionalDependencies: - '@next/swc-darwin-arm64': 15.3.0 - '@next/swc-darwin-x64': 15.3.0 - '@next/swc-linux-arm64-gnu': 15.3.0 - '@next/swc-linux-arm64-musl': 15.3.0 - '@next/swc-linux-x64-gnu': 15.3.0 - '@next/swc-linux-x64-musl': 15.3.0 - '@next/swc-win32-arm64-msvc': 15.3.0 - '@next/swc-win32-x64-msvc': 15.3.0 - sharp: 0.34.1 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - dev: false - - /node-releases@2.0.19: - resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} - dev: true - - /node-source-walk@7.0.1: - resolution: {integrity: sha512-3VW/8JpPqPvnJvseXowjZcirPisssnBuDikk6JIZ8jQzF7KJQX52iPFX4RYYxLycYH7IbMRSPUOga/esVjy5Yg==} - engines: {node: '>=18'} - dependencies: - '@babel/parser': 7.26.9 - dev: true - - /normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - /normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} - dev: true - - /object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - /object-hash@3.0.0: - resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} - engines: {node: '>= 6'} - - /object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - dev: true - - /object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - dev: true - - /object.assign@4.1.7: - resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.3 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - has-symbols: 1.1.0 - object-keys: 1.1.1 - dev: true - - /object.entries@1.1.8: - resolution: {integrity: sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - dev: true - - /object.fromentries@2.0.8: - resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-object-atoms: 1.1.1 - dev: true - - /object.groupby@1.0.3: - resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - dev: true - - /object.values@1.2.1: - resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.3 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - dev: true - - /ol-ext@4.0.31(ol@7.5.2): - resolution: {integrity: sha512-byh26JcXZggkLUY79+c29cxf2ovkkwz5niWJzOIN+dLPDJ+ogSn0y4Wl8fIS9O2JtQff5/cInHRImgZBvl+N1g==} - peerDependencies: - ol: '>= 5.3.0' - dependencies: - ol: 7.5.2 - dev: false - - /ol-mapbox-style@10.7.0: - resolution: {integrity: sha512-S/UdYBuOjrotcR95Iq9AejGYbifKeZE85D9VtH11ryJLQPTZXZSW1J5bIXcr4AlAH6tyjPPHTK34AdkwB32Myw==} - dependencies: - '@mapbox/mapbox-gl-style-spec': 13.28.0 - mapbox-to-css-font: 2.4.5 - ol: 7.5.2 - dev: false - - /ol@7.5.2: - resolution: {integrity: sha512-HJbb3CxXrksM6ct367LsP3N+uh+iBBMdP3DeGGipdV9YAYTP0vTJzqGnoqQ6C2IW4qf8krw9yuyQbc9fjOIaOQ==} - dependencies: - earcut: 2.2.4 - geotiff: 2.1.3 - ol-mapbox-style: 10.7.0 - pbf: 3.2.1 - rbush: 3.0.1 - dev: false - - /open@8.4.2: - resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} - engines: {node: '>=12'} - dependencies: - define-lazy-prop: 2.0.0 - is-docker: 2.2.1 - is-wsl: 2.2.0 - dev: true - - /optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - dev: true - - /own-keys@1.0.1: - resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} - engines: {node: '>= 0.4'} - dependencies: - get-intrinsic: 1.3.0 - object-keys: 1.1.1 - safe-push-apply: 1.0.0 - dev: true - - /p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} - dependencies: - p-try: 2.2.0 - dev: false - - /p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - dependencies: - yocto-queue: 0.1.0 - dev: true - - /p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} - dependencies: - p-limit: 2.3.0 - dev: false - - /p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - dependencies: - p-limit: 3.1.0 - dev: true - - /p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - dev: false - - /package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - - /pako@1.0.11: - resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} - dev: false - - /pako@2.1.0: - resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} - dev: false - - /parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - dependencies: - callsites: 3.1.0 - dev: true - - /parse-headers@2.0.6: - resolution: {integrity: sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==} - dev: false - - /parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - dependencies: - '@babel/code-frame': 7.26.2 - error-ex: 1.3.2 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - dev: true - - /path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - /path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - /path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - /path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.2 - - /path-type@6.0.0: - resolution: {integrity: sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==} - engines: {node: '>=18'} - dev: true - - /path@0.12.7: - resolution: {integrity: sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==} - dependencies: - process: 0.11.10 - util: 0.10.4 - dev: false - - /pathval@2.0.0: - resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} - engines: {node: '>= 14.16'} - dev: true - - /patronum@2.3.0(effector@23.3.0): - resolution: {integrity: sha512-BfKIOpoymVz6XnkOn8Fi5QZ1a3r/3lXdd8BcdHmYDbIXPTIRnD1EPFBFev/DheWnOge6/ZswEqgNF2ANLGOxLw==} - peerDependencies: - effector: ^23 - dependencies: - effector: 23.3.0 - dev: true - - /pbf@3.2.1: - resolution: {integrity: sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==} - hasBin: true - dependencies: - ieee754: 1.2.1 - resolve-protobuf-schema: 2.1.0 - dev: false - - /picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - /picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - /picomatch@4.0.2: - resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} - engines: {node: '>=12'} - dev: true - - /pify@2.3.0: - resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} - engines: {node: '>=0.10.0'} - - /pirates@4.0.6: - resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} - engines: {node: '>= 6'} - - /pluralize@8.0.0: - resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} - engines: {node: '>=4'} - dev: true - - /pngjs@5.0.0: - resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} - engines: {node: '>=10.13.0'} - dev: false - - /polished@4.3.1: - resolution: {integrity: sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==} - engines: {node: '>=10'} - dependencies: - '@babel/runtime': 7.26.9 - dev: true - - /possible-typed-array-names@1.1.0: - resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} - engines: {node: '>= 0.4'} - dev: true - - /postcss-import@15.1.0(postcss@8.5.3): - resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} - engines: {node: '>=14.0.0'} - peerDependencies: - postcss: ^8.0.0 - dependencies: - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - read-cache: 1.0.0 - resolve: 1.22.10 - - /postcss-js@4.0.1(postcss@8.5.3): - resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} - engines: {node: ^12 || ^14 || >= 16} - peerDependencies: - postcss: ^8.4.21 - dependencies: - camelcase-css: 2.0.1 - postcss: 8.5.3 - - /postcss-load-config@4.0.2(postcss@8.5.3): - resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} - engines: {node: '>= 14'} - peerDependencies: - postcss: '>=8.0.9' - ts-node: '>=9.0.0' - peerDependenciesMeta: - postcss: - optional: true - ts-node: - optional: true - dependencies: - lilconfig: 3.1.3 - postcss: 8.5.3 - yaml: 2.7.0 - - /postcss-load-config@6.0.1(postcss@8.5.3): - resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} - engines: {node: '>= 18'} - peerDependencies: - jiti: '>=1.21.0' - postcss: '>=8.0.9' - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - jiti: - optional: true - postcss: - optional: true - tsx: - optional: true - yaml: - optional: true - dependencies: - lilconfig: 3.1.3 - postcss: 8.5.3 - dev: true - - /postcss-nested@6.2.0(postcss@8.5.3): - resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.2.14 - dependencies: - postcss: 8.5.3 - postcss-selector-parser: 6.1.2 - - /postcss-selector-parser@6.1.2: - resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} - engines: {node: '>=4'} - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - - /postcss-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - - /postcss-values-parser@6.0.2(postcss@8.5.3): - resolution: {integrity: sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw==} - engines: {node: '>=10'} - peerDependencies: - postcss: ^8.2.9 - dependencies: - color-name: 1.1.4 - is-url-superb: 4.0.0 - postcss: 8.5.3 - quote-unquote: 1.0.0 - dev: true - - /postcss@8.4.31: - resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} - engines: {node: ^10 || ^12 || >=14} - dependencies: - nanoid: 3.3.8 - picocolors: 1.1.1 - source-map-js: 1.2.1 - dev: false - - /postcss@8.5.3: - resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} - engines: {node: ^10 || ^12 || >=14} - dependencies: - nanoid: 3.3.8 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - /preact@10.12.1: - resolution: {integrity: sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==} - dev: false - - /precinct@12.2.0: - resolution: {integrity: sha512-NFBMuwIfaJ4SocE9YXPU/n4AcNSoFMVFjP72nvl3cx69j/ke61/hPOWFREVxLkFhhEGnA8ZuVfTqJBa+PK3b5w==} - engines: {node: '>=18'} - hasBin: true - dependencies: - '@dependents/detective-less': 5.0.1 - commander: 12.1.0 - detective-amd: 6.0.1 - detective-cjs: 6.0.1 - detective-es6: 5.0.1 - detective-postcss: 7.0.1(postcss@8.5.3) - detective-sass: 6.0.1 - detective-scss: 5.0.1 - detective-stylus: 5.0.1 - detective-typescript: 14.0.0(typescript@5.7.3) - detective-vue2: 2.2.0(typescript@5.7.3) - module-definition: 6.0.1 - node-source-walk: 7.0.1 - postcss: 8.5.3 - typescript: 5.7.3 - transitivePeerDependencies: - - supports-color - dev: true - - /prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - dev: true - - /prettier-plugin-tailwindcss@0.6.11(prettier@3.5.2): - resolution: {integrity: sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA==} - engines: {node: '>=14.21.3'} - peerDependencies: - '@ianvs/prettier-plugin-sort-imports': '*' - '@prettier/plugin-pug': '*' - '@shopify/prettier-plugin-liquid': '*' - '@trivago/prettier-plugin-sort-imports': '*' - '@zackad/prettier-plugin-twig': '*' - prettier: ^3.0 - prettier-plugin-astro: '*' - prettier-plugin-css-order: '*' - prettier-plugin-import-sort: '*' - prettier-plugin-jsdoc: '*' - prettier-plugin-marko: '*' - prettier-plugin-multiline-arrays: '*' - prettier-plugin-organize-attributes: '*' - prettier-plugin-organize-imports: '*' - prettier-plugin-sort-imports: '*' - prettier-plugin-style-order: '*' - prettier-plugin-svelte: '*' - peerDependenciesMeta: - '@ianvs/prettier-plugin-sort-imports': - optional: true - '@prettier/plugin-pug': - optional: true - '@shopify/prettier-plugin-liquid': - optional: true - '@trivago/prettier-plugin-sort-imports': - optional: true - '@zackad/prettier-plugin-twig': - optional: true - prettier-plugin-astro: - optional: true - prettier-plugin-css-order: - optional: true - prettier-plugin-import-sort: - optional: true - prettier-plugin-jsdoc: - optional: true - prettier-plugin-marko: - optional: true - prettier-plugin-multiline-arrays: - optional: true - prettier-plugin-organize-attributes: - optional: true - prettier-plugin-organize-imports: - optional: true - prettier-plugin-sort-imports: - optional: true - prettier-plugin-style-order: - optional: true - prettier-plugin-svelte: - optional: true - dependencies: - prettier: 3.5.2 - dev: true - - /prettier@3.5.2: - resolution: {integrity: sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==} - engines: {node: '>=14'} - hasBin: true - dev: true - - /pretty-format@27.5.1: - resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - ansi-regex: 5.0.1 - ansi-styles: 5.2.0 - react-is: 17.0.2 - dev: true - - /prexit@2.3.0: - resolution: {integrity: sha512-mX+LIbtS0anKtl2PykYabxninwloblUQMRO6CubeSmjxb+kKlATuJoH9UeN8NLE4TgIEFWfBXw7V3GkWbBrSmg==} - engines: {node: '>=12'} - dev: true - - /process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - dev: false - - /process@0.11.10: - resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} - engines: {node: '>= 0.6.0'} - - /proj4@2.15.0: - resolution: {integrity: sha512-LqCNEcPdI03BrCHxPLj29vsd5afsm+0sV1H/O3nTDKrv8/LA01ea1z4QADDMjUqxSXWnrmmQDjqFm1J/uZ5RLw==} - dependencies: - mgrs: 1.0.0 - wkt-parser: 1.4.0 - dev: false - - /prop-types@15.8.1: - resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - react-is: 16.13.1 - - /proper-lockfile@4.1.2: - resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} - dependencies: - graceful-fs: 4.2.11 - retry: 0.12.0 - signal-exit: 3.0.7 - dev: false - - /protocol-buffers-schema@3.6.0: - resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==} - dev: false - - /proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - dev: false - - /punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - dev: true - - /qrcode@1.5.4: - resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} - engines: {node: '>=10.13.0'} - hasBin: true - dependencies: - dijkstrajs: 1.0.3 - pngjs: 5.0.0 - yargs: 15.4.1 - dev: false - - /querystringify@2.2.0: - resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} - dev: false - - /queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - /quick-lru@6.1.2: - resolution: {integrity: sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==} - engines: {node: '>=12'} - dev: false - - /quickselect@2.0.0: - resolution: {integrity: sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==} - dev: false - - /quote-unquote@1.0.0: - resolution: {integrity: sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==} - dev: true - - /randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - dependencies: - safe-buffer: 5.2.1 - dev: true - - /rbush@3.0.1: - resolution: {integrity: sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==} - dependencies: - quickselect: 2.0.0 - dev: false - - /react-chartjs-2@5.3.0(chart.js@4.4.8)(react@18.3.1): - resolution: {integrity: sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==} - peerDependencies: - chart.js: ^4.1.1 - react: ^18.3.1 - dependencies: - chart.js: 4.4.8 - react: 18.3.1 - dev: false - - /react-day-picker@8.10.1(date-fns@3.6.0)(react@18.3.1): - resolution: {integrity: sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==} - peerDependencies: - date-fns: ^2.28.0 || ^3.0.0 - react: ^18.3.1 - dependencies: - date-fns: 3.6.0 - react: 18.3.1 - dev: false - - /react-day-picker@9.7.0(react@18.3.1): - resolution: {integrity: sha512-urlK4C9XJZVpQ81tmVgd2O7lZ0VQldZeHzNejbwLWZSkzHH498KnArT0EHNfKBOWwKc935iMLGZdxXPRISzUxQ==} - engines: {node: '>=18'} - peerDependencies: - react: ^18.3.1 - dependencies: - '@date-fns/tz': 1.2.0 - date-fns: 4.1.0 - date-fns-jalali: 4.1.0-0 - react: 18.3.1 - dev: false - - /react-dnd-html5-backend@16.0.1: - resolution: {integrity: sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==} - dependencies: - dnd-core: 16.0.1 - dev: false - - /react-dnd-touch-backend@16.0.1: - resolution: {integrity: sha512-NonoCABzzjyWGZuDxSG77dbgMZ2Wad7eQiCd/ECtsR2/NBLTjGksPUx9UPezZ1nQ/L7iD130Tz3RUshL/ClKLA==} - dependencies: - '@react-dnd/invariant': 4.0.2 - dnd-core: 16.0.1 - dev: false - - /react-dnd@16.0.1(@types/node@22.13.5)(@types/react@18.3.18)(react@18.3.1): - resolution: {integrity: sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==} - peerDependencies: - '@types/hoist-non-react-statics': '>= 3.3.1' - '@types/node': '>= 12' - '@types/react': ^18.3.18 - react: ^18.3.1 - peerDependenciesMeta: - '@types/hoist-non-react-statics': - optional: true - '@types/node': - optional: true - '@types/react': - optional: true - dependencies: - '@react-dnd/invariant': 4.0.2 - '@react-dnd/shallowequal': 4.0.2 - '@types/node': 22.13.5 - '@types/react': 18.3.18 - dnd-core: 16.0.1 - fast-deep-equal: 3.1.3 - hoist-non-react-statics: 3.3.2 - react: 18.3.1 - dev: false - - /react-dnd@16.0.1(@types/node@22.13.9)(@types/react@18.3.18)(react@18.3.1): - resolution: {integrity: sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==} - peerDependencies: - '@types/hoist-non-react-statics': '>= 3.3.1' - '@types/node': '>= 12' - '@types/react': ^18.3.18 - react: ^18.3.1 - peerDependenciesMeta: - '@types/hoist-non-react-statics': - optional: true - '@types/node': - optional: true - '@types/react': - optional: true - dependencies: - '@react-dnd/invariant': 4.0.2 - '@react-dnd/shallowequal': 4.0.2 - '@types/node': 22.13.9 - '@types/react': 18.3.18 - dnd-core: 16.0.1 - fast-deep-equal: 3.1.3 - hoist-non-react-statics: 3.3.2 - react: 18.3.1 - dev: false - - /react-docgen-typescript@2.2.2(typescript@5.6.3): - resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==} - peerDependencies: - typescript: '>= 4.3.x' - dependencies: - typescript: 5.6.3 - dev: true - - /react-docgen@7.1.1: - resolution: {integrity: sha512-hlSJDQ2synMPKFZOsKo9Hi8WWZTC7POR8EmWvTSjow+VDgKzkmjQvFm2fk0tmRw+f0vTOIYKlarR0iL4996pdg==} - engines: {node: '>=16.14.0'} - dependencies: - '@babel/core': 7.26.9 - '@babel/traverse': 7.26.9 - '@babel/types': 7.26.9 - '@types/babel__core': 7.20.5 - '@types/babel__traverse': 7.20.6 - '@types/doctrine': 0.0.9 - '@types/resolve': 1.20.6 - doctrine: 3.0.0 - resolve: 1.22.10 - strip-indent: 4.0.0 - transitivePeerDependencies: - - supports-color - dev: true - - /react-dom@18.3.1(react@18.3.1): - resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} - peerDependencies: - react: ^18.3.1 - dependencies: - loose-envify: 1.4.0 - react: 18.3.1 - scheduler: 0.23.2 - - /react-dropzone@14.3.8(react@18.3.1): - resolution: {integrity: sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==} - engines: {node: '>= 10.13'} - peerDependencies: - react: ^18.3.1 - dependencies: - attr-accept: 2.2.5 - file-selector: 2.1.2 - prop-types: 15.8.1 - react: 18.3.1 - dev: false - - /react-hook-form@7.54.2(react@18.3.1): - resolution: {integrity: sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==} - engines: {node: '>=18.0.0'} - peerDependencies: - react: ^18.3.1 - dependencies: - react: 18.3.1 - dev: false - - /react-hot-toast@2.5.2(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==} - engines: {node: '>=10'} - peerDependencies: - react: ^18.3.1 - react-dom: ^18.3.1 - dependencies: - csstype: 3.1.3 - goober: 2.1.16(csstype@3.1.3) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /react-is@16.13.1: - resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - - /react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - dev: true - - /react-is@18.3.1: - resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - dev: false - - /react-refresh@0.14.2: - resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} - engines: {node: '>=0.10.0'} - dev: true - - /react-remove-scroll-bar@2.3.8(@types/react@18.3.18)(react@18.3.1): - resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': ^18.3.18 - react: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.3.18 - react: 18.3.1 - react-style-singleton: 2.2.3(@types/react@18.3.18)(react@18.3.1) - tslib: 2.8.1 - dev: false - - /react-remove-scroll@2.6.3(@types/react@18.3.18)(react@18.3.1): - resolution: {integrity: sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': ^18.3.18 - react: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.3.18 - react: 18.3.1 - react-remove-scroll-bar: 2.3.8(@types/react@18.3.18)(react@18.3.1) - react-style-singleton: 2.2.3(@types/react@18.3.18)(react@18.3.1) - tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@18.3.18)(react@18.3.1) - use-sidecar: 1.1.3(@types/react@18.3.18)(react@18.3.1) - dev: false - - /react-smooth@4.0.4(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} - peerDependencies: - react: ^18.3.1 - react-dom: ^18.3.1 - dependencies: - fast-equals: 5.2.2 - prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-transition-group: 4.4.5(react-dom@18.3.1)(react@18.3.1) - dev: false - - /react-style-singleton@2.2.3(@types/react@18.3.18)(react@18.3.1): - resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': ^18.3.18 - react: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.3.18 - get-nonce: 1.0.1 - react: 18.3.1 - tslib: 2.8.1 - dev: false - - /react-transition-group@4.4.5(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} - peerDependencies: - react: ^18.3.1 - react-dom: ^18.3.1 - dependencies: - '@babel/runtime': 7.26.9 - dom-helpers: 5.2.1 - loose-envify: 1.4.0 - prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /react-use-measure@2.1.7(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==} - peerDependencies: - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - react-dom: - optional: true - dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /react@18.3.1: - resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} - engines: {node: '>=0.10.0'} - dependencies: - loose-envify: 1.4.0 - - /read-cache@1.0.0: - resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} - dependencies: - pify: 2.3.0 - - /readable-stream@2.3.8: - resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 2.0.1 - safe-buffer: 5.1.2 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - dev: false - - /readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - dependencies: - picomatch: 2.3.1 - - /readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} - dev: true - - /recast@0.23.10: - resolution: {integrity: sha512-mbCmRMJUKCJ1h41V0cu2C26ULBURwuoZ34C9rChjcDaeJ/4Kv5al3O2HPwTs2m0wQ1vGhMY+tguhzU1aE8md1A==} - engines: {node: '>= 4'} - dependencies: - ast-types: 0.16.1 - esprima: 4.0.1 - source-map: 0.6.1 - tiny-invariant: 1.3.3 - tslib: 2.8.1 - dev: true - - /recharts-scale@0.4.5: - resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} - dependencies: - decimal.js-light: 2.5.1 - dev: false - - /recharts@2.15.1(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==} - engines: {node: '>=14'} - peerDependencies: - react: ^18.3.1 - react-dom: ^18.3.1 - dependencies: - clsx: 2.1.1 - eventemitter3: 4.0.7 - lodash: 4.17.21 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-is: 18.3.1 - react-smooth: 4.0.4(react-dom@18.3.1)(react@18.3.1) - recharts-scale: 0.4.5 - tiny-invariant: 1.3.3 - victory-vendor: 36.9.2 - dev: false - - /redent@3.0.0: - resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} - engines: {node: '>=8'} - dependencies: - indent-string: 4.0.0 - strip-indent: 3.0.0 - dev: true - - /redux@4.2.1: - resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} - dependencies: - '@babel/runtime': 7.26.9 - dev: false - - /reflect.getprototypeof@1.0.10: - resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - which-builtin-type: 1.2.1 - dev: true - - /regenerator-runtime@0.14.1: - resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} - - /regexp.prototype.flags@1.5.4: - resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-errors: 1.3.0 - get-proto: 1.0.1 - gopd: 1.2.0 - set-function-name: 2.0.2 - dev: true - - /require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - - /require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - dev: true - - /require-main-filename@2.0.0: - resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} - dev: false - - /requires-port@1.0.0: - resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - dev: false - - /resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - dev: true - - /resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - dev: true - - /resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - dev: true - - /resolve-protobuf-schema@2.1.0: - resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} - dependencies: - protocol-buffers-schema: 3.6.0 - dev: false - - /resolve@1.22.10: - resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} - engines: {node: '>= 0.4'} - hasBin: true - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - /resolve@2.0.0-next.5: - resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} - hasBin: true - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - dev: true - - /retry@0.12.0: - resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} - engines: {node: '>= 4'} - dev: false - - /reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - /rollup@4.34.8: - resolution: {integrity: sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - dependencies: - '@types/estree': 1.0.6 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.34.8 - '@rollup/rollup-android-arm64': 4.34.8 - '@rollup/rollup-darwin-arm64': 4.34.8 - '@rollup/rollup-darwin-x64': 4.34.8 - '@rollup/rollup-freebsd-arm64': 4.34.8 - '@rollup/rollup-freebsd-x64': 4.34.8 - '@rollup/rollup-linux-arm-gnueabihf': 4.34.8 - '@rollup/rollup-linux-arm-musleabihf': 4.34.8 - '@rollup/rollup-linux-arm64-gnu': 4.34.8 - '@rollup/rollup-linux-arm64-musl': 4.34.8 - '@rollup/rollup-linux-loongarch64-gnu': 4.34.8 - '@rollup/rollup-linux-powerpc64le-gnu': 4.34.8 - '@rollup/rollup-linux-riscv64-gnu': 4.34.8 - '@rollup/rollup-linux-s390x-gnu': 4.34.8 - '@rollup/rollup-linux-x64-gnu': 4.34.8 - '@rollup/rollup-linux-x64-musl': 4.34.8 - '@rollup/rollup-win32-arm64-msvc': 4.34.8 - '@rollup/rollup-win32-ia32-msvc': 4.34.8 - '@rollup/rollup-win32-x64-msvc': 4.34.8 - fsevents: 2.3.3 - dev: true - - /run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - dependencies: - queue-microtask: 1.2.3 - - /rw@1.3.3: - resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} - dev: false - - /safe-array-concat@1.1.3: - resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} - engines: {node: '>=0.4'} - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.3 - get-intrinsic: 1.3.0 - has-symbols: 1.1.0 - isarray: 2.0.5 - dev: true - - /safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - dev: false - - /safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - dev: true - - /safe-push-apply@1.0.0: - resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} - engines: {node: '>= 0.4'} - dependencies: - es-errors: 1.3.0 - isarray: 2.0.5 - dev: true - - /safe-regex-test@1.1.0: - resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.3 - es-errors: 1.3.0 - is-regex: 1.2.1 - dev: true - - /sax@1.4.1: - resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} - dev: false - - /scheduler@0.23.2: - resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} - dependencies: - loose-envify: 1.4.0 - - /schema-utils@4.3.0: - resolution: {integrity: sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==} - engines: {node: '>= 10.13.0'} - dependencies: - '@types/json-schema': 7.0.15 - ajv: 8.17.1 - ajv-formats: 2.1.1(ajv@8.17.1) - ajv-keywords: 5.1.0(ajv@8.17.1) - dev: true - - /semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - dev: true - - /semver@7.7.1: - resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} - engines: {node: '>=10'} - hasBin: true - - /serialize-javascript@6.0.2: - resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - dependencies: - randombytes: 2.1.0 - dev: true - - /set-blocking@2.0.0: - resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - dev: false - - /set-function-length@1.2.2: - resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} - engines: {node: '>= 0.4'} - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - dev: true - - /set-function-name@2.0.2: - resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} - engines: {node: '>= 0.4'} - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - functions-have-names: 1.2.3 - has-property-descriptors: 1.0.2 - dev: true - - /set-proto@1.0.0: - resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} - engines: {node: '>= 0.4'} - dependencies: - dunder-proto: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - dev: true - - /setimmediate@1.0.5: - resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} - dev: false - - /sharp@0.33.5: - resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - requiresBuild: true - dependencies: - color: 4.2.3 - detect-libc: 2.0.3 - semver: 7.7.1 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.33.5 - '@img/sharp-darwin-x64': 0.33.5 - '@img/sharp-libvips-darwin-arm64': 1.0.4 - '@img/sharp-libvips-darwin-x64': 1.0.4 - '@img/sharp-libvips-linux-arm': 1.0.5 - '@img/sharp-libvips-linux-arm64': 1.0.4 - '@img/sharp-libvips-linux-s390x': 1.0.4 - '@img/sharp-libvips-linux-x64': 1.0.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 - '@img/sharp-linux-arm': 0.33.5 - '@img/sharp-linux-arm64': 0.33.5 - '@img/sharp-linux-s390x': 0.33.5 - '@img/sharp-linux-x64': 0.33.5 - '@img/sharp-linuxmusl-arm64': 0.33.5 - '@img/sharp-linuxmusl-x64': 0.33.5 - '@img/sharp-wasm32': 0.33.5 - '@img/sharp-win32-ia32': 0.33.5 - '@img/sharp-win32-x64': 0.33.5 - dev: false - optional: true - - /sharp@0.34.1: - resolution: {integrity: sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - requiresBuild: true - dependencies: - color: 4.2.3 - detect-libc: 2.0.3 - semver: 7.7.1 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.1 - '@img/sharp-darwin-x64': 0.34.1 - '@img/sharp-libvips-darwin-arm64': 1.1.0 - '@img/sharp-libvips-darwin-x64': 1.1.0 - '@img/sharp-libvips-linux-arm': 1.1.0 - '@img/sharp-libvips-linux-arm64': 1.1.0 - '@img/sharp-libvips-linux-ppc64': 1.1.0 - '@img/sharp-libvips-linux-s390x': 1.1.0 - '@img/sharp-libvips-linux-x64': 1.1.0 - '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 - '@img/sharp-libvips-linuxmusl-x64': 1.1.0 - '@img/sharp-linux-arm': 0.34.1 - '@img/sharp-linux-arm64': 0.34.1 - '@img/sharp-linux-s390x': 0.34.1 - '@img/sharp-linux-x64': 0.34.1 - '@img/sharp-linuxmusl-arm64': 0.34.1 - '@img/sharp-linuxmusl-x64': 0.34.1 - '@img/sharp-wasm32': 0.34.1 - '@img/sharp-win32-ia32': 0.34.1 - '@img/sharp-win32-x64': 0.34.1 - dev: false - optional: true - - /shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - dependencies: - shebang-regex: 3.0.0 - - /shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - /side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} - engines: {node: '>= 0.4'} - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - dev: true - - /side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.3 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - dev: true - - /side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.3 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - dev: true - - /side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.0 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - dev: true - - /signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - dev: false - - /signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - - /simple-swizzle@0.2.2: - resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} - requiresBuild: true - dependencies: - is-arrayish: 0.3.2 - dev: false - optional: true - - /sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - dev: true - - /slash@5.1.0: - resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} - engines: {node: '>=14.16'} - dev: true - - /sonner@2.0.1(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-FRBphaehZ5tLdLcQ8g2WOIRE+Y7BCfWi5Zyd8bCvBjiW8TxxAyoWZIxS661Yz6TGPqFQ4VLzOF89WEYhfynSFQ==} - peerDependencies: - react: ^18.3.1 - react-dom: ^18.3.1 - dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /sort-asc@0.1.0: - resolution: {integrity: sha512-jBgdDd+rQ+HkZF2/OHCmace5dvpos/aWQpcxuyRs9QUbPRnkEJmYVo81PIGpjIdpOcsnJ4rGjStfDHsbn+UVyw==} - engines: {node: '>=0.10.0'} - dev: false - - /sort-desc@0.1.1: - resolution: {integrity: sha512-jfZacW5SKOP97BF5rX5kQfJmRVZP5/adDUTY8fCSPvNcXDVpUEe2pr/iKGlcyZzchRJZrswnp68fgk3qBXgkJw==} - engines: {node: '>=0.10.0'} - dev: false - - /sort-object@0.3.2: - resolution: {integrity: sha512-aAQiEdqFTTdsvUFxXm3umdo04J7MRljoVGbBlkH7BgNsMvVNAJyGj7C/wV1A8wHWAJj/YikeZbfuCKqhggNWGA==} - engines: {node: '>=0.10.0'} - dependencies: - sort-asc: 0.1.0 - sort-desc: 0.1.1 - dev: false - - /source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - /source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - dev: true - - /source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - dev: true - - /source-map@0.8.0-beta.0: - resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} - engines: {node: '>= 8'} - dependencies: - whatwg-url: 7.1.0 - dev: true - - /ssf@0.11.2: - resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} - engines: {node: '>=0.8'} - dependencies: - frac: 1.1.2 - dev: false - - /stable-hash@0.0.4: - resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==} - dev: true - - /steiger@0.5.5(typescript@5.6.3): - resolution: {integrity: sha512-d4eZwqJjfG7dKgwXKBwmZsNWSRqeQrRpEvEr58rhMmNbICcQsJ8cpK9n6nuish3kNjKyYzx6t2TpQYEO26xTFA==} - hasBin: true - dependencies: - '@clack/prompts': 0.8.2 - '@feature-sliced/steiger-plugin': 0.5.4(typescript@5.6.3) - chokidar: 4.0.3 - cosmiconfig: 9.0.0(typescript@5.6.3) - effector: 23.3.0 - empathic: 1.0.0 - fastest-levenshtein: 1.0.16 - globby: 14.1.0 - immer: 10.1.1 - lodash-es: 4.17.21 - micromatch: 4.0.8 - patronum: 2.3.0(effector@23.3.0) - picocolors: 1.1.1 - prexit: 2.3.0 - yargs: 17.7.2 - zod: 3.24.2 - zod-validation-error: 3.4.0(zod@3.24.2) - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /storybook@8.6.0(prettier@3.5.2): - resolution: {integrity: sha512-NEHCK2nXogw7WxAkJfMwPnc5Cmh/KqVNrdaFeXBAKhC2Zr1FTZIiCR5nSUT85run44Wkpmo7qvUoKvII9GOWbA==} - hasBin: true - peerDependencies: - prettier: ^2 || ^3 - peerDependenciesMeta: - prettier: - optional: true - dependencies: - '@storybook/core': 8.6.0(prettier@3.5.2)(storybook@8.6.0) - prettier: 3.5.2 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: true - - /streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - dev: false - - /string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - /string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.0 - - /string.prototype.includes@2.0.1: - resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - dev: true - - /string.prototype.matchall@4.0.12: - resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.3 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-symbols: 1.1.0 - internal-slot: 1.1.0 - regexp.prototype.flags: 1.5.4 - set-function-name: 2.0.2 - side-channel: 1.1.0 - dev: true - - /string.prototype.repeat@1.0.0: - resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} - dependencies: - define-properties: 1.2.1 - es-abstract: 1.23.9 - dev: true - - /string.prototype.trim@1.2.10: - resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.3 - define-data-property: 1.1.4 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-object-atoms: 1.1.1 - has-property-descriptors: 1.0.2 - dev: true - - /string.prototype.trimend@1.0.9: - resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.3 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - dev: true - - /string.prototype.trimstart@1.0.8: - resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - dev: true - - /string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - dependencies: - safe-buffer: 5.1.2 - dev: false - - /strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - dependencies: - ansi-regex: 5.0.1 - - /strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} - engines: {node: '>=12'} - dependencies: - ansi-regex: 6.1.0 - - /strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} - dev: true - - /strip-indent@3.0.0: - resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} - engines: {node: '>=8'} - dependencies: - min-indent: 1.0.1 - dev: true - - /strip-indent@4.0.0: - resolution: {integrity: sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==} - engines: {node: '>=12'} - dependencies: - min-indent: 1.0.1 - dev: true - - /strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - dev: true - - /styled-jsx@5.1.6(react@18.3.1): - resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} - engines: {node: '>= 12.0.0'} - peerDependencies: - '@babel/core': '*' - babel-plugin-macros: '*' - react: ^18.3.1 - peerDependenciesMeta: - '@babel/core': - optional: true - babel-plugin-macros: - optional: true - dependencies: - client-only: 0.0.1 - react: 18.3.1 - dev: false - - /sucrase@3.35.0: - resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - dependencies: - '@jridgewell/gen-mapping': 0.3.8 - commander: 4.1.1 - glob: 10.4.5 - lines-and-columns: 1.2.4 - mz: 2.7.0 - pirates: 4.0.6 - ts-interface-checker: 0.1.13 - - /supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - dependencies: - has-flag: 4.0.0 - dev: true - - /supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - dependencies: - has-flag: 4.0.0 - dev: true - - /supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - /tailwind-merge@3.0.2: - resolution: {integrity: sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==} - dev: false - - /tailwindcss-animate@1.0.7(tailwindcss@3.4.17): - resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} - peerDependencies: - tailwindcss: '>=3.0.0 || insiders' - dependencies: - tailwindcss: 3.4.17 - dev: false - - /tailwindcss@3.4.17: - resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} - engines: {node: '>=14.0.0'} - hasBin: true - dependencies: - '@alloc/quick-lru': 5.2.0 - arg: 5.0.2 - chokidar: 3.6.0 - didyoumean: 1.2.2 - dlv: 1.1.3 - fast-glob: 3.3.3 - glob-parent: 6.0.2 - is-glob: 4.0.3 - jiti: 1.21.7 - lilconfig: 3.1.3 - micromatch: 4.0.8 - normalize-path: 3.0.0 - object-hash: 3.0.0 - picocolors: 1.1.1 - postcss: 8.5.3 - postcss-import: 15.1.0(postcss@8.5.3) - postcss-js: 4.0.1(postcss@8.5.3) - postcss-load-config: 4.0.2(postcss@8.5.3) - postcss-nested: 6.2.0(postcss@8.5.3) - postcss-selector-parser: 6.1.2 - resolve: 1.22.10 - sucrase: 3.35.0 - transitivePeerDependencies: - - ts-node - - /tapable@2.2.1: - resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} - engines: {node: '>=6'} - dev: true - - /terser-webpack-plugin@5.3.13(webpack@5.98.0): - resolution: {integrity: sha512-JG3pBixF6kx2o0Yfz2K6pqh72DpwTI08nooHd06tcj5WyIt5SsSiUYqRT+kemrGUNSuSzVhwfZ28aO8gogajNQ==} - engines: {node: '>= 10.13.0'} - peerDependencies: - '@swc/core': '*' - esbuild: '*' - uglify-js: '*' - webpack: ^5.1.0 - peerDependenciesMeta: - '@swc/core': - optional: true - esbuild: - optional: true - uglify-js: - optional: true - dependencies: - '@jridgewell/trace-mapping': 0.3.25 - jest-worker: 27.5.1 - schema-utils: 4.3.0 - serialize-javascript: 6.0.2 - terser: 5.39.0 - webpack: 5.98.0 - dev: true - - /terser@5.39.0: - resolution: {integrity: sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==} - engines: {node: '>=10'} - hasBin: true - dependencies: - '@jridgewell/source-map': 0.3.6 - acorn: 8.14.0 - commander: 2.20.3 - source-map-support: 0.5.21 - dev: true - - /thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} - dependencies: - thenify: 3.3.1 - - /thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - dependencies: - any-promise: 1.3.0 - - /tiny-invariant@1.3.3: - resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - - /tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - dev: true - - /tinyglobby@0.2.12: - resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} - engines: {node: '>=12.0.0'} - dependencies: - fdir: 6.4.3(picomatch@4.0.2) - picomatch: 4.0.2 - dev: true - - /tinyrainbow@1.2.0: - resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} - engines: {node: '>=14.0.0'} - dev: true - - /tinyspy@3.0.2: - resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} - engines: {node: '>=14.0.0'} - dev: true - - /to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - dependencies: - is-number: 7.0.0 - - /tr46@1.0.1: - resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} - dependencies: - punycode: 2.3.1 - dev: true - - /tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true - dev: true - - /ts-api-utils@2.0.1(typescript@5.6.3): - resolution: {integrity: sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==} - engines: {node: '>=18.12'} - peerDependencies: - typescript: '>=4.8.4' - dependencies: - typescript: 5.6.3 - dev: true - - /ts-api-utils@2.0.1(typescript@5.7.3): - resolution: {integrity: sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==} - engines: {node: '>=18.12'} - peerDependencies: - typescript: '>=4.8.4' - dependencies: - typescript: 5.7.3 - dev: true - - /ts-dedent@2.2.0: - resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} - engines: {node: '>=6.10'} - dev: true - - /ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - - /tsconfck@3.1.5(typescript@5.6.3): - resolution: {integrity: sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg==} - engines: {node: ^18 || >=20} - hasBin: true - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - dependencies: - typescript: 5.6.3 - dev: true - - /tsconfig-paths@3.15.0: - resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - dependencies: - '@types/json5': 0.0.29 - json5: 1.0.2 - minimist: 1.2.8 - strip-bom: 3.0.0 - dev: true - - /tsconfig-paths@4.2.0: - resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} - engines: {node: '>=6'} - dependencies: - json5: 2.2.3 - minimist: 1.2.8 - strip-bom: 3.0.0 - dev: true - - /tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - /tsup@8.3.6(postcss@8.5.3)(typescript@5.6.3): - resolution: {integrity: sha512-XkVtlDV/58S9Ye0JxUUTcrQk4S+EqlOHKzg6Roa62rdjL1nGWNUstG0xgI4vanHdfIpjP448J8vlN0oK6XOJ5g==} - engines: {node: '>=18'} - hasBin: true - peerDependencies: - '@microsoft/api-extractor': ^7.36.0 - '@swc/core': ^1 - postcss: ^8.4.12 - typescript: '>=4.5.0' - peerDependenciesMeta: - '@microsoft/api-extractor': - optional: true - '@swc/core': - optional: true - postcss: - optional: true - typescript: - optional: true - dependencies: - bundle-require: 5.1.0(esbuild@0.24.2) - cac: 6.7.14 - chokidar: 4.0.3 - consola: 3.4.0 - debug: 4.4.0 - esbuild: 0.24.2 - joycon: 3.1.1 - picocolors: 1.1.1 - postcss: 8.5.3 - postcss-load-config: 6.0.1(postcss@8.5.3) - resolve-from: 5.0.0 - rollup: 4.34.8 - source-map: 0.8.0-beta.0 - sucrase: 3.35.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.12 - tree-kill: 1.2.2 - typescript: 5.6.3 - transitivePeerDependencies: - - jiti - - supports-color - - tsx - - yaml - dev: true - - /turbo-darwin-64@2.4.2: - resolution: {integrity: sha512-HFfemyWB60CJtEvVQj9yby5rkkWw9fLAdLtAPGtPQoU3tKh8t/uzCAZKso2aPVbib9vGUuGbPGoGpaRXdVhj5g==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /turbo-darwin-arm64@2.4.2: - resolution: {integrity: sha512-uwSx1dsBSSFeEC0nxyx2O219FEsS/haiESaWwE9JI8mHkQK61s6w6fN2G586krKxyNam4AIxRltleL+O2Em94g==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /turbo-linux-64@2.4.2: - resolution: {integrity: sha512-Fy/uL8z/LAYcPbm7a1LwFnTY9pIi5FAi12iuHsgB7zHjdh4eeIKS2NIg4nroAmTcUTUZ0/cVTo4bDOCUcS3aKw==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /turbo-linux-arm64@2.4.2: - resolution: {integrity: sha512-AEA0d8h5W/K6iiXfEgiNwWt0yqRL1NpBs8zQCLdc4/L7WeYeJW3sORWX8zt7xhutF/KW9gTm8ehKpiK6cCIsAA==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /turbo-windows-64@2.4.2: - resolution: {integrity: sha512-CybtIZ9wRgnnNFVN9En9G+rxsO+mwU81fvW4RpE8BWyNEkhQ8J28qYf4PaimueMxGHHp/28i/G7Kcdn2GAWG0g==} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /turbo-windows-arm64@2.4.2: - resolution: {integrity: sha512-7V0yneVPL8Y3TgrkUIjw7Odmwu1tHnyIiPHFM7eFcA7U+H6hPXyCxge7nC3wOKfjhKCQqUm+Vf/k6kjmLz5G4g==} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /turbo@2.4.2: - resolution: {integrity: sha512-Qxi0ioQCxMRUCcHKHZkTnYH8e7XCpNfg9QiJcyfWIc+ZXeaCjzV5rCGlbQlTXMAtI8qgfP8fZADv3CFtPwqdPQ==} - hasBin: true - optionalDependencies: - turbo-darwin-64: 2.4.2 - turbo-darwin-arm64: 2.4.2 - turbo-linux-64: 2.4.2 - turbo-linux-arm64: 2.4.2 - turbo-windows-64: 2.4.2 - turbo-windows-arm64: 2.4.2 - dev: true - - /tus-js-client@4.3.1: - resolution: {integrity: sha512-ZLeYmjrkaU1fUsKbIi8JML52uAocjEZtBx4DKjRrqzrZa0O4MYwT6db+oqePlspV+FxXJAyFBc/L5gwUi2OFsg==} - engines: {node: '>=18'} - dependencies: - buffer-from: 1.1.2 - combine-errors: 3.0.3 - is-stream: 2.0.1 - js-base64: 3.7.7 - lodash.throttle: 4.1.1 - proper-lockfile: 4.1.2 - url-parse: 1.5.10 - dev: false - - /type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - dependencies: - prelude-ls: 1.2.1 - dev: true - - /type-fest@2.19.0: - resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} - engines: {node: '>=12.20'} - dev: true - - /typed-array-buffer@1.0.3: - resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.3 - es-errors: 1.3.0 - is-typed-array: 1.1.15 - dev: true - - /typed-array-byte-length@1.0.3: - resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - dev: true - - /typed-array-byte-offset@1.0.4: - resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} - engines: {node: '>= 0.4'} - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - reflect.getprototypeof: 1.0.10 - dev: true - - /typed-array-length@1.0.7: - resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - is-typed-array: 1.1.15 - possible-typed-array-names: 1.1.0 - reflect.getprototypeof: 1.0.10 - dev: true - - /typescript-eslint@8.25.0(eslint@9.21.0)(typescript@5.6.3): - resolution: {integrity: sha512-TxRdQQLH4g7JkoFlYG3caW5v1S6kEkz8rqt80iQJZUYPq1zD1Ra7HfQBJJ88ABRaMvHAXnwRvRB4V+6sQ9xN5Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' - dependencies: - '@typescript-eslint/eslint-plugin': 8.25.0(@typescript-eslint/parser@8.25.0)(eslint@9.21.0)(typescript@5.6.3) - '@typescript-eslint/parser': 8.25.0(eslint@9.21.0)(typescript@5.6.3) - '@typescript-eslint/utils': 8.25.0(eslint@9.21.0)(typescript@5.6.3) - eslint: 9.21.0 - typescript: 5.6.3 - transitivePeerDependencies: - - supports-color - dev: true - - /typescript@5.6.3: - resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} - engines: {node: '>=14.17'} - hasBin: true - dev: true - - /typescript@5.7.3: - resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} - engines: {node: '>=14.17'} - hasBin: true - dev: true - - /unbox-primitive@1.1.0: - resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.3 - has-bigints: 1.1.0 - has-symbols: 1.1.0 - which-boxed-primitive: 1.1.1 - dev: true - - /undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} - dev: true - - /undici-types@6.20.0: - resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} - - /unicorn-magic@0.3.0: - resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} - engines: {node: '>=18'} - dev: true - - /unplugin@1.16.1: - resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} - engines: {node: '>=14.0.0'} - dependencies: - acorn: 8.14.0 - webpack-virtual-modules: 0.6.2 - dev: true - - /update-browserslist-db@1.1.3(browserslist@4.24.4): - resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - dependencies: - browserslist: 4.24.4 - escalade: 3.2.0 - picocolors: 1.1.1 - dev: true - - /uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - dependencies: - punycode: 2.3.1 - dev: true - - /url-parse@1.5.10: - resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} - dependencies: - querystringify: 2.2.0 - requires-port: 1.0.0 - dev: false - - /use-callback-ref@1.3.3(@types/react@18.3.18)(react@18.3.1): - resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': ^18.3.18 - react: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.3.18 - react: 18.3.1 - tslib: 2.8.1 - dev: false - - /use-sidecar@1.1.3(@types/react@18.3.18)(react@18.3.1): - resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': ^18.3.18 - react: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.3.18 - detect-node-es: 1.1.0 - react: 18.3.1 - tslib: 2.8.1 - dev: false - - /util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - /util@0.10.4: - resolution: {integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==} - dependencies: - inherits: 2.0.3 - dev: false - - /util@0.12.5: - resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} - dependencies: - inherits: 2.0.4 - is-arguments: 1.2.0 - is-generator-function: 1.1.0 - is-typed-array: 1.1.15 - which-typed-array: 1.1.18 - dev: true - - /uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} - hasBin: true - dev: true - - /victory-vendor@36.9.2: - resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} - dependencies: - '@types/d3-array': 3.2.1 - '@types/d3-ease': 3.0.2 - '@types/d3-interpolate': 3.0.4 - '@types/d3-scale': 4.0.9 - '@types/d3-shape': 3.1.7 - '@types/d3-time': 3.0.4 - '@types/d3-timer': 3.0.2 - d3-array: 3.2.4 - d3-ease: 3.0.1 - d3-interpolate: 3.0.1 - d3-scale: 4.0.2 - d3-shape: 3.2.0 - d3-time: 3.1.0 - d3-timer: 3.0.1 - dev: false - - /vite-tsconfig-paths@5.1.4(typescript@5.6.3)(vite@6.2.0): - resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} - peerDependencies: - vite: '*' - peerDependenciesMeta: - vite: - optional: true - dependencies: - debug: 4.4.0 - globrex: 0.1.2 - tsconfck: 3.1.5(typescript@5.6.3) - vite: 6.2.0(@types/node@22.13.9) - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /vite@5.4.18(@types/node@22.13.9): - resolution: {integrity: sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - dependencies: - '@types/node': 22.13.9 - esbuild: 0.21.5 - postcss: 8.5.3 - rollup: 4.34.8 - optionalDependencies: - fsevents: 2.3.3 - dev: true - - /vite@6.2.0(@types/node@22.13.5): - resolution: {integrity: sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - jiti: '>=1.21.0' - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - dependencies: - '@types/node': 22.13.5 - esbuild: 0.25.0 - postcss: 8.5.3 - rollup: 4.34.8 - optionalDependencies: - fsevents: 2.3.3 - dev: true - - /vite@6.2.0(@types/node@22.13.9): - resolution: {integrity: sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - jiti: '>=1.21.0' - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - dependencies: - '@types/node': 22.13.9 - esbuild: 0.25.0 - postcss: 8.5.3 - rollup: 4.34.8 - optionalDependencies: - fsevents: 2.3.3 - dev: true - - /watchpack@2.4.2: - resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} - engines: {node: '>=10.13.0'} - dependencies: - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - dev: true - - /web-worker@1.5.0: - resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==} - dev: false - - /webidl-conversions@4.0.2: - resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} - dev: true - - /webpack-sources@3.2.3: - resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} - engines: {node: '>=10.13.0'} - dev: true - - /webpack-virtual-modules@0.6.2: - resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - dev: true - - /webpack@5.98.0: - resolution: {integrity: sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==} - engines: {node: '>=10.13.0'} - hasBin: true - peerDependencies: - webpack-cli: '*' - peerDependenciesMeta: - webpack-cli: - optional: true - dependencies: - '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.6 - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/wasm-edit': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.14.0 - browserslist: 4.24.4 - chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.1 - es-module-lexer: 1.6.0 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.0 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 4.3.0 - tapable: 2.2.1 - terser-webpack-plugin: 5.3.13(webpack@5.98.0) - watchpack: 2.4.2 - webpack-sources: 3.2.3 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - dev: true - - /whatwg-url@7.1.0: - resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} - dependencies: - lodash.sortby: 4.7.0 - tr46: 1.0.1 - webidl-conversions: 4.0.2 - dev: true - - /which-boxed-primitive@1.1.1: - resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} - engines: {node: '>= 0.4'} - dependencies: - is-bigint: 1.1.0 - is-boolean-object: 1.2.2 - is-number-object: 1.1.1 - is-string: 1.1.1 - is-symbol: 1.1.1 - dev: true - - /which-builtin-type@1.2.1: - resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.3 - function.prototype.name: 1.1.8 - has-tostringtag: 1.0.2 - is-async-function: 2.1.1 - is-date-object: 1.1.0 - is-finalizationregistry: 1.1.1 - is-generator-function: 1.1.0 - is-regex: 1.2.1 - is-weakref: 1.1.1 - isarray: 2.0.5 - which-boxed-primitive: 1.1.1 - which-collection: 1.0.2 - which-typed-array: 1.1.18 - dev: true - - /which-collection@1.0.2: - resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} - engines: {node: '>= 0.4'} - dependencies: - is-map: 2.0.3 - is-set: 2.0.3 - is-weakmap: 2.0.2 - is-weakset: 2.0.4 - dev: true - - /which-module@2.0.1: - resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} - dev: false - - /which-typed-array@1.1.18: - resolution: {integrity: sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==} - engines: {node: '>= 0.4'} - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - call-bound: 1.0.3 - for-each: 0.3.5 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - dev: true - - /which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - dependencies: - isexe: 2.0.0 - - /wkt-parser@1.4.0: - resolution: {integrity: sha512-qpwO7Ihds/YYDTi1aADFTI1Sm9YC/tTe3SHD24EeIlZxy7Ik6a1b4HOz7jAi0xdUAw487duqpo8OGu+Tf4nwlQ==} - dev: false - - /wmf@1.0.2: - resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} - engines: {node: '>=0.8'} - dev: false - - /word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - dev: true - - /word@0.3.0: - resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} - engines: {node: '>=0.8'} - dev: false - - /wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - dev: false - - /wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - /wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - dependencies: - ansi-styles: 6.2.1 - string-width: 5.1.2 - strip-ansi: 7.1.0 - - /ws@8.18.1: - resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - dev: true - - /xlsx-populate@1.21.0: - resolution: {integrity: sha512-8v2Gm8BehXo6LU7KT802QoXTPkYY1SKk5V8g/UuYZnNB3JzXqud/P99Pxr2yXeKyt+sKlCatmidz6jQNie1hRw==} - dependencies: - cfb: 1.2.2 - jszip: 3.10.1 - lodash: 4.17.21 - sax: 1.4.1 - dev: false - - /xlsx@0.18.5: - resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} - engines: {node: '>=0.8'} - hasBin: true - dependencies: - adler-32: 1.3.1 - cfb: 1.2.2 - codepage: 1.15.0 - crc-32: 1.2.2 - ssf: 0.11.2 - wmf: 1.0.2 - word: 0.3.0 - dev: false - - /xml-utils@1.10.2: - resolution: {integrity: sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==} - dev: false - - /y18n@4.0.3: - resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} - dev: false - - /y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - dev: true - - /yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - dev: true - - /yaml@2.7.0: - resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} - engines: {node: '>= 14'} - hasBin: true - - /yargs-parser@18.1.3: - resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} - engines: {node: '>=6'} - dependencies: - camelcase: 5.3.1 - decamelize: 1.2.0 - dev: false - - /yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - dev: true - - /yargs@15.4.1: - resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} - engines: {node: '>=8'} - dependencies: - cliui: 6.0.0 - decamelize: 1.2.0 - find-up: 4.1.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - require-main-filename: 2.0.0 - set-blocking: 2.0.0 - string-width: 4.2.3 - which-module: 2.0.1 - y18n: 4.0.3 - yargs-parser: 18.1.3 - dev: false - - /yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - dev: true - - /yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - dev: true - - /zod-validation-error@3.4.0(zod@3.24.2): - resolution: {integrity: sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==} - engines: {node: '>=18.0.0'} - peerDependencies: - zod: ^3.18.0 - dependencies: - zod: 3.24.2 - dev: true - - /zod@3.24.2: - resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} - - /zstddec@0.1.0: - resolution: {integrity: sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==} - dev: false - - /zustand@5.0.3(@types/react@18.3.18)(react@18.3.1): - resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==} - engines: {node: '>=12.20.0'} - peerDependencies: - '@types/react': ^18.3.18 - immer: '>=9.0.6' - react: ^18.3.1 - use-sync-external-store: '>=1.2.0' - peerDependenciesMeta: - '@types/react': - optional: true - immer: - optional: true - react: - optional: true - use-sync-external-store: - optional: true - dependencies: - '@types/react': 18.3.18 - react: 18.3.1 - dev: false